@utopia-studio-design/design-system-cli 0.7.1 → 0.8.0
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 +18 -5
- package/bin/utopia-ds.mjs +16 -3
- package/data/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
- package/data/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
- package/data/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
- package/data/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
- package/data/blocks/semantic-pattern-parts/PatternParts.tsx +7 -0
- package/data/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
- package/data/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
- package/data/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
- package/data/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
- package/data/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
- package/data/blocks/semantic-pattern-parts/auth-parts.css +27 -0
- package/data/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
- package/data/blocks/semantic-pattern-parts/product-parts.css +25 -0
- package/data/blocks/semantic-pattern-parts/types.ts +3 -0
- package/data/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
- package/data/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
- package/data/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
- package/data/blocks/ux-pattern-examples/OnboardingExample.tsx +15 -0
- package/data/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
- package/data/blocks/ux-pattern-examples/ReviewExample.tsx +14 -0
- package/data/blocks/ux-pattern-examples/SignupExample.tsx +22 -0
- package/data/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
- package/data/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
- package/data/blocks/ux-pattern-examples/copy.ts +6 -0
- package/data/blocks/ux-pattern-examples/ux-examples.css +18 -0
- package/data/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
- package/data/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
- package/data/docs/ceramic-release-2026-09-06.md +2 -0
- package/data/docs/create-a-brandbook.md +79 -0
- package/data/docs/dextrum-document-family.md +47 -0
- package/data/docs/guide.md +2 -0
- package/data/docs/presentation-asset-repository.md +5 -0
- package/data/docs/ui-parts-qa.md +172 -0
- package/data/docs/ui-parts.md +105 -0
- package/data/docs/ux-pattern-adaptation-qa.md +38 -0
- package/data/docs/ux-pattern-adaptation.md +41 -0
- package/data/docs/ux-pattern-migration.md +111 -0
- package/data/docs/ux-pattern-release-2026-09-13.md +26 -0
- package/data/docs/ux-source-evidence/account-auth.json +41 -0
- package/data/docs/ux-source-evidence/account-forgot-password.json +36 -0
- package/data/docs/ux-source-evidence/account-invite.json +63 -0
- package/data/docs/ux-source-evidence/account-locked.json +63 -0
- package/data/docs/ux-source-evidence/account-login.json +28 -0
- package/data/docs/ux-source-evidence/account-mfa.json +62 -0
- package/data/docs/ux-source-evidence/account-onboarding-credentials.json +52 -0
- package/data/docs/ux-source-evidence/account-onboarding-legal.json +56 -0
- package/data/docs/ux-source-evidence/account-onboarding-orientation.json +66 -0
- package/data/docs/ux-source-evidence/account-onboarding-profile.json +50 -0
- package/data/docs/ux-source-evidence/account-onboarding.json +69 -0
- package/data/docs/ux-source-evidence/account-reset-password.json +27 -0
- package/data/docs/ux-source-evidence/account-session-expired.json +62 -0
- package/data/docs/ux-source-evidence/account-signup.json +33 -0
- package/data/docs/ux-source-evidence/account-verify-email.json +34 -0
- package/data/docs/ux-source-evidence/account-welcome.json +41 -0
- package/data/docs/ux-source-evidence/admin-analytics.json +54 -0
- package/data/docs/ux-source-evidence/admin-audit.json +31 -0
- package/data/docs/ux-source-evidence/admin-broadcasts-new.json +36 -0
- package/data/docs/ux-source-evidence/admin-broadcasts.json +41 -0
- package/data/docs/ux-source-evidence/admin-closures.json +52 -0
- package/data/docs/ux-source-evidence/admin-devices.json +51 -0
- package/data/docs/ux-source-evidence/admin-escalations.json +35 -0
- package/data/docs/ux-source-evidence/admin-incidents.json +54 -0
- package/data/docs/ux-source-evidence/admin-loading.json +17 -0
- package/data/docs/ux-source-evidence/admin-onboarding-detail.json +80 -0
- package/data/docs/ux-source-evidence/admin-onboarding.json +62 -0
- package/data/docs/ux-source-evidence/admin-payments.json +45 -0
- package/data/docs/ux-source-evidence/admin-providers.json +38 -0
- package/data/docs/ux-source-evidence/admin-settings.json +18 -0
- package/data/docs/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
- package/data/docs/ux-source-evidence/admin-subscriptions-detail.json +48 -0
- package/data/docs/ux-source-evidence/admin-subscriptions-new.json +43 -0
- package/data/docs/ux-source-evidence/admin-subscriptions.json +41 -0
- package/data/docs/ux-source-evidence/admin-transactions.json +41 -0
- package/data/docs/ux-source-evidence/error.json +30 -0
- package/data/docs/ux-source-evidence/loading.json +19 -0
- package/data/docs/ux-source-evidence/marketing-blog.json +48 -0
- package/data/docs/ux-source-evidence/marketing-contact.json +36 -0
- package/data/docs/ux-source-evidence/marketing.json +64 -0
- package/data/docs/ux-source-evidence/not-found.json +28 -0
- package/data/docs/ux-source-evidence/portal-loading.json +17 -0
- package/data/docs/ux-source-evidence/portal-mentor-messages.json +31 -0
- package/data/docs/ux-source-evidence/portal-mentor-profile.json +23 -0
- package/data/docs/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
- package/data/docs/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
- package/data/docs/ux-source-evidence/portal-mentor-settings.json +18 -0
- package/data/docs/ux-source-evidence/portal-mentor.json +38 -0
- package/data/docs/ux-source-evidence/portal-sessions-detail-live.json +48 -0
- package/data/docs/ux-source-evidence/portal-sponsor-billing.json +28 -0
- package/data/docs/ux-source-evidence/portal-sponsor-profile.json +22 -0
- package/data/docs/ux-source-evidence/portal-sponsor-sessions.json +28 -0
- package/data/docs/ux-source-evidence/portal-sponsor-settings.json +18 -0
- package/data/docs/ux-source-evidence/portal-sponsor-trainees.json +36 -0
- package/data/docs/ux-source-evidence/portal-sponsor.json +52 -0
- package/data/docs/ux-source-evidence/portal-trainee-competency.json +42 -0
- package/data/docs/ux-source-evidence/portal-trainee-credits.json +40 -0
- package/data/docs/ux-source-evidence/portal-trainee-mentors.json +48 -0
- package/data/docs/ux-source-evidence/portal-trainee-messages.json +32 -0
- package/data/docs/ux-source-evidence/portal-trainee-notifications.json +35 -0
- package/data/docs/ux-source-evidence/portal-trainee-profile.json +26 -0
- package/data/docs/ux-source-evidence/portal-trainee-request.json +54 -0
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
- package/data/docs/ux-source-evidence/portal-trainee-sessions.json +36 -0
- package/data/docs/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
- package/data/docs/ux-source-evidence/portal-trainee-settings.json +18 -0
- package/data/docs/ux-source-evidence/portal-trainee.json +44 -0
- package/data/docs/ux-source-evidence/portal-tv-detail.json +39 -0
- package/data/docs/ux-source-evidence/portal-tv.json +44 -0
- package/data/docs/ux-source-evidence/shared-admin-admin-shell.json +35 -0
- package/data/docs/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
- package/data/docs/ux-source-evidence/shared-auth-auth-shell.json +25 -0
- package/data/docs/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
- package/data/docs/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
- package/data/docs/ux-source-evidence/shared-auth-status-indicator.json +41 -0
- package/data/docs/ux-source-evidence/shared-portal-empty-state.json +24 -0
- package/data/docs/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
- package/data/docs/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
- package/data/docs/ux-source-evidence/shared-portal-portal-shell.json +36 -0
- package/data/docs/ux-source-evidence/shared-portal-profile-form.json +32 -0
- package/data/docs/ux-source-evidence/shared-portal-settings-view.json +81 -0
- package/data/docs/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
- package/data/docs/ux-source-evidence/shared-portal-user-menu.json +34 -0
- package/data/manifests/blocks.json +230 -21
- package/data/manifests/brandbook-skill.json +33 -0
- package/data/manifests/catalog.json +1 -0
- package/data/manifests/patterns.json +358 -0
- package/data/manifests/theme-dextrum.json +132 -21
- package/data/manifests/themes.json +253 -39
- package/data/manifests/ui-patterns.json +1029 -0
- package/data/manifests/ux-examples.json +2177 -0
- package/data/manifests/ux-patterns.json +1135 -0
- package/data/manifests/ux-source-inventory.json +4669 -0
- package/data/ui-parts/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/ui-parts/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/auth-parts.css +27 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/product-parts.css +25 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/types.ts +3 -0
- package/data/ui-parts/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
- package/data/ui-parts/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
- package/data/ui-parts/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
- package/data/ui-parts/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
- package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
- package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
- package/data/ui-parts/blocks/ux-pattern-examples/copy.ts +6 -0
- package/data/ui-parts/blocks/ux-pattern-examples/ux-examples.css +18 -0
- package/data/ui-parts/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
- package/data/ui-parts/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
- package/data/ux-source-evidence/account-auth.json +41 -0
- package/data/ux-source-evidence/account-forgot-password.json +36 -0
- package/data/ux-source-evidence/account-invite.json +63 -0
- package/data/ux-source-evidence/account-locked.json +63 -0
- package/data/ux-source-evidence/account-login.json +28 -0
- package/data/ux-source-evidence/account-mfa.json +62 -0
- package/data/ux-source-evidence/account-onboarding-credentials.json +52 -0
- package/data/ux-source-evidence/account-onboarding-legal.json +56 -0
- package/data/ux-source-evidence/account-onboarding-orientation.json +66 -0
- package/data/ux-source-evidence/account-onboarding-profile.json +50 -0
- package/data/ux-source-evidence/account-onboarding.json +69 -0
- package/data/ux-source-evidence/account-reset-password.json +27 -0
- package/data/ux-source-evidence/account-session-expired.json +62 -0
- package/data/ux-source-evidence/account-signup.json +33 -0
- package/data/ux-source-evidence/account-verify-email.json +34 -0
- package/data/ux-source-evidence/account-welcome.json +41 -0
- package/data/ux-source-evidence/admin-analytics.json +54 -0
- package/data/ux-source-evidence/admin-audit.json +31 -0
- package/data/ux-source-evidence/admin-broadcasts-new.json +36 -0
- package/data/ux-source-evidence/admin-broadcasts.json +41 -0
- package/data/ux-source-evidence/admin-closures.json +52 -0
- package/data/ux-source-evidence/admin-devices.json +51 -0
- package/data/ux-source-evidence/admin-escalations.json +35 -0
- package/data/ux-source-evidence/admin-incidents.json +54 -0
- package/data/ux-source-evidence/admin-loading.json +17 -0
- package/data/ux-source-evidence/admin-onboarding-detail.json +80 -0
- package/data/ux-source-evidence/admin-onboarding.json +62 -0
- package/data/ux-source-evidence/admin-payments.json +45 -0
- package/data/ux-source-evidence/admin-providers.json +38 -0
- package/data/ux-source-evidence/admin-settings.json +18 -0
- package/data/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
- package/data/ux-source-evidence/admin-subscriptions-detail.json +48 -0
- package/data/ux-source-evidence/admin-subscriptions-new.json +43 -0
- package/data/ux-source-evidence/admin-subscriptions.json +41 -0
- package/data/ux-source-evidence/admin-transactions.json +41 -0
- package/data/ux-source-evidence/error.json +30 -0
- package/data/ux-source-evidence/loading.json +19 -0
- package/data/ux-source-evidence/marketing-blog.json +48 -0
- package/data/ux-source-evidence/marketing-contact.json +36 -0
- package/data/ux-source-evidence/marketing.json +64 -0
- package/data/ux-source-evidence/not-found.json +28 -0
- package/data/ux-source-evidence/portal-loading.json +17 -0
- package/data/ux-source-evidence/portal-mentor-messages.json +31 -0
- package/data/ux-source-evidence/portal-mentor-profile.json +23 -0
- package/data/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
- package/data/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
- package/data/ux-source-evidence/portal-mentor-settings.json +18 -0
- package/data/ux-source-evidence/portal-mentor.json +38 -0
- package/data/ux-source-evidence/portal-sessions-detail-live.json +48 -0
- package/data/ux-source-evidence/portal-sponsor-billing.json +28 -0
- package/data/ux-source-evidence/portal-sponsor-profile.json +22 -0
- package/data/ux-source-evidence/portal-sponsor-sessions.json +28 -0
- package/data/ux-source-evidence/portal-sponsor-settings.json +18 -0
- package/data/ux-source-evidence/portal-sponsor-trainees.json +36 -0
- package/data/ux-source-evidence/portal-sponsor.json +52 -0
- package/data/ux-source-evidence/portal-trainee-competency.json +42 -0
- package/data/ux-source-evidence/portal-trainee-credits.json +40 -0
- package/data/ux-source-evidence/portal-trainee-mentors.json +48 -0
- package/data/ux-source-evidence/portal-trainee-messages.json +32 -0
- package/data/ux-source-evidence/portal-trainee-notifications.json +35 -0
- package/data/ux-source-evidence/portal-trainee-profile.json +26 -0
- package/data/ux-source-evidence/portal-trainee-request.json +54 -0
- package/data/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
- package/data/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
- package/data/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
- package/data/ux-source-evidence/portal-trainee-sessions.json +36 -0
- package/data/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
- package/data/ux-source-evidence/portal-trainee-settings.json +18 -0
- package/data/ux-source-evidence/portal-trainee.json +44 -0
- package/data/ux-source-evidence/portal-tv-detail.json +39 -0
- package/data/ux-source-evidence/portal-tv.json +44 -0
- package/data/ux-source-evidence/shared-admin-admin-shell.json +35 -0
- package/data/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
- package/data/ux-source-evidence/shared-auth-auth-shell.json +25 -0
- package/data/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
- package/data/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
- package/data/ux-source-evidence/shared-auth-status-indicator.json +41 -0
- package/data/ux-source-evidence/shared-portal-empty-state.json +24 -0
- package/data/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
- package/data/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
- package/data/ux-source-evidence/shared-portal-portal-shell.json +36 -0
- package/data/ux-source-evidence/shared-portal-profile-form.json +32 -0
- package/data/ux-source-evidence/shared-portal-settings-view.json +81 -0
- package/data/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
- package/data/ux-source-evidence/shared-portal-user-menu.json +34 -0
- package/lib/api.mjs +228 -4
- package/package.json +2 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Complete UX source migration ledger
|
|
2
|
+
|
|
3
|
+
Status: **IN PROGRESS — broad representative coverage, not full source-interaction parity.**
|
|
4
|
+
|
|
5
|
+
Pinned source: `The-Utopia-Studio/mentix-design` at `6b4b2770d04e42cc2e5453c3d80e35c282b3438f`.
|
|
6
|
+
Remote HEAD was checked against this revision on 2026-09-13.
|
|
7
|
+
|
|
8
|
+
## What is actually done
|
|
9
|
+
|
|
10
|
+
- Source inventory: 66 page definitions + shared/system helpers = 85 entries.
|
|
11
|
+
- 104 indexed screenshot states, plus pages omitted from that screenshot index.
|
|
12
|
+
- 218 exact named function fragments available as bounded, internal MCP evidence.
|
|
13
|
+
- 59 source-backed examples on individual pages, with 188 selectable states. These represent 65 of 66 source page definitions; the original `/auth` reference index is superseded by the catalogue.
|
|
14
|
+
- 20 independent Ceramic parts available to composition discovery. Fields, record tables, checklists, ratings, measures, trends and media stages extend the existing account, activity, directory and delivery parts.
|
|
15
|
+
- Catalogue and individual `#/templates/patterns/:id` routes replace the stacked one-pager. Legacy example-query links remain usable.
|
|
16
|
+
- The invented review and workspace examples are retired from UI/MCP discovery.
|
|
17
|
+
|
|
18
|
+
"Source indexed" means readable reference code, not a semantic implementation or a production service.
|
|
19
|
+
|
|
20
|
+
## Remaining parity work
|
|
21
|
+
|
|
22
|
+
1. Replace remaining generic record/dashboard fixtures with source-specific columns, filters and decision states. A page mapping alone does not establish this parity.
|
|
23
|
+
2. Preserve the finer onboarding interactions: credential matrices, document previews, per-item checks, orientation completion and plan comparison. Current drafts and progress are local examples, not cross-page persistence.
|
|
24
|
+
3. Complete scheduling detail variations. Upcoming, pending and completed tabs now select distinct matching records; booking, feedback and assessment exercise key validations but not every original branch.
|
|
25
|
+
4. Expand profile defaults, settings variants and original public-page sections. Current forms and content pages are representative compositions.
|
|
26
|
+
5. Session-expiry modal remains unported; existing recovery is inline. Shared application/portal shells intentionally are not copied into the library.
|
|
27
|
+
6. Compare each source fragment with its representative port before marking it complete. Live authentication, storage, payments, media and provider integration remain host responsibilities, not required side effects of gallery demos.
|
|
28
|
+
|
|
29
|
+
Each port must preserve actual source interaction intent, expose useful pieces independently, replace brand-specific policies/data, map to registered Ceramic semantics, and prove the rendered consumer. Never fill a missing preview with a made-up screen or original screenshot labeled as themed.
|
|
30
|
+
|
|
31
|
+
## Per-page ledger
|
|
32
|
+
|
|
33
|
+
| Source page | Source-pattern ID | Gallery implementation |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `/auth` | `account-auth` | Reference index; replaced by the UX catalogue, not duplicated as a product flow |
|
|
36
|
+
| `/forgot-password` | `account-forgot-password` | `password-recovery-01` — representative port; 4 selectable states |
|
|
37
|
+
| `/invite` | `account-invite` | `invitation-01` — representative port; 4 selectable states |
|
|
38
|
+
| `/locked` | `account-locked` | `account-protection-01` — representative port; 4 selectable states |
|
|
39
|
+
| `/login` | `account-login` | `login-01` — representative port; 4 selectable states |
|
|
40
|
+
| `/mfa` | `account-mfa` | `two-factor-01` — representative port; 4 selectable states |
|
|
41
|
+
| `/onboarding/credentials` | `account-onboarding-credentials` | `credentials-01` — representative port; 5 selectable states |
|
|
42
|
+
| `/onboarding/legal` | `account-onboarding-legal` | `agreements-01` — representative port; 5 selectable states |
|
|
43
|
+
| `/onboarding/orientation` | `account-onboarding-orientation` | `orientation-01` — representative port; 3 selectable states; `plan-selection-01` — representative port; 3 selectable states |
|
|
44
|
+
| `/onboarding` | `account-onboarding` | `setup-progress-01` — representative port; 4 selectable states |
|
|
45
|
+
| `/onboarding/profile` | `account-onboarding-profile` | `profile-setup-01` — representative port; 5 selectable states |
|
|
46
|
+
| `/reset-password` | `account-reset-password` | `password-reset-01` — representative port; 4 selectable states |
|
|
47
|
+
| `/session-expired` | `account-session-expired` | `session-recovery-01` — representative port; 4 selectable states |
|
|
48
|
+
| `/signup` | `account-signup` | `signup-01` — representative port; 3 selectable states |
|
|
49
|
+
| `/verify-email` | `account-verify-email` | `verify-email-01` — representative port; 3 selectable states |
|
|
50
|
+
| `/welcome` | `account-welcome` | `welcome-01` — representative port; 2 selectable states |
|
|
51
|
+
| `/admin/analytics` | `admin-analytics` | `analytics-01` — representative port; 2 selectable states |
|
|
52
|
+
| `/admin/audit` | `admin-audit` | `audit-log-01` — representative port; 3 selectable states |
|
|
53
|
+
| `/admin/broadcasts/new` | `admin-broadcasts-new` | `broadcast-schedule-01` — representative port; 3 selectable states |
|
|
54
|
+
| `/admin/broadcasts` | `admin-broadcasts` | `broadcast-management-01` — representative port; 3 selectable states |
|
|
55
|
+
| `/admin/closures` | `admin-closures` | `closure-queue-01` — representative port; 3 selectable states |
|
|
56
|
+
| `/admin/devices` | `admin-devices` | `device-monitoring-01` — representative port; 3 selectable states |
|
|
57
|
+
| `/admin/escalations` | `admin-escalations` | `matching-escalations-01` — representative port; 3 selectable states |
|
|
58
|
+
| `/admin/incidents` | `admin-incidents` | `incident-tracking-01` — representative port; 3 selectable states |
|
|
59
|
+
| `/admin/onboarding/[id]` | `admin-onboarding-detail` | `verification-checklist-01` — representative port; 3 selectable states |
|
|
60
|
+
| `/admin/onboarding` | `admin-onboarding` | `verification-queue-01` — representative port; 3 selectable states |
|
|
61
|
+
| `/admin/payments` | `admin-payments` | `payment-cycle-01` — representative port; 3 selectable states |
|
|
62
|
+
| `/admin/providers` | `admin-providers` | `provider-connections-01` — representative port; 3 selectable states |
|
|
63
|
+
| `/admin/settings` | `admin-settings` | `account-settings-01` — representative port; 6 selectable states |
|
|
64
|
+
| `/admin/subscriptions/[id]/allocate` | `admin-subscriptions-detail-allocate` | `credit-allocation-01` — representative port; 3 selectable states |
|
|
65
|
+
| `/admin/subscriptions/[id]` | `admin-subscriptions-detail` | `subscription-detail-01` — representative port; 3 selectable states |
|
|
66
|
+
| `/admin/subscriptions/new` | `admin-subscriptions-new` | `subscription-create-01` — representative port; 3 selectable states |
|
|
67
|
+
| `/admin/subscriptions` | `admin-subscriptions` | `subscriptions-01` — representative port; 3 selectable states |
|
|
68
|
+
| `/admin/transactions` | `admin-transactions` | `credit-transactions-01` — representative port; 3 selectable states |
|
|
69
|
+
| `/marketing/blog` | `marketing-blog` | `article-library-01` — representative port; 2 selectable states |
|
|
70
|
+
| `/marketing/contact` | `marketing-contact` | `contact-form-01` — representative port; 3 selectable states |
|
|
71
|
+
| `/marketing` | `marketing` | `product-introduction-01` — representative port; 1 selectable states |
|
|
72
|
+
| `/portal/mentor/messages` | `portal-mentor-messages` | `conversation-list-01` — representative port; 2 selectable states |
|
|
73
|
+
| `/portal/mentor` | `portal-mentor` | `expert-overview-01` — representative port; 2 selectable states |
|
|
74
|
+
| `/portal/mentor/profile` | `portal-mentor-profile` | `profile-editing-01` — representative port; 4 selectable states |
|
|
75
|
+
| `/portal/mentor/requests/[id]` | `portal-mentor-requests-detail` | `request-decision-01` — representative port; 3 selectable states |
|
|
76
|
+
| `/portal/mentor/sessions/[id]/assess` | `portal-mentor-sessions-detail-assess` | `structured-assessment-01` — representative port; 2 selectable states |
|
|
77
|
+
| `/portal/mentor/settings` | `portal-mentor-settings` | `account-settings-01` — representative port; 6 selectable states |
|
|
78
|
+
| `/portal/sessions/[id]/live` | `portal-sessions-detail-live` | `live-session-01` — representative port; 3 selectable states |
|
|
79
|
+
| `/portal/sponsor/billing` | `portal-sponsor-billing` | `invoices-01` — representative port; 3 selectable states |
|
|
80
|
+
| `/portal/sponsor` | `portal-sponsor` | `subscription-detail-01` — representative port; 3 selectable states |
|
|
81
|
+
| `/portal/sponsor/profile` | `portal-sponsor-profile` | `profile-editing-01` — representative port; 4 selectable states |
|
|
82
|
+
| `/portal/sponsor/sessions` | `portal-sponsor-sessions` | `session-list-01` — representative port; 3 selectable states |
|
|
83
|
+
| `/portal/sponsor/settings` | `portal-sponsor-settings` | `account-settings-01` — representative port; 6 selectable states |
|
|
84
|
+
| `/portal/sponsor/trainees` | `portal-sponsor-trainees` | `team-allocations-01` — representative port; 3 selectable states |
|
|
85
|
+
| `/portal/trainee/competency` | `portal-trainee-competency` | `skill-progression-01` — representative port; 2 selectable states |
|
|
86
|
+
| `/portal/trainee/credits` | `portal-trainee-credits` | `credit-balance-01` — representative port; 3 selectable states |
|
|
87
|
+
| `/portal/trainee/mentors` | `portal-trainee-mentors` | `people-directory-01` — representative port; 3 selectable states |
|
|
88
|
+
| `/portal/trainee/messages` | `portal-trainee-messages` | `conversation-list-01` — representative port; 2 selectable states |
|
|
89
|
+
| `/portal/trainee/notifications` | `portal-trainee-notifications` | `notifications-01` — representative port; 4 selectable states |
|
|
90
|
+
| `/portal/trainee` | `portal-trainee` | `personal-overview-01` — representative port; 3 selectable states |
|
|
91
|
+
| `/portal/trainee/profile` | `portal-trainee-profile` | `profile-editing-01` — representative port; 4 selectable states |
|
|
92
|
+
| `/portal/trainee/request` | `portal-trainee-request` | `booking-request-01` — representative port; 5 selectable states |
|
|
93
|
+
| `/portal/trainee/sessions/[id]/feedback` | `portal-trainee-sessions-detail-feedback` | `session-feedback-01` — representative port; 2 selectable states |
|
|
94
|
+
| `/portal/trainee/sessions/[id]` | `portal-trainee-sessions-detail` | `session-detail-01` — representative port; 3 selectable states |
|
|
95
|
+
| `/portal/trainee/sessions/[id]/precheck` | `portal-trainee-sessions-detail-precheck` | `connectivity-check-01` — representative port; 3 selectable states |
|
|
96
|
+
| `/portal/trainee/sessions` | `portal-trainee-sessions` | `session-list-01` — representative port; 3 selectable states |
|
|
97
|
+
| `/portal/trainee/settings/notifications` | `portal-trainee-settings-notifications` | `notification-preferences-01` — representative port; 2 selectable states |
|
|
98
|
+
| `/portal/trainee/settings` | `portal-trainee-settings` | `account-settings-01` — representative port; 6 selectable states |
|
|
99
|
+
| `/portal/tv/[id]` | `portal-tv-detail` | `broadcast-viewer-01` — representative port; 3 selectable states |
|
|
100
|
+
| `/portal/tv` | `portal-tv` | `media-library-01` — representative port; 3 selectable states |
|
|
101
|
+
|
|
102
|
+
## Evidence and boundaries
|
|
103
|
+
|
|
104
|
+
- Exact source functions and hashes: `docs/design-system/ux-source-evidence/` (internal, never customer attribution).
|
|
105
|
+
- Source registry: `packages/design-system/src/manifests/ux-source-inventory.json`.
|
|
106
|
+
- CLI source read-only tools: list_ux_source_patterns, find_ux_source_patterns, get_ux_source_part.
|
|
107
|
+
- Public gallery only renders entries registered in ux-examples.json.
|
|
108
|
+
- No accounts, messages, uploads, payments, media permission, security changes, commits, deployment or npm publishing occurred.
|
|
109
|
+
- Source token mappings are adaptation guidance requiring slot-level decisions, not a global string replacement or proof of theme compliance.
|
|
110
|
+
- Browser QA: all 59 routes and 188 exposed states across the existing 8-theme matrix; desktop/mobile Chromium, 959px and Arabic/360px. Validation, file-type rejection, allocation limits, verification prerequisites, assessment gating, record lifecycle and local media/chat behavior have targeted tests. This is not a pixel/interaction parity check of every upstream fragment.
|
|
111
|
+
- Rams quick_review blocked by account review quota; no score exists.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# UX-only production merge
|
|
2
|
+
|
|
3
|
+
Scope: 59 representative UX examples, 188 selectable states, 20 independent
|
|
4
|
+
semantic parts, catalogue/detail routing, source inventory, CLI/MCP discovery
|
|
5
|
+
and adaptation logic. Based on `origin/main` e669f43; no local room/auth/billing,
|
|
6
|
+
Convex, environment, domain, dependency or unrelated brand changes are included.
|
|
7
|
+
|
|
8
|
+
PASS WITH RISKS: clean npm installation, design-system build, app production
|
|
9
|
+
build, bundle budgets, CLI/MCP/standalone tests, semantic page ownership, and
|
|
10
|
+
38 desktop/mobile Chromium tests passed in the isolated release worktree.
|
|
11
|
+
Detailed interaction parity remains incomplete as recorded in the migration ledger.
|
|
12
|
+
No npm release is part of this merge; existing npm versions are unchanged.
|
|
13
|
+
|
|
14
|
+
Bounded input geometry is applied only inside UX examples. The shared token
|
|
15
|
+
definitions do not restyle existing room consumers. Current membership guards
|
|
16
|
+
are preserved for catalogue and nested detail routes.
|
|
17
|
+
|
|
18
|
+
Rams quick_review and pre-commit review_files were requested but the workspace
|
|
19
|
+
review quota is exhausted, so no score was obtained. Its one partial warning
|
|
20
|
+
about the pre-existing presentation thumbnail does not reproduce from the
|
|
21
|
+
stylesheet: its parent reserves 16/10 aspect ratio and the image fills that area.
|
|
22
|
+
No baseline or guard was weakened. Browser-engine parity and live authenticated
|
|
23
|
+
account-specific chrome remain untested. npm reported existing dependency audit
|
|
24
|
+
findings (24 total, including 3 high); dependency files are unchanged.
|
|
25
|
+
|
|
26
|
+
Room validation is a separate local task; production room code is unchanged.
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|