Harden public product reality surfaces
This commit is contained in:
parent
02399cf736
commit
2dbdf20e52
9 changed files with 791 additions and 440 deletions
|
|
@ -398,7 +398,22 @@ When a new packet materially adds or changes a normalized feature:
|
|||
|
||||
## Summary
|
||||
|
||||
### Latest continuity note (`2026-06-30`)
|
||||
### Latest continuity note (`2026-07-02`)
|
||||
|
||||
- the first-party `website/` public/auth shell now carries one shared
|
||||
product-reality strip across public marketing routes and browser account
|
||||
access routes:
|
||||
- desktop Unreal remains the simulator authority
|
||||
- the website remains the account, pricing, documentation, release, notices,
|
||||
support, and protected handoff surface
|
||||
- keyboard and mouse remain the viable current development and operator
|
||||
review path
|
||||
- live commerce/download/auth configuration plus physical headset/controller
|
||||
observation remain explicit launch gates
|
||||
- this is a copy and layout coherence hardening pass, not a topology widening
|
||||
and not a promotion of the optional full-browser simulator branch
|
||||
|
||||
### Previous continuity note (`2026-06-30`)
|
||||
|
||||
- the first-party `website/` public manual lane widened again without changing
|
||||
topology:
|
||||
|
|
|
|||
|
|
@ -184,6 +184,25 @@ controller settings, layout presets, and rebinding truth for the exact shipped
|
|||
XR input ids. But it is still not the same thing as packaged
|
||||
headset/controller proof or generic marketed VR completion.
|
||||
|
||||
The current launch-UX truth on `2026-07-02` should be stated the same way on
|
||||
public pages, manuals, and operator closeouts:
|
||||
|
||||
- keyboard and mouse development is viable now because the desktop runtime owns
|
||||
shipped classic keyboard, pointer, orbit, zoom, and higher-dimensional
|
||||
interaction surfaces
|
||||
- bounded XR runtime, controller-settings, layout, and rebinding ownership are
|
||||
real first-party seams, but live headset-connected and controller-input
|
||||
observation remain the proof gates before marketing finished VR completion
|
||||
- the raw first-run desktop experience is still dashboard/operator-surface
|
||||
centered rather than a fully polished consumer start menu, so any rollout
|
||||
copy must keep first-launch verification, pairing, and settings truth visible
|
||||
- Paddle checkout URLs, public Windows release URLs, production auth/runtime
|
||||
configuration, and corresponding-source/notices URLs remain deployment
|
||||
configuration gates, not hardcoded product facts
|
||||
- the website remains useful because it owns public docs, account, pricing,
|
||||
notices, release posture, support, and protected download handoff; it should
|
||||
not be described as simulator parity with the downloadable
|
||||
|
||||
Canonical audit note:
|
||||
|
||||
- `C:\HyperTwist\docs\ops\HYPERTWIST_UNREAL_INPUT_AND_XR_COMPLETENESS_AUDIT_2026-06-22.md`
|
||||
|
|
|
|||
|
|
@ -413,7 +413,15 @@ Current consolidated milestone snapshot:
|
|||
later same-family continuity follow-up widens that same decision guide
|
||||
across the public open-source notices, privacy, terms, and
|
||||
shipping/payment routes as well so the remaining legal/distribution pages do
|
||||
not fall back to older implicit next-step wording
|
||||
not fall back to older implicit next-step wording,
|
||||
and the current `2026-07-02` public/auth shell polish pass now adds one
|
||||
shared product-reality strip across the public marketing shell plus the
|
||||
browser account routes so every public-facing route repeats the same
|
||||
launch-safe truth: the downloadable desktop runtime is the simulator
|
||||
authority, the website owns account/pricing/docs/release/notice handoff,
|
||||
keyboard and mouse are the viable current development path, and live
|
||||
checkout/download/auth plus physical headset/controller observation remain
|
||||
explicit gates rather than hidden assumptions
|
||||
- classic-cube `Phase 9A` replay recording is now closed through first-party
|
||||
runtime capture, `.json` replay persistence, local playback reconstruction,
|
||||
schema-light replay normalization across save/load/viewer import, and live
|
||||
|
|
|
|||
|
|
@ -43,6 +43,22 @@ interface BillingStateStoreOptions {
|
|||
now?: () => Date
|
||||
}
|
||||
|
||||
interface BillingEventIdentityCandidates {
|
||||
subscriptionIds: string[]
|
||||
transactionIds: string[]
|
||||
customerIds: string[]
|
||||
}
|
||||
|
||||
interface BillingStateStoreRuntime {
|
||||
defaultPlan: BillingPlan
|
||||
defaultRole: BillingRole
|
||||
productPlanMap: Record<string, BillingPlan>
|
||||
pricePlanMap: Record<string, BillingPlan>
|
||||
now: () => Date
|
||||
loadState: () => BillingStateFile
|
||||
saveState: (state: BillingStateFile) => void
|
||||
}
|
||||
|
||||
export interface BillingEventApplyResult {
|
||||
kind: 'applied' | 'duplicate' | 'ignored' | 'noop'
|
||||
reason:
|
||||
|
|
@ -311,6 +327,149 @@ function parsePaddleEvent(rawBody: string): PaddleEventEnvelope | null {
|
|||
}
|
||||
}
|
||||
|
||||
function readPrimaryEventId(envelope: PaddleEventEnvelope, prefix: 'subscription' | 'transaction'): string {
|
||||
const primaryId = readStringAtPath(envelope.data, ['id'])
|
||||
return primaryId && envelope.event_type.startsWith(`${prefix}.`) ? primaryId : ''
|
||||
}
|
||||
|
||||
function buildEventIdentityCandidates(envelope: PaddleEventEnvelope): BillingEventIdentityCandidates {
|
||||
return {
|
||||
subscriptionIds: [
|
||||
readPrimaryEventId(envelope, 'subscription'),
|
||||
...readAllCandidateIds(envelope.data, 'subscriptionId'),
|
||||
].filter((value): value is string => Boolean(value)),
|
||||
transactionIds: [
|
||||
readPrimaryEventId(envelope, 'transaction'),
|
||||
...readAllCandidateIds(envelope.data, 'transactionId'),
|
||||
].filter((value): value is string => Boolean(value)),
|
||||
customerIds: readAllCandidateIds(envelope.data, 'customerId'),
|
||||
}
|
||||
}
|
||||
|
||||
function findExistingAccount(
|
||||
state: BillingStateFile,
|
||||
email: string,
|
||||
ids: BillingEventIdentityCandidates,
|
||||
): BillingEntitlementState | null {
|
||||
if (email && state.accounts[email]) {
|
||||
return state.accounts[email]
|
||||
}
|
||||
|
||||
for (const account of Object.values(state.accounts)) {
|
||||
if (
|
||||
(account.subscriptionId && ids.subscriptionIds.includes(account.subscriptionId))
|
||||
|| (account.transactionId && ids.transactionIds.includes(account.transactionId))
|
||||
|| (account.customerId && ids.customerIds.includes(account.customerId))
|
||||
) {
|
||||
return account
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function markProcessedEvent(
|
||||
state: BillingStateFile,
|
||||
envelope: PaddleEventEnvelope,
|
||||
saveState: (state: BillingStateFile) => void,
|
||||
) {
|
||||
state.processedEvents[envelope.event_id] = envelope.occurred_at
|
||||
saveState(state)
|
||||
}
|
||||
|
||||
function buildNextEntitlementState({
|
||||
envelope,
|
||||
existingAccount,
|
||||
ids,
|
||||
resolvedEmail,
|
||||
runtime,
|
||||
}: {
|
||||
envelope: PaddleEventEnvelope
|
||||
existingAccount: BillingEntitlementState | null
|
||||
ids: BillingEventIdentityCandidates
|
||||
resolvedEmail: string
|
||||
runtime: Pick<BillingStateStoreRuntime, 'defaultPlan' | 'defaultRole' | 'productPlanMap' | 'pricePlanMap' | 'now'>
|
||||
}): BillingEntitlementState {
|
||||
const planFromItems = resolvePlanFromItemMaps(envelope.data, runtime.productPlanMap, runtime.pricePlanMap)
|
||||
const nextPlan = planFromItems
|
||||
?? readPlanFromCustomData(envelope.data, existingAccount?.plan ?? runtime.defaultPlan)
|
||||
const nextRole = readRoleFromCustomData(envelope.data, existingAccount?.role ?? runtime.defaultRole)
|
||||
const accessStatus = inferAccessStatus(envelope.event_type, envelope.data)
|
||||
|
||||
return {
|
||||
email: resolvedEmail,
|
||||
plan: nextPlan,
|
||||
role: nextRole,
|
||||
canDownload: deriveCanDownload(nextPlan, accessStatus),
|
||||
accessStatus,
|
||||
source: 'paddle',
|
||||
customerId: ids.customerIds[0] || existingAccount?.customerId || null,
|
||||
subscriptionId: ids.subscriptionIds[0] || existingAccount?.subscriptionId || null,
|
||||
transactionId: ids.transactionIds[0] || existingAccount?.transactionId || null,
|
||||
lastEventId: envelope.event_id,
|
||||
lastEventType: envelope.event_type,
|
||||
lastEventAt: envelope.occurred_at,
|
||||
updatedAt: toIsoString(runtime.now()),
|
||||
}
|
||||
}
|
||||
|
||||
function applyParsedPaddleEvent(
|
||||
envelope: PaddleEventEnvelope,
|
||||
runtime: BillingStateStoreRuntime,
|
||||
): BillingEventApplyResult {
|
||||
const state = runtime.loadState()
|
||||
if (state.processedEvents[envelope.event_id]) {
|
||||
return {
|
||||
kind: 'duplicate',
|
||||
reason: 'duplicate_event',
|
||||
eventId: envelope.event_id,
|
||||
eventType: envelope.event_type,
|
||||
}
|
||||
}
|
||||
|
||||
const email = readEmailFromEventData(envelope.data)
|
||||
const ids = buildEventIdentityCandidates(envelope)
|
||||
const existingAccount = findExistingAccount(state, email, ids)
|
||||
|
||||
if (existingAccount && !isLaterEvent(existingAccount.lastEventAt, envelope.occurred_at)) {
|
||||
markProcessedEvent(state, envelope, runtime.saveState)
|
||||
return {
|
||||
kind: 'ignored',
|
||||
reason: 'stale_event',
|
||||
eventId: envelope.event_id,
|
||||
eventType: envelope.event_type,
|
||||
email: existingAccount.email,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedEmail = email || existingAccount?.email || ''
|
||||
if (!resolvedEmail) {
|
||||
markProcessedEvent(state, envelope, runtime.saveState)
|
||||
return {
|
||||
kind: 'noop',
|
||||
reason: 'unresolved_account',
|
||||
eventId: envelope.event_id,
|
||||
eventType: envelope.event_type,
|
||||
}
|
||||
}
|
||||
|
||||
state.accounts[resolvedEmail] = buildNextEntitlementState({
|
||||
envelope,
|
||||
existingAccount,
|
||||
ids,
|
||||
resolvedEmail,
|
||||
runtime,
|
||||
})
|
||||
markProcessedEvent(state, envelope, runtime.saveState)
|
||||
return {
|
||||
kind: 'applied',
|
||||
reason: 'processed',
|
||||
eventId: envelope.event_id,
|
||||
eventType: envelope.event_type,
|
||||
email: resolvedEmail,
|
||||
}
|
||||
}
|
||||
|
||||
export function createBillingStateStore({
|
||||
statePath,
|
||||
defaultPlan,
|
||||
|
|
@ -332,32 +491,6 @@ export function createBillingStateStore({
|
|||
writeJsonFile(statePath, state)
|
||||
}
|
||||
|
||||
function findExistingAccount(
|
||||
state: BillingStateFile,
|
||||
email: string,
|
||||
ids: {
|
||||
subscriptionIds: string[]
|
||||
transactionIds: string[]
|
||||
customerIds: string[]
|
||||
},
|
||||
): BillingEntitlementState | null {
|
||||
if (email && state.accounts[email]) {
|
||||
return state.accounts[email]
|
||||
}
|
||||
|
||||
for (const account of Object.values(state.accounts)) {
|
||||
if (
|
||||
(account.subscriptionId && ids.subscriptionIds.includes(account.subscriptionId))
|
||||
|| (account.transactionId && ids.transactionIds.includes(account.transactionId))
|
||||
|| (account.customerId && ids.customerIds.includes(account.customerId))
|
||||
) {
|
||||
return account
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function getEntitlementByEmail(email: string): BillingEntitlementState | null {
|
||||
const normalizedEmail = normalizeEmail(email)
|
||||
if (!normalizedEmail) {
|
||||
|
|
@ -379,89 +512,15 @@ export function createBillingStateStore({
|
|||
}
|
||||
}
|
||||
|
||||
const state = loadState()
|
||||
if (state.processedEvents[envelope.event_id]) {
|
||||
return {
|
||||
kind: 'duplicate',
|
||||
reason: 'duplicate_event',
|
||||
eventId: envelope.event_id,
|
||||
eventType: envelope.event_type,
|
||||
}
|
||||
}
|
||||
|
||||
const email = readEmailFromEventData(envelope.data)
|
||||
const ids = {
|
||||
subscriptionIds: [
|
||||
readStringAtPath(envelope.data, ['id']) && envelope.event_type.startsWith('subscription.')
|
||||
? String(readStringAtPath(envelope.data, ['id']))
|
||||
: '',
|
||||
...readAllCandidateIds(envelope.data, 'subscriptionId'),
|
||||
].filter(Boolean),
|
||||
transactionIds: [
|
||||
readStringAtPath(envelope.data, ['id']) && envelope.event_type.startsWith('transaction.')
|
||||
? String(readStringAtPath(envelope.data, ['id']))
|
||||
: '',
|
||||
...readAllCandidateIds(envelope.data, 'transactionId'),
|
||||
].filter(Boolean),
|
||||
customerIds: readAllCandidateIds(envelope.data, 'customerId'),
|
||||
}
|
||||
|
||||
const existingAccount = findExistingAccount(state, email, ids)
|
||||
if (existingAccount && !isLaterEvent(existingAccount.lastEventAt, envelope.occurred_at)) {
|
||||
state.processedEvents[envelope.event_id] = envelope.occurred_at
|
||||
saveState(state)
|
||||
return {
|
||||
kind: 'ignored',
|
||||
reason: 'stale_event',
|
||||
eventId: envelope.event_id,
|
||||
eventType: envelope.event_type,
|
||||
email: existingAccount.email,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedEmail = email || existingAccount?.email || ''
|
||||
if (!resolvedEmail) {
|
||||
state.processedEvents[envelope.event_id] = envelope.occurred_at
|
||||
saveState(state)
|
||||
return {
|
||||
kind: 'noop',
|
||||
reason: 'unresolved_account',
|
||||
eventId: envelope.event_id,
|
||||
eventType: envelope.event_type,
|
||||
}
|
||||
}
|
||||
|
||||
const planFromItems = resolvePlanFromItemMaps(envelope.data, productPlanMap, pricePlanMap)
|
||||
const nextPlan = planFromItems
|
||||
?? readPlanFromCustomData(envelope.data, existingAccount?.plan ?? defaultPlan)
|
||||
const nextRole = readRoleFromCustomData(envelope.data, existingAccount?.role ?? defaultRole)
|
||||
const accessStatus = inferAccessStatus(envelope.event_type, envelope.data)
|
||||
const nextState: BillingEntitlementState = {
|
||||
email: resolvedEmail,
|
||||
plan: nextPlan,
|
||||
role: nextRole,
|
||||
canDownload: deriveCanDownload(nextPlan, accessStatus),
|
||||
accessStatus,
|
||||
source: 'paddle',
|
||||
customerId: ids.customerIds[0] || existingAccount?.customerId || null,
|
||||
subscriptionId: ids.subscriptionIds[0] || existingAccount?.subscriptionId || null,
|
||||
transactionId: ids.transactionIds[0] || existingAccount?.transactionId || null,
|
||||
lastEventId: envelope.event_id,
|
||||
lastEventType: envelope.event_type,
|
||||
lastEventAt: envelope.occurred_at,
|
||||
updatedAt: toIsoString(now()),
|
||||
}
|
||||
|
||||
state.accounts[resolvedEmail] = nextState
|
||||
state.processedEvents[envelope.event_id] = envelope.occurred_at
|
||||
saveState(state)
|
||||
return {
|
||||
kind: 'applied',
|
||||
reason: 'processed',
|
||||
eventId: envelope.event_id,
|
||||
eventType: envelope.event_type,
|
||||
email: resolvedEmail,
|
||||
}
|
||||
return applyParsedPaddleEvent(envelope, {
|
||||
defaultPlan,
|
||||
defaultRole,
|
||||
productPlanMap,
|
||||
pricePlanMap,
|
||||
now,
|
||||
loadState,
|
||||
saveState,
|
||||
})
|
||||
}
|
||||
|
||||
function getProcessedEventCount() {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,59 @@ import { MarketingLaunchStatusBanner } from '../ui/PublicLaunchStatus'
|
|||
import { brandConfig } from '../../site-config'
|
||||
import { footerLinks, marketingNavLinks } from '../../public-route-registry'
|
||||
|
||||
export const publicProductRealityStrip = (
|
||||
<section className="product-reality-strip" aria-label="Current HyperTwist product reality">
|
||||
<article className="product-reality-strip__item">
|
||||
<p className="eyebrow">Primary product</p>
|
||||
<strong>Desktop simulator</strong>
|
||||
<p>Native Unreal owns the real training runtime, package proof, and higher-dimensional execution.</p>
|
||||
</article>
|
||||
<article className="product-reality-strip__item">
|
||||
<p className="eyebrow">Web purpose</p>
|
||||
<strong>Access and rollout</strong>
|
||||
<p>The browser owns docs, account, pricing, notices, release posture, and desktop pairing.</p>
|
||||
</article>
|
||||
<article className="product-reality-strip__item">
|
||||
<p className="eyebrow">Ready today</p>
|
||||
<strong>Keyboard and mouse</strong>
|
||||
<p>Desktop control surfaces are usable for development and operator review without a headset.</p>
|
||||
</article>
|
||||
<article className="product-reality-strip__item">
|
||||
<p className="eyebrow">Still gated</p>
|
||||
<strong>Launch config and XR proof</strong>
|
||||
<p>Public download, Paddle, live auth, and headset/controller observation remain explicit gates.</p>
|
||||
</article>
|
||||
</section>
|
||||
)
|
||||
|
||||
export const compactProductRealityStrip = (
|
||||
<section
|
||||
className="product-reality-strip product-reality-strip--compact"
|
||||
aria-label="Current HyperTwist product reality"
|
||||
>
|
||||
<article className="product-reality-strip__item">
|
||||
<p className="eyebrow">Primary product</p>
|
||||
<strong>Desktop simulator</strong>
|
||||
<p>Native Unreal owns the real training runtime, package proof, and higher-dimensional execution.</p>
|
||||
</article>
|
||||
<article className="product-reality-strip__item">
|
||||
<p className="eyebrow">Web purpose</p>
|
||||
<strong>Access and rollout</strong>
|
||||
<p>The browser owns docs, account, pricing, notices, release posture, and desktop pairing.</p>
|
||||
</article>
|
||||
<article className="product-reality-strip__item">
|
||||
<p className="eyebrow">Ready today</p>
|
||||
<strong>Keyboard and mouse</strong>
|
||||
<p>Desktop control surfaces are usable for development and operator review without a headset.</p>
|
||||
</article>
|
||||
<article className="product-reality-strip__item">
|
||||
<p className="eyebrow">Still gated</p>
|
||||
<strong>Launch config and XR proof</strong>
|
||||
<p>Public download, Paddle, live auth, and headset/controller observation remain explicit gates.</p>
|
||||
</article>
|
||||
</section>
|
||||
)
|
||||
|
||||
export function MarketingShell({
|
||||
title,
|
||||
eyebrow,
|
||||
|
|
@ -74,6 +127,7 @@ export function MarketingShell({
|
|||
<p className="page-hero__lede">{lede}</p>
|
||||
</section>
|
||||
<MarketingLaunchStatusBanner />
|
||||
{publicProductRealityStrip}
|
||||
{children}
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
isOrcidOAuthEnabled,
|
||||
} from '../auth/supertokens-runtime'
|
||||
import { SiteMetadata } from '../components/seo/SiteMetadata'
|
||||
import { compactProductRealityStrip } from '../components/layout/MarketingShell'
|
||||
import { OperationalStatusCallout } from '../components/ui/OperationalStatusCallout'
|
||||
import { browserDesktopRealityCards, deliverySurfaceCards, desktopWorkflowTracks, operatorManualTracks } from '../site-data'
|
||||
import { buildSupportPath, getDownloadPlatformLabel, normalizeDownloadPlatform } from '../site-routes'
|
||||
|
|
@ -30,6 +31,7 @@ function AuthShell({
|
|||
<p className="eyebrow">Browser account access</p>
|
||||
<h1>{title}</h1>
|
||||
<p className="auth-card__subtitle">{subtitle}</p>
|
||||
{compactProductRealityStrip}
|
||||
{children}
|
||||
<div className="auth-card__footer">{footer}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { PublicLaunchStatus } from '../components/ui/PublicLaunchStatus'
|
|||
import { ReleaseValidationSummary } from '../components/ui/ReleaseValidationSummary'
|
||||
import { brandConfig, paddleReadyDescription } from '../site-config'
|
||||
import { buildReleaseMetadataItems, resolveReleaseCommerceView } from '../release-manifest'
|
||||
import type { ReleaseManifestPlatformView } from '../release-manifest'
|
||||
import { buildLoginPath, buildProtectedDownloadPath, buildSupportPath } from '../site-routes'
|
||||
import {
|
||||
browserLimitCards,
|
||||
|
|
@ -211,6 +212,101 @@ export function PricingPage() {
|
|||
)
|
||||
}
|
||||
|
||||
function ReleaseManifestStatusSections({
|
||||
isLoading,
|
||||
isError,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Section title="Release manifest status">
|
||||
<article className="callout">
|
||||
<p>Loading the current server-backed release manifest. Static preview metadata remains visible until the live manifest arrives.</p>
|
||||
</article>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
{isError ? (
|
||||
<Section title="Release manifest status">
|
||||
<OperationalStatusCallout
|
||||
badge="Public release fallback"
|
||||
title="The live release manifest could not be loaded from the auth server right now."
|
||||
summary="This page is showing bounded fallback site metadata instead of current runtime release authority, and direct package delivery stays intentionally withheld until that authority returns."
|
||||
sections={[
|
||||
{
|
||||
title: 'What still works',
|
||||
items: [
|
||||
'Supported targets, packaged proof, and public rollout guidance remain visible.',
|
||||
'Platform selection can still be preserved into the protected sign-in and download handoff.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'What stays intentionally withheld',
|
||||
items: [
|
||||
'Raw desktop delivery URLs remain behind the protected release lane.',
|
||||
'This public page does not claim live entitled release authority while the auth server is unavailable.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Recommended recovery order',
|
||||
items: [
|
||||
'Use the protected dashboard once the auth server recovers if you need actual package delivery.',
|
||||
'Open operator support if release-authority fallback persists during rollout or purchase work.',
|
||||
],
|
||||
},
|
||||
]}
|
||||
actions={[
|
||||
{ label: 'Open protected downloads', to: buildProtectedDownloadPath('windows') },
|
||||
{ label: 'Open support', to: buildSupportPath('operator-access') },
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function AvailableTargetsSection({ platforms }: { platforms: readonly ReleaseManifestPlatformView[] }) {
|
||||
return (
|
||||
<Section title="Available targets">
|
||||
<div className="card-grid">
|
||||
{platforms.map((platform) => {
|
||||
const metadataItems = buildReleaseMetadataItems(platform)
|
||||
return (
|
||||
<article key={platform.platform_key} className="card">
|
||||
<h3>{platform.platform}</h3>
|
||||
<p className="status-pill">{platform.subtitle}</p>
|
||||
<p>{platform.details}</p>
|
||||
{metadataItems.length > 0 ? (
|
||||
<ul className="list top-gap">
|
||||
{metadataItems.map((item) => (
|
||||
<li key={`${platform.platform_key}-${item.label}`}>
|
||||
{item.label}: {item.value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
<ReleaseValidationSummary platform={platform} />
|
||||
{platform.configured ? (
|
||||
<Link className="button button--primary button--full" to={buildProtectedDownloadPath(platform.platform_key)}>
|
||||
Sign in for {platform.platform} access
|
||||
</Link>
|
||||
) : (
|
||||
<div className="button button--ghost button--full is-disabled" aria-disabled="true">
|
||||
Release URL not configured yet
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export function DownloadPage() {
|
||||
const { releaseManifestQuery, releaseManifest, windowsValidationPlatform } = usePublicReleaseManifestView('public-download')
|
||||
|
||||
|
|
@ -226,84 +322,12 @@ export function DownloadPage() {
|
|||
title="Download the desktop build and pair it with your browser account."
|
||||
lede="The website provides account, release, and legal surfaces. The actual simulator ships through the desktop lane, with package validation and release discipline carried over from the HyperTwist roadmap."
|
||||
>
|
||||
{releaseManifestQuery.isLoading ? (
|
||||
<Section title="Release manifest status">
|
||||
<article className="callout">
|
||||
<p>Loading the current server-backed release manifest. Static preview metadata remains visible until the live manifest arrives.</p>
|
||||
</article>
|
||||
</Section>
|
||||
) : null}
|
||||
<ReleaseManifestStatusSections
|
||||
isLoading={releaseManifestQuery.isLoading}
|
||||
isError={releaseManifestQuery.isError}
|
||||
/>
|
||||
|
||||
{releaseManifestQuery.isError ? (
|
||||
<Section title="Release manifest status">
|
||||
<OperationalStatusCallout
|
||||
badge="Public release fallback"
|
||||
title="The live release manifest could not be loaded from the auth server right now."
|
||||
summary="This page is showing bounded fallback site metadata instead of current runtime release authority, and direct package delivery stays intentionally withheld until that authority returns."
|
||||
sections={[
|
||||
{
|
||||
title: 'What still works',
|
||||
items: [
|
||||
'Supported targets, packaged proof, and public rollout guidance remain visible.',
|
||||
'Platform selection can still be preserved into the protected sign-in and download handoff.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'What stays intentionally withheld',
|
||||
items: [
|
||||
'Raw desktop delivery URLs remain behind the protected release lane.',
|
||||
'This public page does not claim live entitled release authority while the auth server is unavailable.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Recommended recovery order',
|
||||
items: [
|
||||
'Use the protected dashboard once the auth server recovers if you need actual package delivery.',
|
||||
'Open operator support if release-authority fallback persists during rollout or purchase work.',
|
||||
],
|
||||
},
|
||||
]}
|
||||
actions={[
|
||||
{ label: 'Open protected downloads', to: buildProtectedDownloadPath('windows') },
|
||||
{ label: 'Open support', to: buildSupportPath('operator-access') },
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
<Section title="Available targets">
|
||||
<div className="card-grid">
|
||||
{releaseManifest.platforms.map((platform) => {
|
||||
const metadataItems = buildReleaseMetadataItems(platform)
|
||||
return (
|
||||
<article key={platform.platform_key} className="card">
|
||||
<h3>{platform.platform}</h3>
|
||||
<p className="status-pill">{platform.subtitle}</p>
|
||||
<p>{platform.details}</p>
|
||||
{metadataItems.length > 0 ? (
|
||||
<ul className="list top-gap">
|
||||
{metadataItems.map((item) => (
|
||||
<li key={`${platform.platform_key}-${item.label}`}>
|
||||
{item.label}: {item.value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
<ReleaseValidationSummary platform={platform} />
|
||||
{platform.configured ? (
|
||||
<Link className="button button--primary button--full" to={buildProtectedDownloadPath(platform.platform_key)}>
|
||||
Sign in for {platform.platform} access
|
||||
</Link>
|
||||
) : (
|
||||
<div className="button button--ghost button--full is-disabled" aria-disabled="true">
|
||||
Release URL not configured yet
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Section>
|
||||
<AvailableTargetsSection platforms={releaseManifest.platforms} />
|
||||
|
||||
<Section title="How the release lane works">
|
||||
<div className="card-grid">
|
||||
|
|
|
|||
|
|
@ -66,6 +66,325 @@ import {
|
|||
usePublicReleaseManifestView,
|
||||
} from './public-page-helpers'
|
||||
|
||||
function HomeHeroPanel() {
|
||||
return (
|
||||
<section className="hero-panel">
|
||||
<div className="hero-grid">
|
||||
<div className="hero-copy">
|
||||
<p>
|
||||
HyperTwist is a native training environment for classic cube practice, recognition
|
||||
and correction closure, replay explanation, coaching, higher-dimensional puzzle
|
||||
families, and serious packaged study sessions. The web lane is intentionally narrower:
|
||||
it owns access, documentation, release posture, support, and pairing so the
|
||||
downloadable can stay focused on the simulator itself.
|
||||
</p>
|
||||
<div className="button-row">
|
||||
<Link className="button button--primary" to="/download">
|
||||
Download desktop app
|
||||
</Link>
|
||||
<Link className="button button--ghost" to="/browser">
|
||||
Why keep the web version?
|
||||
</Link>
|
||||
<Link className="button button--ghost" to="/help">
|
||||
Open help center
|
||||
</Link>
|
||||
<Link className="button button--ghost" to="/app">
|
||||
Open operator dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-visual-card">
|
||||
<img
|
||||
src="/branding/hypertwist-3d-symbol.png"
|
||||
alt="HyperTwist symbol"
|
||||
className="hero-visual-card__image"
|
||||
/>
|
||||
<p className="hero-visual-card__caption">
|
||||
Browser shell for access, rollout, and support. Native Unreal runtime for the simulator.
|
||||
</p>
|
||||
<ul className="list top-gap">
|
||||
<li>Use the web for pricing, docs, release notes, notices, and protected downloads.</li>
|
||||
<li>Use the downloadable for real cube-state workflows, higher-dimensional maps, and diagnostics.</li>
|
||||
<li>Read XR/controller widening honestly as a separate native completion packet, not as a finished browser or desktop claim today.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric-grid">
|
||||
{heroMetrics.map((metric) => (
|
||||
<article key={metric.label} className="metric-card">
|
||||
<strong>{metric.value}</strong>
|
||||
<span>{metric.label}</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeShippingNowSection() {
|
||||
return (
|
||||
<Section
|
||||
title="What ships now"
|
||||
description="The public site only describes current product truth or explicitly marked retained/spec-only branches."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{shippingNowCards.map((item) => (
|
||||
<article key={item} className="card card--compact">
|
||||
<Sparkles size={18} />
|
||||
<p>{item}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeCapabilityPillarsSection() {
|
||||
return (
|
||||
<Section
|
||||
title="Capability pillars"
|
||||
description="The page structure keeps HyperTwist's public shell disciplined around real runtime authority instead of mixing product narrative, rollout status, and simulator claims together."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{capabilityPillars.map((pillar) => (
|
||||
<article key={pillar.title} className="card">
|
||||
<h3>{pillar.title}</h3>
|
||||
<p>{pillar.description}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeRoadmapHonestySection() {
|
||||
return (
|
||||
<Section
|
||||
title="Roadmap-honest posture"
|
||||
description="Important boundaries stay visible instead of being blurred into vague marketing claims."
|
||||
>
|
||||
<div className="split-grid">
|
||||
{roadmapHonestyCards.map((item) => (
|
||||
<article key={item} className="callout">
|
||||
<p>{item}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeDeliverySurfacesSection() {
|
||||
return (
|
||||
<Section
|
||||
title="Delivery surfaces"
|
||||
description="Use the website for account, release, pricing, and notices. Use the desktop runtime for the core simulator."
|
||||
>
|
||||
<div className="feature-band">
|
||||
<article className="feature-band__card">
|
||||
<MonitorCog size={22} />
|
||||
<h3>Browser account and operator shell</h3>
|
||||
<p>Authenticated browser access for release posture, desktop pairing, notices, and operator state.</p>
|
||||
<Link to="/app" className="inline-link">
|
||||
Open dashboard <ArrowRight size={15} />
|
||||
</Link>
|
||||
</article>
|
||||
<article className="feature-band__card">
|
||||
<Download size={22} />
|
||||
<h3>Desktop download and package lane</h3>
|
||||
<p>Public download posture for the native Unreal build, with legal linkage already wired in.</p>
|
||||
<Link to="/download" className="inline-link">
|
||||
Open download center <ArrowRight size={15} />
|
||||
</Link>
|
||||
</article>
|
||||
<article className="feature-band__card">
|
||||
<Landmark size={22} />
|
||||
<h3>Checkout and notices discipline</h3>
|
||||
<p>Paddle-ready pricing plus public notices surfaces for any downloadable build containing MPL-covered material.</p>
|
||||
<Link to="/open-source-notices" className="inline-link">
|
||||
Review notices <ArrowRight size={15} />
|
||||
</Link>
|
||||
</article>
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeFirstSessionSection() {
|
||||
return (
|
||||
<Section
|
||||
title="How a real first session flows"
|
||||
description="This is the public-facing operator path from curiosity into the actual simulator lane, without pretending the browser already replaced the desktop runtime."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{operatorManualTracks.slice(0, 3).map((track) => (
|
||||
<article key={track.title} className="card">
|
||||
<h3>{track.title}</h3>
|
||||
<p>{track.description}</p>
|
||||
<ul className="list top-gap">
|
||||
{track.steps.map((step) => (
|
||||
<li key={step}>{step}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
type ResourceCollection = {
|
||||
readonly title: string
|
||||
readonly items: readonly string[]
|
||||
}
|
||||
|
||||
function filterResourceCollections(query: string): readonly ResourceCollection[] {
|
||||
const normalized = query.trim().toLowerCase()
|
||||
if (!normalized) return resourceCollections
|
||||
|
||||
return resourceCollections
|
||||
.map((collection) => ({
|
||||
...collection,
|
||||
items: collection.items.filter((item) => (
|
||||
item.toLowerCase().includes(normalized) || collection.title.toLowerCase().includes(normalized)
|
||||
)),
|
||||
}))
|
||||
.filter((collection) => collection.items.length > 0)
|
||||
}
|
||||
|
||||
function ResourcesFinderSection({
|
||||
query,
|
||||
filteredCollections,
|
||||
onQueryChange,
|
||||
}: {
|
||||
query: string
|
||||
filteredCollections: readonly ResourceCollection[]
|
||||
onQueryChange: (value: string) => void
|
||||
}) {
|
||||
return (
|
||||
<Section title="Resource finder" description="Search the public resource categories that matter to operators and buyers.">
|
||||
<label className="input-label" htmlFor="resource-query">Filter resources</label>
|
||||
<input
|
||||
id="resource-query"
|
||||
className="input"
|
||||
value={query}
|
||||
onChange={(event) => onQueryChange(event.target.value)}
|
||||
placeholder="Search training, rollout, notices..."
|
||||
/>
|
||||
<div className="card-grid top-gap">
|
||||
{filteredCollections.map((collection) => (
|
||||
<article key={collection.title} className="card">
|
||||
<h3>{collection.title}</h3>
|
||||
<ul className="list">
|
||||
{collection.items.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function ResourcesDirectRoutesSection() {
|
||||
return (
|
||||
<Section title="Direct routes">
|
||||
<div className="feature-band">
|
||||
<Link to="/browser" className="feature-band__card feature-band__card--link">
|
||||
<ExternalLink size={22} />
|
||||
<h3>Browser guide</h3>
|
||||
<p>The public explanation of what the web version is for, what it is weaker at, and when to move into the downloadable.</p>
|
||||
</Link>
|
||||
<Link to="/features" className="feature-band__card feature-band__card--link">
|
||||
<Boxes size={22} />
|
||||
<h3>Feature atlas</h3>
|
||||
<p>One public page for current capability, surface boundaries, and release posture.</p>
|
||||
</Link>
|
||||
<Link to="/help" className="feature-band__card feature-band__card--link">
|
||||
<MonitorCog size={22} />
|
||||
<h3>Help center</h3>
|
||||
<p>Knowledge-base style guidance for onboarding, controls, release handoff, and rollout-safe troubleshooting.</p>
|
||||
</Link>
|
||||
<Link to="/getting-started" className="feature-band__card feature-band__card--link">
|
||||
<MonitorCog size={22} />
|
||||
<h3>Getting started</h3>
|
||||
<p>The canonical first-session path from browser access into the packaged desktop runtime.</p>
|
||||
</Link>
|
||||
<Link to="/launch-status" className="feature-band__card feature-band__card--link">
|
||||
<Landmark size={22} />
|
||||
<h3>Launch status</h3>
|
||||
<p>The canonical public checklist for preview-versus-launch posture, release authority, and rollout blockers.</p>
|
||||
</Link>
|
||||
<Link to="/docs" className="feature-band__card feature-band__card--link">
|
||||
<BookOpenText size={22} />
|
||||
<h3>Docs landing</h3>
|
||||
<p>Product-facing documentation, boundaries, and rollout guidance.</p>
|
||||
</Link>
|
||||
<Link to="/support" className="feature-band__card feature-band__card--link">
|
||||
<ExternalLink size={22} />
|
||||
<h3>Support</h3>
|
||||
<p>Contact, rollout questions, and account/download help.</p>
|
||||
</Link>
|
||||
<Link to="/contact" className="feature-band__card feature-band__card--link">
|
||||
<Landmark size={22} />
|
||||
<h3>Contact</h3>
|
||||
<p>The direct operator contact route for access, runtime, rollout, pricing, and notice follow-through.</p>
|
||||
</Link>
|
||||
<Link to="/changelog" className="feature-band__card feature-band__card--link">
|
||||
<Sparkles size={22} />
|
||||
<h3>Release notes</h3>
|
||||
<p>Recent public-facing packets and posture updates.</p>
|
||||
</Link>
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function ResourcesOperatorPlaybooksSection() {
|
||||
return (
|
||||
<Section
|
||||
title="Operator playbooks"
|
||||
description="These are the public-safe working patterns that matter once a team moves from curiosity into real rollout."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{operatorPlaybooks.map((playbook) => (
|
||||
<article key={playbook.title} className="card">
|
||||
<h3>{playbook.title}</h3>
|
||||
<ul className="list top-gap">
|
||||
{playbook.steps.map((step) => (
|
||||
<li key={step}>{step}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function ResourcesDeploymentSnapshotSection() {
|
||||
return (
|
||||
<Section
|
||||
title="Deployment readiness snapshot"
|
||||
description="The public site can be detailed without becoming misleading when rollout guidance stays separated into identity, package, and legal lanes."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{deploymentReadinessTracks.map((track) => (
|
||||
<article key={track.title} className="card">
|
||||
<h3>{track.title}</h3>
|
||||
<ul className="list top-gap">
|
||||
{track.steps.map((step) => (
|
||||
<li key={step}>{step}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export function HomeLanding() {
|
||||
const { releaseManifest, windowsValidationPlatform } = usePublicReleaseManifestView('public-home')
|
||||
|
||||
|
|
@ -81,56 +400,7 @@ export function HomeLanding() {
|
|||
title="From first solves to 120-cell, HyperTwist keeps the real simulator in the downloadable."
|
||||
lede="The website is a real product surface for account access, pricing, release posture, legal follow-through, and operator help. The downloadable is the much more powerful runtime for recognition, replay, coaching, higher-dimensional execution, and package-validated training."
|
||||
>
|
||||
<section className="hero-panel">
|
||||
<div className="hero-grid">
|
||||
<div className="hero-copy">
|
||||
<p>
|
||||
HyperTwist is a native training environment for classic cube practice, recognition
|
||||
and correction closure, replay explanation, coaching, higher-dimensional puzzle
|
||||
families, and serious packaged study sessions. The web lane is intentionally narrower:
|
||||
it owns access, documentation, release posture, support, and pairing so the
|
||||
downloadable can stay focused on the simulator itself.
|
||||
</p>
|
||||
<div className="button-row">
|
||||
<Link className="button button--primary" to="/download">
|
||||
Download desktop app
|
||||
</Link>
|
||||
<Link className="button button--ghost" to="/browser">
|
||||
Why keep the web version?
|
||||
</Link>
|
||||
<Link className="button button--ghost" to="/help">
|
||||
Open help center
|
||||
</Link>
|
||||
<Link className="button button--ghost" to="/app">
|
||||
Open operator dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-visual-card">
|
||||
<img
|
||||
src="/branding/hypertwist-3d-symbol.png"
|
||||
alt="HyperTwist symbol"
|
||||
className="hero-visual-card__image"
|
||||
/>
|
||||
<p className="hero-visual-card__caption">
|
||||
Browser shell for access, rollout, and support. Native Unreal runtime for the simulator.
|
||||
</p>
|
||||
<ul className="list top-gap">
|
||||
<li>Use the web for pricing, docs, release notes, notices, and protected downloads.</li>
|
||||
<li>Use the downloadable for real cube-state workflows, higher-dimensional maps, and diagnostics.</li>
|
||||
<li>Read XR/controller widening honestly as a separate native completion packet, not as a finished browser or desktop claim today.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric-grid">
|
||||
{heroMetrics.map((metric) => (
|
||||
<article key={metric.label} className="metric-card">
|
||||
<strong>{metric.value}</strong>
|
||||
<span>{metric.label}</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<HomeHeroPanel />
|
||||
|
||||
<Section
|
||||
title="Current public site status"
|
||||
|
|
@ -151,19 +421,7 @@ export function HomeLanding() {
|
|||
description="The homepage should answer the web-versus-desktop question directly where first-time operators actually encounter the product boundary, instead of forcing them to infer it from deeper manuals or support pages."
|
||||
/>
|
||||
|
||||
<Section
|
||||
title="What ships now"
|
||||
description="The public site only describes current product truth or explicitly marked retained/spec-only branches."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{shippingNowCards.map((item) => (
|
||||
<article key={item} className="card card--compact">
|
||||
<Sparkles size={18} />
|
||||
<p>{item}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<HomeShippingNowSection />
|
||||
|
||||
<BulletCardSection
|
||||
title="Puzzle families and study lanes"
|
||||
|
|
@ -171,19 +429,7 @@ export function HomeLanding() {
|
|||
cards={puzzleCatalogCards}
|
||||
/>
|
||||
|
||||
<Section
|
||||
title="Capability pillars"
|
||||
description="The page structure keeps HyperTwist's public shell disciplined around real runtime authority instead of mixing product narrative, rollout status, and simulator claims together."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{capabilityPillars.map((pillar) => (
|
||||
<article key={pillar.title} className="card">
|
||||
<h3>{pillar.title}</h3>
|
||||
<p>{pillar.description}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<HomeCapabilityPillarsSection />
|
||||
|
||||
<BulletCardSection
|
||||
title="Who HyperTwist is for"
|
||||
|
|
@ -191,50 +437,9 @@ export function HomeLanding() {
|
|||
cards={audienceFitCards}
|
||||
/>
|
||||
|
||||
<Section
|
||||
title="Roadmap-honest posture"
|
||||
description="Important boundaries stay visible instead of being blurred into vague marketing claims."
|
||||
>
|
||||
<div className="split-grid">
|
||||
{roadmapHonestyCards.map((item) => (
|
||||
<article key={item} className="callout">
|
||||
<p>{item}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<HomeRoadmapHonestySection />
|
||||
|
||||
<Section
|
||||
title="Delivery surfaces"
|
||||
description="Use the website for account, release, pricing, and notices. Use the desktop runtime for the core simulator."
|
||||
>
|
||||
<div className="feature-band">
|
||||
<article className="feature-band__card">
|
||||
<MonitorCog size={22} />
|
||||
<h3>Browser account and operator shell</h3>
|
||||
<p>Authenticated browser access for release posture, desktop pairing, notices, and operator state.</p>
|
||||
<Link to="/app" className="inline-link">
|
||||
Open dashboard <ArrowRight size={15} />
|
||||
</Link>
|
||||
</article>
|
||||
<article className="feature-band__card">
|
||||
<Download size={22} />
|
||||
<h3>Desktop download and package lane</h3>
|
||||
<p>Public download posture for the native Unreal build, with legal linkage already wired in.</p>
|
||||
<Link to="/download" className="inline-link">
|
||||
Open download center <ArrowRight size={15} />
|
||||
</Link>
|
||||
</article>
|
||||
<article className="feature-band__card">
|
||||
<Landmark size={22} />
|
||||
<h3>Checkout and notices discipline</h3>
|
||||
<p>Paddle-ready pricing plus public notices surfaces for any downloadable build containing MPL-covered material.</p>
|
||||
<Link to="/open-source-notices" className="inline-link">
|
||||
Review notices <ArrowRight size={15} />
|
||||
</Link>
|
||||
</article>
|
||||
</div>
|
||||
</Section>
|
||||
<HomeDeliverySurfacesSection />
|
||||
|
||||
<PublicManualRouteAtlasSection
|
||||
title="Which public page should you open next?"
|
||||
|
|
@ -268,24 +473,7 @@ export function HomeLanding() {
|
|||
description="The homepage now also makes the current shared-auth sign-in lineup visible before operators commit to the protected dashboard, pricing, or desktop-release handoff."
|
||||
/>
|
||||
|
||||
<Section
|
||||
title="How a real first session flows"
|
||||
description="This is the public-facing operator path from curiosity into the actual simulator lane, without pretending the browser already replaced the desktop runtime."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{operatorManualTracks.slice(0, 3).map((track) => (
|
||||
<article key={track.title} className="card">
|
||||
<h3>{track.title}</h3>
|
||||
<p>{track.description}</p>
|
||||
<ul className="list top-gap">
|
||||
{track.steps.map((step) => (
|
||||
<li key={step}>{step}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<HomeFirstSessionSection />
|
||||
|
||||
<StepCardSection
|
||||
title="Desktop workflows already supported"
|
||||
|
|
@ -465,16 +653,7 @@ export function ResourcesPage() {
|
|||
const deferredQuery = useDeferredValue(query)
|
||||
const { releaseManifest, windowsValidationPlatform } = usePublicReleaseManifestView('public-resources')
|
||||
|
||||
const filteredCollections = useMemo(() => {
|
||||
const normalized = deferredQuery.trim().toLowerCase()
|
||||
if (!normalized) return resourceCollections
|
||||
return resourceCollections
|
||||
.map((collection) => ({
|
||||
...collection,
|
||||
items: collection.items.filter((item) => item.toLowerCase().includes(normalized) || collection.title.toLowerCase().includes(normalized)),
|
||||
}))
|
||||
.filter((collection) => collection.items.length > 0)
|
||||
}, [deferredQuery])
|
||||
const filteredCollections = useMemo(() => filterResourceCollections(deferredQuery), [deferredQuery])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -488,78 +667,13 @@ export function ResourcesPage() {
|
|||
title="Resources that explain the product without leaking operator-only internals."
|
||||
lede="This surface is organized as a deliberate HyperTwist resource portal, constrained to product-safe rollout, simulator, and distribution guidance."
|
||||
>
|
||||
<Section title="Resource finder" description="Search the public resource categories that matter to operators and buyers.">
|
||||
<label className="input-label" htmlFor="resource-query">Filter resources</label>
|
||||
<input
|
||||
id="resource-query"
|
||||
className="input"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search training, rollout, notices..."
|
||||
/>
|
||||
<div className="card-grid top-gap">
|
||||
{filteredCollections.map((collection) => (
|
||||
<article key={collection.title} className="card">
|
||||
<h3>{collection.title}</h3>
|
||||
<ul className="list">
|
||||
{collection.items.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<ResourcesFinderSection
|
||||
query={query}
|
||||
filteredCollections={filteredCollections}
|
||||
onQueryChange={setQuery}
|
||||
/>
|
||||
|
||||
<Section title="Direct routes">
|
||||
<div className="feature-band">
|
||||
<Link to="/browser" className="feature-band__card feature-band__card--link">
|
||||
<ExternalLink size={22} />
|
||||
<h3>Browser guide</h3>
|
||||
<p>The public explanation of what the web version is for, what it is weaker at, and when to move into the downloadable.</p>
|
||||
</Link>
|
||||
<Link to="/features" className="feature-band__card feature-band__card--link">
|
||||
<Boxes size={22} />
|
||||
<h3>Feature atlas</h3>
|
||||
<p>One public page for current capability, surface boundaries, and release posture.</p>
|
||||
</Link>
|
||||
<Link to="/help" className="feature-band__card feature-band__card--link">
|
||||
<MonitorCog size={22} />
|
||||
<h3>Help center</h3>
|
||||
<p>Knowledge-base style guidance for onboarding, controls, release handoff, and rollout-safe troubleshooting.</p>
|
||||
</Link>
|
||||
<Link to="/getting-started" className="feature-band__card feature-band__card--link">
|
||||
<MonitorCog size={22} />
|
||||
<h3>Getting started</h3>
|
||||
<p>The canonical first-session path from browser access into the packaged desktop runtime.</p>
|
||||
</Link>
|
||||
<Link to="/launch-status" className="feature-band__card feature-band__card--link">
|
||||
<Landmark size={22} />
|
||||
<h3>Launch status</h3>
|
||||
<p>The canonical public checklist for preview-versus-launch posture, release authority, and rollout blockers.</p>
|
||||
</Link>
|
||||
<Link to="/docs" className="feature-band__card feature-band__card--link">
|
||||
<BookOpenText size={22} />
|
||||
<h3>Docs landing</h3>
|
||||
<p>Product-facing documentation, boundaries, and rollout guidance.</p>
|
||||
</Link>
|
||||
<Link to="/support" className="feature-band__card feature-band__card--link">
|
||||
<ExternalLink size={22} />
|
||||
<h3>Support</h3>
|
||||
<p>Contact, rollout questions, and account/download help.</p>
|
||||
</Link>
|
||||
<Link to="/contact" className="feature-band__card feature-band__card--link">
|
||||
<Landmark size={22} />
|
||||
<h3>Contact</h3>
|
||||
<p>The direct operator contact route for access, runtime, rollout, pricing, and notice follow-through.</p>
|
||||
</Link>
|
||||
<Link to="/changelog" className="feature-band__card feature-band__card--link">
|
||||
<Sparkles size={22} />
|
||||
<h3>Release notes</h3>
|
||||
<p>Recent public-facing packets and posture updates.</p>
|
||||
</Link>
|
||||
</div>
|
||||
</Section>
|
||||
<ResourcesDirectRoutesSection />
|
||||
|
||||
<PublicManualRouteAtlasSection
|
||||
description="The resource portal is stronger when it also explains what each public page is for, so teams can move from reference reading into the exact route that owns the next question."
|
||||
|
|
@ -584,23 +698,7 @@ export function ResourcesPage() {
|
|||
description="Resources are stronger when they do not stop at explanation: this public guide says plainly whether the next move is protected downloads, pricing, browser/account follow-through, or notices/source review."
|
||||
/>
|
||||
|
||||
<Section
|
||||
title="Operator playbooks"
|
||||
description="These are the public-safe working patterns that matter once a team moves from curiosity into real rollout."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{operatorPlaybooks.map((playbook) => (
|
||||
<article key={playbook.title} className="card">
|
||||
<h3>{playbook.title}</h3>
|
||||
<ul className="list top-gap">
|
||||
{playbook.steps.map((step) => (
|
||||
<li key={step}>{step}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<ResourcesOperatorPlaybooksSection />
|
||||
|
||||
<StepCardSection
|
||||
title="Practical software workflows"
|
||||
|
|
@ -645,23 +743,7 @@ export function ResourcesPage() {
|
|||
description="This public-safe guide focuses on how operators and trainees actually drive the current desktop runtime today."
|
||||
/>
|
||||
|
||||
<Section
|
||||
title="Deployment readiness snapshot"
|
||||
description="The public site can be detailed without becoming misleading when rollout guidance stays separated into identity, package, and legal lanes."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{deploymentReadinessTracks.map((track) => (
|
||||
<article key={track.title} className="card">
|
||||
<h3>{track.title}</h3>
|
||||
<ul className="list top-gap">
|
||||
{track.steps.map((step) => (
|
||||
<li key={step}>{step}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<ResourcesDeploymentSnapshotSection />
|
||||
|
||||
<PublicPackagedDesktopProofSection
|
||||
platform={windowsValidationPlatform}
|
||||
|
|
|
|||
|
|
@ -513,6 +513,85 @@ img {
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.product-reality-strip {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.85rem;
|
||||
margin: -0.35rem 0 1.4rem;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--ht-border);
|
||||
border-radius: 1.45rem;
|
||||
background:
|
||||
radial-gradient(circle at 12% 0%, rgba(84, 203, 255, 0.12), transparent 34%),
|
||||
radial-gradient(circle at 88% 12%, rgba(247, 178, 103, 0.11), transparent 30%),
|
||||
rgba(8, 13, 27, 0.62);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
||||
0 18px 54px rgba(0, 0, 0, 0.14);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-reality-strip::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(90deg, transparent, rgba(84, 203, 255, 0.09), transparent);
|
||||
transform: translateX(-62%);
|
||||
animation: ht-scanline 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.product-reality-strip__item {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
padding: 0.9rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.045);
|
||||
border-radius: 1.05rem;
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.product-reality-strip__item .eyebrow {
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.product-reality-strip__item strong {
|
||||
display: block;
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.product-reality-strip__item p:last-child {
|
||||
margin: 0.55rem 0 0;
|
||||
color: var(--ht-muted);
|
||||
line-height: 1.55;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.product-reality-strip--compact {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.product-reality-strip--compact .product-reality-strip__item {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
@keyframes ht-scanline {
|
||||
0%, 45% {
|
||||
transform: translateX(-62%);
|
||||
opacity: 0;
|
||||
}
|
||||
55% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(62%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding-left: 1.15rem;
|
||||
|
|
@ -923,6 +1002,10 @@ code {
|
|||
.site-footer__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.product-reality-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
|
|
@ -962,4 +1045,9 @@ code {
|
|||
max-width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.product-reality-strip,
|
||||
.product-reality-strip--compact {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue