Harden HyperTwist route loading surfaces
This commit is contained in:
parent
ae4e517e5d
commit
18f4a32990
10 changed files with 208 additions and 14 deletions
|
|
@ -156,6 +156,8 @@ describe('AppRouteTree', () => {
|
|||
renderRoute('/')
|
||||
|
||||
expect(screen.getByText('Loading HyperTwist...')).toBeTruthy()
|
||||
expect(screen.getByText('Public release shell')).toBeTruthy()
|
||||
expect(screen.getByText(/public product story, current launch posture, and browser-versus-desktop authority map/i)).toBeTruthy()
|
||||
expect(await screen.findByText('HyperTwist turns cube practice into a real operator-grade training stack.')).toBeTruthy()
|
||||
})
|
||||
|
||||
|
|
@ -180,6 +182,8 @@ describe('AppRouteTree', () => {
|
|||
it('renders the login page through the real public route tree', async () => {
|
||||
renderRoute('/login?next=%2Fapp%2Fdownloads')
|
||||
|
||||
expect(screen.getByText('Browser account access')).toBeTruthy()
|
||||
expect(screen.getByText(/safe next-step routing, and protected release continuity/i)).toBeTruthy()
|
||||
expect(await screen.findByText('Log in to HyperTwist')).toBeTruthy()
|
||||
expect(screen.getByRole('link', { name: /create an account/i }).getAttribute('href')).toBe('/register?next=%2Fapp%2Fdownloads')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -35,6 +35,14 @@ describe('public route registry', () => {
|
|||
expect(crawlablePublicRoutes.map((route) => route.path)).not.toContain('/register')
|
||||
})
|
||||
|
||||
it('carries non-empty loader metadata for every shared public-shell route', () => {
|
||||
for (const route of publicRouteRegistry) {
|
||||
expect(route.loaderTitle.trim().length).toBeGreaterThan(0)
|
||||
expect((route.loaderDescription || '').trim().length).toBeGreaterThan(0)
|
||||
expect((route.loaderEyebrow || '').trim().length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('renders sitemap XML from the shared crawlable route registry', () => {
|
||||
const xml = buildPublicSitemapXml()
|
||||
|
||||
|
|
|
|||
|
|
@ -156,10 +156,13 @@ describe('website route and shell behavior', () => {
|
|||
isLoading: true,
|
||||
})
|
||||
|
||||
renderProtectedRoute('/app/account')
|
||||
const { container } = renderProtectedRoute('/app/account')
|
||||
|
||||
expect(screen.getByText('Loading operator access...')).toBeTruthy()
|
||||
expect(screen.getByText('Browser account access')).toBeTruthy()
|
||||
expect(screen.getByText(/Checking your saved browser session, protected release posture, and browser-to-desktop continuity/i)).toBeTruthy()
|
||||
expect(container.querySelector('.general-page-loader-shell--fullscreen')).not.toBeNull()
|
||||
expect(screen.getByAltText('HyperTwist')).toBeTruthy()
|
||||
expect(screen.getByRole('link', { name: 'Open sign-in' }).getAttribute('href')).toBe('/login?next=%2Fapp%2Faccount')
|
||||
expect(screen.getByRole('link', { name: 'Open support' }).getAttribute('href')).toBe('/support?topic=operator-access')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ export function ProtectedRoute() {
|
|||
return (
|
||||
<GeneralPageLoader
|
||||
title="Loading operator access..."
|
||||
eyebrow="Browser account access"
|
||||
description="Checking your saved browser session, protected release posture, and browser-to-desktop continuity before the operator shell opens."
|
||||
fullscreen
|
||||
showBrand
|
||||
>
|
||||
<div className="button-row general-page-loader-actions">
|
||||
<Link className="button button--ghost" to={loginRedirectTarget}>
|
||||
|
|
|
|||
|
|
@ -5,10 +5,16 @@ export function GeneralPageLoader({
|
|||
title = 'Loading',
|
||||
description,
|
||||
children,
|
||||
eyebrow,
|
||||
fullscreen = false,
|
||||
showBrand = false,
|
||||
}: {
|
||||
title?: string
|
||||
description?: string
|
||||
children?: ReactNode
|
||||
eyebrow?: string
|
||||
fullscreen?: boolean
|
||||
showBrand?: boolean
|
||||
}) {
|
||||
const [isCompleting, setIsCompleting] = useState(false)
|
||||
|
||||
|
|
@ -29,8 +35,23 @@ export function GeneralPageLoader({
|
|||
const spinnerColor = isCompleting ? 'var(--ht-amber)' : 'var(--ht-cyan)'
|
||||
|
||||
return (
|
||||
<div className="general-page-loader-shell" role="status" aria-live="polite" aria-label={title}>
|
||||
<div
|
||||
className={`general-page-loader-shell${fullscreen ? ' general-page-loader-shell--fullscreen' : ''}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label={title}
|
||||
>
|
||||
<div className="general-page-loader-frame">
|
||||
{showBrand ? (
|
||||
<div className="general-page-loader-brand">
|
||||
<img
|
||||
src="/branding/hypertwist-3d-symbol.png"
|
||||
alt="HyperTwist"
|
||||
className="general-page-loader-brand__image"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{eyebrow ? <p className="general-page-loader-eyebrow">{eyebrow}</p> : null}
|
||||
<NamedAgentSpinner
|
||||
name="cascade"
|
||||
size={32}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
"path": "/",
|
||||
"label": "Home",
|
||||
"loaderTitle": "Loading HyperTwist...",
|
||||
"loaderEyebrow": "Public release shell",
|
||||
"loaderDescription": "Loading the public product story, current launch posture, and browser-versus-desktop authority map.",
|
||||
"nav": true,
|
||||
"footer": false,
|
||||
"crawlable": true
|
||||
|
|
@ -11,6 +13,8 @@
|
|||
"path": "/features",
|
||||
"label": "Features",
|
||||
"loaderTitle": "Feature atlas",
|
||||
"loaderEyebrow": "Capability atlas",
|
||||
"loaderDescription": "Loading the shipped feature map, higher-dimensional runtime posture, and explicit desktop-first boundaries.",
|
||||
"nav": true,
|
||||
"footer": true,
|
||||
"crawlable": true
|
||||
|
|
@ -19,6 +23,8 @@
|
|||
"path": "/about",
|
||||
"label": "About",
|
||||
"loaderTitle": "About HyperTwist",
|
||||
"loaderEyebrow": "Product narrative",
|
||||
"loaderDescription": "Loading the mission, higher-dimensional seriousness, and current control-boundary truth behind the product.",
|
||||
"nav": true,
|
||||
"footer": true,
|
||||
"crawlable": true
|
||||
|
|
@ -27,6 +33,8 @@
|
|||
"path": "/resources",
|
||||
"label": "Resources",
|
||||
"loaderTitle": "Resources",
|
||||
"loaderEyebrow": "Public reference portal",
|
||||
"loaderDescription": "Loading rollout-safe resources, simulator guidance, and current higher-dimensional operator references.",
|
||||
"nav": true,
|
||||
"footer": true,
|
||||
"crawlable": true
|
||||
|
|
@ -35,6 +43,8 @@
|
|||
"path": "/docs",
|
||||
"label": "Docs",
|
||||
"loaderTitle": "Documentation",
|
||||
"loaderEyebrow": "Public manual",
|
||||
"loaderDescription": "Loading the feature-registry-backed manual, rollout doctrine, and browser-versus-desktop usage guide.",
|
||||
"nav": false,
|
||||
"footer": false,
|
||||
"crawlable": true
|
||||
|
|
@ -43,6 +53,8 @@
|
|||
"path": "/support",
|
||||
"label": "Support",
|
||||
"loaderTitle": "Support",
|
||||
"loaderEyebrow": "Operator help lane",
|
||||
"loaderDescription": "Loading rollout guidance, account and entitlement help, and browser-to-desktop escalation routes.",
|
||||
"nav": false,
|
||||
"footer": false,
|
||||
"crawlable": true
|
||||
|
|
@ -51,6 +63,8 @@
|
|||
"path": "/changelog",
|
||||
"label": "Release notes",
|
||||
"loaderTitle": "Release notes",
|
||||
"loaderEyebrow": "Public release history",
|
||||
"loaderDescription": "Loading recent browser, package, control-boundary, and rollout changes from the active shipping lane.",
|
||||
"nav": false,
|
||||
"footer": false,
|
||||
"crawlable": true
|
||||
|
|
@ -59,6 +73,8 @@
|
|||
"path": "/pricing",
|
||||
"label": "Pricing",
|
||||
"loaderTitle": "Pricing",
|
||||
"loaderEyebrow": "Paddle-ready plans",
|
||||
"loaderDescription": "Loading plan posture, public launch readiness, and the current desktop-first distribution model.",
|
||||
"nav": true,
|
||||
"footer": true,
|
||||
"crawlable": true
|
||||
|
|
@ -67,6 +83,8 @@
|
|||
"path": "/download",
|
||||
"label": "Download",
|
||||
"loaderTitle": "Download center",
|
||||
"loaderEyebrow": "Desktop distribution",
|
||||
"loaderDescription": "Loading the current release manifest posture, package proof, and browser-to-desktop pairing guidance.",
|
||||
"nav": true,
|
||||
"footer": true,
|
||||
"crawlable": true
|
||||
|
|
@ -75,6 +93,8 @@
|
|||
"path": "/open-source-notices",
|
||||
"label": "Open Source Notices",
|
||||
"loaderTitle": "Open source notices",
|
||||
"loaderEyebrow": "Distribution references",
|
||||
"loaderDescription": "Loading notices, corresponding-source posture, and public legal follow-through for downloadable builds.",
|
||||
"nav": false,
|
||||
"footer": true,
|
||||
"crawlable": true
|
||||
|
|
@ -83,6 +103,8 @@
|
|||
"path": "/privacy",
|
||||
"label": "Privacy",
|
||||
"loaderTitle": "Privacy",
|
||||
"loaderEyebrow": "Public policy surface",
|
||||
"loaderDescription": "Loading the browser-account privacy posture, desktop-link boundary, and distribution-safe data handling notes.",
|
||||
"nav": false,
|
||||
"footer": true,
|
||||
"crawlable": true
|
||||
|
|
@ -91,6 +113,8 @@
|
|||
"path": "/terms",
|
||||
"label": "Terms",
|
||||
"loaderTitle": "Terms",
|
||||
"loaderEyebrow": "Public policy surface",
|
||||
"loaderDescription": "Loading access terms, protected download boundaries, and the current desktop-first simulator posture.",
|
||||
"nav": false,
|
||||
"footer": true,
|
||||
"crawlable": true
|
||||
|
|
@ -99,6 +123,8 @@
|
|||
"path": "/shipping-payment",
|
||||
"label": "Shipping & Payment",
|
||||
"loaderTitle": "Shipping & payment",
|
||||
"loaderEyebrow": "Distribution policy",
|
||||
"loaderDescription": "Loading the digital-delivery model, Paddle-ready billing posture, and protected release follow-through.",
|
||||
"nav": false,
|
||||
"footer": true,
|
||||
"crawlable": true
|
||||
|
|
@ -107,6 +133,8 @@
|
|||
"path": "/login",
|
||||
"label": "Log in",
|
||||
"loaderTitle": "Log in",
|
||||
"loaderEyebrow": "Browser account access",
|
||||
"loaderDescription": "Preparing sign-in, safe next-step routing, and protected release continuity before the operator shell opens.",
|
||||
"nav": false,
|
||||
"footer": false,
|
||||
"crawlable": false
|
||||
|
|
@ -115,6 +143,8 @@
|
|||
"path": "/register",
|
||||
"label": "Create account",
|
||||
"loaderTitle": "Create account",
|
||||
"loaderEyebrow": "Browser account access",
|
||||
"loaderDescription": "Preparing account creation, protected release follow-through, and browser-to-desktop continuity for first launch.",
|
||||
"nav": false,
|
||||
"footer": false,
|
||||
"crawlable": false
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ export type PublicRouteDefinition = {
|
|||
path: string
|
||||
label: string
|
||||
loaderTitle: string
|
||||
loaderEyebrow?: string
|
||||
loaderDescription?: string
|
||||
nav: boolean
|
||||
footer: boolean
|
||||
crawlable: boolean
|
||||
|
|
|
|||
|
|
@ -11,8 +11,60 @@ const BrowserAccessPage = lazy(() => import('../pages/app-pages').then((m) => ({
|
|||
const AccountPage = lazy(() => import('../pages/app-pages').then((m) => ({ default: m.AccountPage })))
|
||||
const NoticesPage = lazy(() => import('../pages/app-pages').then((m) => ({ default: m.NoticesPage })))
|
||||
|
||||
function withAppLoader(title: string, element: React.ReactNode) {
|
||||
return <Suspense fallback={<GeneralPageLoader title={title} />}>{element}</Suspense>
|
||||
const protectedAppLoaders = {
|
||||
dashboard: {
|
||||
title: 'Loading dashboard...',
|
||||
eyebrow: 'Protected operator shell',
|
||||
description:
|
||||
'Restoring account posture, release authority, package proof, and browser-to-desktop pairing surfaces inside the signed-in operator lane.',
|
||||
},
|
||||
downloads: {
|
||||
title: 'Loading downloads...',
|
||||
eyebrow: 'Protected release lane',
|
||||
description:
|
||||
'Resolving entitled desktop targets, current package proof, and rollout references before the signed-in download surface opens.',
|
||||
},
|
||||
browserAccess: {
|
||||
title: 'Loading browser access...',
|
||||
eyebrow: 'Protected browser shell',
|
||||
description:
|
||||
'Restoring the signed-in browser posture, release guidance, and explicit browser-versus-desktop authority boundaries.',
|
||||
},
|
||||
account: {
|
||||
title: 'Loading account...',
|
||||
eyebrow: 'Protected account lane',
|
||||
description:
|
||||
'Refreshing the signed-in account view, current plan posture, and release-access continuity before desktop follow-through resumes.',
|
||||
},
|
||||
notices: {
|
||||
title: 'Loading notices...',
|
||||
eyebrow: 'Protected distribution references',
|
||||
description:
|
||||
'Restoring signed-in notices, corresponding-source posture, and download/distribution references inside the protected release shell.',
|
||||
},
|
||||
} as const
|
||||
|
||||
function withAppLoader(
|
||||
loader: {
|
||||
title: string
|
||||
eyebrow: string
|
||||
description: string
|
||||
},
|
||||
element: React.ReactNode,
|
||||
) {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<GeneralPageLoader
|
||||
title={loader.title}
|
||||
eyebrow={loader.eyebrow}
|
||||
description={loader.description}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{element}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export function AppRouteTree() {
|
||||
|
|
@ -22,11 +74,11 @@ export function AppRouteTree() {
|
|||
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/app" element={<AppShell />}>
|
||||
<Route index element={withAppLoader('Loading dashboard...', <DashboardOverviewPage />)} />
|
||||
<Route path="downloads" element={withAppLoader('Loading downloads...', <DownloadCenterPage />)} />
|
||||
<Route path="browser-access" element={withAppLoader('Loading browser access...', <BrowserAccessPage />)} />
|
||||
<Route path="account" element={withAppLoader('Loading account...', <AccountPage />)} />
|
||||
<Route path="notices" element={withAppLoader('Loading notices...', <NoticesPage />)} />
|
||||
<Route index element={withAppLoader(protectedAppLoaders.dashboard, <DashboardOverviewPage />)} />
|
||||
<Route path="downloads" element={withAppLoader(protectedAppLoaders.downloads, <DownloadCenterPage />)} />
|
||||
<Route path="browser-access" element={withAppLoader(protectedAppLoaders.browserAccess, <BrowserAccessPage />)} />
|
||||
<Route path="account" element={withAppLoader(protectedAppLoaders.account, <AccountPage />)} />
|
||||
<Route path="notices" element={withAppLoader(protectedAppLoaders.notices, <NoticesPage />)} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Suspense, lazy, type ReactNode } from 'react'
|
||||
import { Route } from 'react-router-dom'
|
||||
import { GeneralPageLoader } from '../components/ui/Skeletons'
|
||||
import { publicRouteRegistry } from '../public-route-registry'
|
||||
import { publicRouteRegistry, type PublicRouteDefinition } from '../public-route-registry'
|
||||
|
||||
const HomeLanding = lazy(() => import('../pages/public-pages-marketing').then((m) => ({ default: m.HomeLanding })))
|
||||
const FeaturesPage = lazy(() => import('../pages/public-pages-features').then((m) => ({ default: m.FeaturesPage })))
|
||||
|
|
@ -37,8 +37,24 @@ const publicRouteElements: Record<string, ReactNode> = {
|
|||
'/register': <RegisterPage />,
|
||||
}
|
||||
|
||||
function withGeneralLoader(title: string, element: ReactNode) {
|
||||
return <Suspense fallback={<GeneralPageLoader title={title} />}>{element}</Suspense>
|
||||
function withGeneralLoader(route: PublicRouteDefinition, element: ReactNode) {
|
||||
const isAuthRoute = route.path === '/login' || route.path === '/register'
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<GeneralPageLoader
|
||||
title={route.loaderTitle}
|
||||
eyebrow={route.loaderEyebrow}
|
||||
description={route.loaderDescription}
|
||||
fullscreen={isAuthRoute}
|
||||
showBrand
|
||||
/>
|
||||
}
|
||||
>
|
||||
{element}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderPublicRoutes() {
|
||||
|
|
@ -48,7 +64,7 @@ export function renderPublicRoutes() {
|
|||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={withGeneralLoader(route.loaderTitle, publicRouteElements[route.path])}
|
||||
element={withGeneralLoader(route, publicRouteElements[route.path])}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -664,7 +664,14 @@ code {
|
|||
place-items: center;
|
||||
overflow: hidden;
|
||||
padding: 1.5rem;
|
||||
background: transparent;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(84, 203, 255, 0.12), transparent 40%),
|
||||
radial-gradient(circle at bottom, rgba(247, 178, 103, 0.1), transparent 48%),
|
||||
linear-gradient(180deg, rgba(6, 10, 20, 0.9), rgba(7, 11, 19, 0.96));
|
||||
}
|
||||
|
||||
.general-page-loader-shell--fullscreen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.general-page-loader-frame {
|
||||
|
|
@ -684,7 +691,51 @@ code {
|
|||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.general-page-loader-frame::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 58%);
|
||||
}
|
||||
|
||||
.general-page-loader-brand {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
border-radius: 1.4rem;
|
||||
border: 1px solid rgba(84, 203, 255, 0.24);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(84, 203, 255, 0.12), transparent 58%),
|
||||
rgba(8, 13, 26, 0.9);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 0 22px rgba(84, 203, 255, 0.16);
|
||||
}
|
||||
|
||||
.general-page-loader-brand__image {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.general-page-loader-eyebrow {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
font-size: 0.73rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.general-page-loader-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
font-size: 0.81rem;
|
||||
font-weight: 600;
|
||||
|
|
@ -694,6 +745,8 @@ code {
|
|||
}
|
||||
|
||||
.general-page-loader-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
max-width: 32rem;
|
||||
text-align: center;
|
||||
|
|
@ -702,6 +755,8 @@ code {
|
|||
}
|
||||
|
||||
.general-page-loader-extra {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue