mirror of
https://github.com/simstudioai/sim.git
synced 2026-08-29 02:27:35 +08:00
fix(ui): unify branded error pages (#7057)
* fix(ui): unify branded error pages * fix(desktop): match canonical chip chrome * test(ui): update error chip mock * fix(desktop): package offline font reliably
This commit is contained in:
@@ -110,5 +110,18 @@ test.describe('desktop shell smoke', () => {
|
||||
const window = await app.firstWindow()
|
||||
await window.waitForSelector('#retry', { timeout: 30_000 })
|
||||
expect(window.url().startsWith('file:')).toBe(true)
|
||||
await expect(window.locator('.wordmark')).toBeVisible()
|
||||
await expect(window.locator('.wordmark')).toHaveAttribute('aria-label', 'Sim')
|
||||
await expect(window.locator('#title')).toHaveText('Can’t connect to Sim')
|
||||
await expect(window.locator('#status')).toHaveText('Check status')
|
||||
await expect
|
||||
.poll(() => window.evaluate(() => document.fonts.check('16px "Season Sans"')))
|
||||
.toBe(true)
|
||||
await expect(window.locator('#retry')).toHaveCSS('height', '30px')
|
||||
await expect(window.locator('#retry')).toHaveCSS('border-radius', '8px')
|
||||
await expect(window.locator('#retry')).toHaveCSS('padding-left', '8px')
|
||||
await expect(window.locator('#retry')).toHaveCSS('font-size', '14px')
|
||||
await expect(window.locator('#retry')).toHaveCSS('line-height', '20px')
|
||||
await expect(window.locator('#retry')).toHaveCSS('text-align', 'left')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,10 @@ files:
|
||||
- dist/**
|
||||
- static/**
|
||||
- package.json
|
||||
- from: ../sim/public/brand/fonts
|
||||
to: static
|
||||
filter:
|
||||
- SeasonSansUprightsVF.woff2
|
||||
|
||||
asar: true
|
||||
|
||||
|
||||
@@ -4,26 +4,27 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'"
|
||||
content="default-src 'none'; font-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'"
|
||||
/>
|
||||
<title>Sim — Can’t connect</title>
|
||||
<title>Sim - Can’t connect</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: #ffffff;
|
||||
--fg: #0c0c0c;
|
||||
--muted: #6b6b6b;
|
||||
--border: #e4e4e4;
|
||||
--accent: #701ffc;
|
||||
--accent-fg: #ffffff;
|
||||
@font-face {
|
||||
font-family: 'Season Sans';
|
||||
src:
|
||||
url('../../sim/public/brand/fonts/SeasonSansUprightsVF.woff2') format('woff2'),
|
||||
url('./SeasonSansUprightsVF.woff2') format('woff2');
|
||||
font-style: normal;
|
||||
font-weight: 300 800;
|
||||
font-display: block;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0c0c0c;
|
||||
--fg: #f4f4f4;
|
||||
--muted: #9a9a9a;
|
||||
--border: #262626;
|
||||
}
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #fefefe;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-body: #434343;
|
||||
--text-muted: #7a7a7a;
|
||||
--text-inverse: #ffffff;
|
||||
--surface-hover: #f2f2f2;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -35,111 +36,171 @@
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
min-width: 320px;
|
||||
padding-top: max(env(titlebar-area-height, 38px), 38px);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Season Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
user-select: none;
|
||||
}
|
||||
main {
|
||||
max-width: 420px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
header {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
max-width: 1460px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 80px;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
.mark {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 0 auto 20px;
|
||||
border-radius: 14px;
|
||||
background: var(--accent);
|
||||
.wordmark {
|
||||
display: block;
|
||||
width: 37px;
|
||||
height: 18px;
|
||||
margin: 6px 0;
|
||||
color: var(--text-body);
|
||||
}
|
||||
main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent-fg);
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
padding: 0 16px 64px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 410px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
font-size: 40px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.1;
|
||||
text-wrap: balance;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--muted);
|
||||
margin: 0 0 24px;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
button {
|
||||
display: inline-flex;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
appearance: none;
|
||||
border: 1px solid var(--border);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
color: var(--text-body);
|
||||
border-radius: 8px;
|
||||
padding: 7px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 0 8px;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition:
|
||||
color 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
text-decoration-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
fill 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
stroke 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
button .label {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: currentColor;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
button.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent-fg);
|
||||
background: var(--text-primary);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
button:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.links {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.links a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.links a:hover {
|
||||
color: var(--fg);
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
button.primary:hover {
|
||||
background: var(--text-body);
|
||||
}
|
||||
button.secondary:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
}
|
||||
#detail {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-top: 16px;
|
||||
min-height: 17px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-muted);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
header {
|
||||
padding-right: 32px;
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
header {
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header aria-label="Sim">
|
||||
<svg class="wordmark" viewBox="0 0 441 212" role="img" aria-label="Sim">
|
||||
<g fill="currentColor">
|
||||
<path d="M0 160.9H29.51C29.51 169.08 32.46 175.61 38.37 180.48C44.27 185.12 52.25 187.44 62.31 187.44C73.24 187.44 81.65 185.34 87.56 181.14C93.46 176.71 96.41 170.85 96.41 163.55C96.41 158.24 94.77 153.82 91.49 150.28C88.43 146.74 82.75 143.86 74.44 141.65L46.24 135.01C32.03 131.47 21.42 126.05 14.43 118.75C7.65 111.45 4.26 101.83 4.26 89.88C4.26 79.93 6.78 71.3 11.81 64C17.05 56.7 24.16 51.06 33.12 47.08C42.3 43.09 52.8 41.1 64.6 41.1C76.41 41.1 86.57 43.2 95.1 47.41C103.84 51.61 110.62 57.47 115.43 64.99C120.46 72.52 123.08 81.48 123.3 91.87H93.79C93.57 83.47 90.84 76.94 85.59 72.3C80.34 67.65 73.02 65.33 63.62 65.33C54 65.33 46.57 67.43 41.32 71.63C36.07 75.83 33.45 81.59 33.45 88.89C33.45 99.73 41.32 107.14 57.06 111.12L85.26 118.09C98.81 121.19 108.98 126.28 115.76 133.35C122.53 140.21 125.92 149.61 125.92 161.56C125.92 171.74 123.19 180.7 117.73 188.44C112.26 195.96 104.72 201.82 95.1 206.03C85.7 210.01 74.55 212 61.65 212C42.85 212 27.87 207.35 16.72 198.06C5.57 188.77 0 176.38 0 160.9Z" />
|
||||
<path d="M232.8 212H202.13L202.13 49.76H229.54V77.39C232.8 68.34 239.11 60.66 247.81 54.7C256.73 48.52 267.5 45.43 280.12 45.43C294.26 45.43 306.01 49.29 315.36 57.02C324.72 64.75 330.81 75.01 333.64 87.82H328.09C330.27 75.01 336.25 64.75 346.04 57.02C355.83 49.29 367.9 45.43 382.26 45.43C400.54 45.43 414.89 50.84 425.34 61.66C435.78 72.47 441 87.26 441 106.03V212H410.98V113.65C410.98 100.84 407.71 91.02 401.19 84.17C394.88 77.11 386.29 73.58 375.41 73.58C367.79 73.58 361.05 75.34 355.17 78.88C349.52 82.19 345.06 87.04 341.8 93.45C338.53 99.85 336.9 107.36 336.9 115.97V212H306.55V113.32C306.55 100.51 303.4 90.8 297.09 84.17C290.78 77.33 282.19 73.91 271.31 73.91C263.69 73.91 256.95 75.67 251.08 79.21C245.42 82.52 240.96 87.38 237.7 93.78C234.43 99.96 232.8 107.36 232.8 115.97V212Z" />
|
||||
<path d="M184.83 20.55C184.83 31.9 175.64 41.1 164.29 41.1C152.95 41.1 143.76 31.9 143.76 20.55C143.76 9.2 152.95 0 164.29 0C175.64 0 184.83 9.2 184.83 20.55Z" />
|
||||
<path d="M179.43 212H149.16V49.76C153.76 51.91 158.88 53.12 164.29 53.12C169.7 53.12 174.83 51.91 179.43 49.76V212Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</header>
|
||||
<main>
|
||||
<div class="mark">S</div>
|
||||
<h1 id="title">Can’t connect to Sim</h1>
|
||||
<p id="message">
|
||||
Sim couldn’t reach the server. Check your internet connection, then try again.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<button class="primary" id="retry">Retry</button>
|
||||
<div class="content">
|
||||
<h1 id="title">Can’t connect to Sim</h1>
|
||||
<p id="message">
|
||||
Sim couldn’t reach the server. Check your internet connection, then try again.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<button class="primary" id="retry"><span class="label">Retry</span></button>
|
||||
<button class="secondary" id="status"><span class="label">Check status</span></button>
|
||||
</div>
|
||||
<div id="detail" role="status"></div>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a id="status">Check status</a>
|
||||
</div>
|
||||
<div id="detail"></div>
|
||||
</main>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search)
|
||||
const copy = {
|
||||
offline: {
|
||||
title: 'You’re offline',
|
||||
message: 'Sim needs an internet connection. Reconnect, then try again — we’ll also retry automatically.',
|
||||
message: 'Sim needs an internet connection. Reconnect, then try again. We’ll also retry automatically.',
|
||||
},
|
||||
dns: {
|
||||
title: 'Can’t find the server',
|
||||
|
||||
@@ -8,17 +8,20 @@ export const metadata = {
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<DocsPage>
|
||||
<div className='flex min-h-[70vh] flex-col items-center justify-center gap-4 text-center'>
|
||||
<h1 className='bg-gradient-to-b from-[var(--brand-accent)] to-[var(--brand-accent-hover)] bg-clip-text font-semibold text-8xl text-transparent'>
|
||||
404
|
||||
</h1>
|
||||
<h2 className='font-semibold text-2xl text-[var(--text-primary)]'>Page Not Found</h2>
|
||||
<p className='text-[var(--text-muted)]'>
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink href='/' variant='primary'>
|
||||
Go home
|
||||
</ChipLink>
|
||||
<div className='flex min-h-[60vh] flex-col items-center justify-center px-4 py-24 text-center'>
|
||||
<div className='flex w-full max-w-[410px] flex-col items-center gap-3'>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Page not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<div className='mt-3 flex flex-wrap items-center justify-center gap-2'>
|
||||
<ChipLink href='/' variant='primary'>
|
||||
Return home
|
||||
</ChipLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocsPage>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
import { StatusPage } from '@/components/status-page'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function BlogAuthorNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
<StatusPage
|
||||
title='Author not found'
|
||||
description="The author you're looking for doesn't exist or has been moved."
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Author not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The author you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/blog' className='mt-3'>
|
||||
<ChipLink variant='primary' href='/blog'>
|
||||
Browse blog
|
||||
</ChipLink>
|
||||
</main>
|
||||
</StatusPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
import { StatusPage } from '@/components/status-page'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function BlogNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
<StatusPage
|
||||
title='Post not found'
|
||||
description="The post you're looking for doesn't exist or has been moved."
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Post not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The post you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/blog' className='mt-3'>
|
||||
<ChipLink variant='primary' href='/blog'>
|
||||
Browse blog
|
||||
</ChipLink>
|
||||
</main>
|
||||
</StatusPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
import { StatusPage } from '@/components/status-page'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function ComparisonNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1446px] flex-col items-center justify-center gap-3 px-12 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
<StatusPage
|
||||
title='Comparison not found'
|
||||
description="The comparison you're looking for doesn't exist or has been moved."
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Comparison not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The comparison you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/comparisons' className='mt-3'>
|
||||
<ChipLink variant='primary' href='/comparisons'>
|
||||
Browse comparisons
|
||||
</ChipLink>
|
||||
</main>
|
||||
</StatusPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
import { StatusPage } from '@/components/status-page'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function IntegrationsNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
<StatusPage
|
||||
title='Integration not found'
|
||||
description="The integration you're looking for doesn't exist or has been moved."
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Integration not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The integration you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/integrations' className='mt-3'>
|
||||
<ChipLink variant='primary' href='/integrations'>
|
||||
Browse integrations
|
||||
</ChipLink>
|
||||
</main>
|
||||
</StatusPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
import { StatusPage } from '@/components/status-page'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function LibraryAuthorNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
<StatusPage
|
||||
title='Author not found'
|
||||
description="The author you're looking for doesn't exist or has been moved."
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Author not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The author you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/library' className='mt-3'>
|
||||
<ChipLink variant='primary' href='/library'>
|
||||
Browse library
|
||||
</ChipLink>
|
||||
</main>
|
||||
</StatusPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
import { StatusPage } from '@/components/status-page'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function LibraryNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
<StatusPage
|
||||
title='Post not found'
|
||||
description="The post you're looking for doesn't exist or has been moved."
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Post not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The post you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/library' className='mt-3'>
|
||||
<ChipLink variant='primary' href='/library'>
|
||||
Browse library
|
||||
</ChipLink>
|
||||
</main>
|
||||
</StatusPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
import { StatusPage } from '@/components/status-page'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
@@ -8,19 +9,13 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function ModelsNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
<StatusPage
|
||||
title='Model not found'
|
||||
description="The model you're looking for doesn't exist or has been moved."
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Model not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The model you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/models' className='mt-3'>
|
||||
<ChipLink variant='primary' href='/models'>
|
||||
Browse models
|
||||
</ChipLink>
|
||||
</main>
|
||||
</StatusPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import NextError from 'next/error'
|
||||
import { Chip } from '@sim/emcn'
|
||||
import { StatusPageContent } from '@/components/status-page'
|
||||
import { season } from '@/app/_styles/fonts/season/season'
|
||||
import { LogoShell } from '@/app/(landing)/components'
|
||||
import '@/app/_styles/globals.css'
|
||||
|
||||
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
|
||||
interface GlobalErrorProps {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export default function GlobalError({ reset }: GlobalErrorProps) {
|
||||
return (
|
||||
<html lang='en'>
|
||||
<body>
|
||||
{/* `NextError` is the default Next.js error page component. Its type
|
||||
definition requires a `statusCode` prop. However, since the App Router
|
||||
does not expose status codes for errors, we simply pass 0 to render a
|
||||
generic error message. */}
|
||||
<NextError statusCode={0} />
|
||||
<html lang='en' className='light'>
|
||||
<body className={`${season.variable} font-season`}>
|
||||
<LogoShell center>
|
||||
<StatusPageContent
|
||||
title='Something went wrong'
|
||||
description='An unexpected error occurred. Please try again.'
|
||||
>
|
||||
<Chip variant='primary' onClick={reset}>
|
||||
Try again
|
||||
</Chip>
|
||||
</StatusPageContent>
|
||||
</LogoShell>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
import { StatusPageContent } from '@/components/status-page'
|
||||
import { LogoShell } from '@/app/(landing)/components'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -10,17 +11,14 @@ export const metadata: Metadata = {
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<LogoShell center>
|
||||
<div className='flex w-full max-w-[410px] flex-col items-center gap-3 text-center'>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Page not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/' className='mt-3'>
|
||||
<StatusPageContent
|
||||
title='Page not found'
|
||||
description="The page you're looking for doesn't exist or has been moved."
|
||||
>
|
||||
<ChipLink variant='primary' href='/'>
|
||||
Return home
|
||||
</ChipLink>
|
||||
</div>
|
||||
</StatusPageContent>
|
||||
</LogoShell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { type ReactNode, useEffect } from 'react'
|
||||
import { Button } from '@sim/emcn'
|
||||
import { Chip } from '@sim/emcn'
|
||||
import { TriangleAlert } from '@sim/emcn/icons'
|
||||
import { createLogger } from '@sim/logger'
|
||||
|
||||
@@ -30,7 +30,7 @@ interface ErrorShellProps {
|
||||
|
||||
/**
|
||||
* Centered layout shared by the workspace error boundary and not-found page.
|
||||
* Renders a framed glyph, serif headline, supporting paragraph, and a row of
|
||||
* Renders a framed glyph, brand headline, supporting paragraph, and a row of
|
||||
* action buttons.
|
||||
*/
|
||||
export function ErrorShell({ title, description, icon, children }: ErrorShellProps) {
|
||||
@@ -80,9 +80,9 @@ export function ErrorState({
|
||||
return (
|
||||
<ErrorShell title={title} description={description} icon={icon}>
|
||||
{children}
|
||||
<Button variant='primary' size='md' onClick={reset}>
|
||||
Refresh
|
||||
</Button>
|
||||
<Chip variant='primary' onClick={reset}>
|
||||
Try again
|
||||
</Chip>
|
||||
</ErrorShell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@sim/emcn', () => ({
|
||||
Button: ({ children, ...props }: { children: ReactNode } & Record<string, unknown>) => (
|
||||
Chip: ({ children, ...props }: { children: ReactNode } & Record<string, unknown>) => (
|
||||
<button {...props}>{children}</button>
|
||||
),
|
||||
}))
|
||||
@@ -58,7 +58,7 @@ describe('LogsError boundary', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('calls reset when the refresh action is clicked', () => {
|
||||
it('calls reset when the retry action is clicked', () => {
|
||||
const reset = vi.fn()
|
||||
const error = Object.assign(new Error('boom'), { digest: 'abc123' })
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('LogsError boundary', () => {
|
||||
})
|
||||
|
||||
act(() => {
|
||||
findButtonByText('Refresh').click()
|
||||
findButtonByText('Try again').click()
|
||||
})
|
||||
|
||||
expect(reset).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
||||
import { Button } from '@sim/emcn'
|
||||
import { RefreshCw } from '@sim/emcn/icons'
|
||||
import { Chip } from '@sim/emcn'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { truncate } from '@sim/utils/string'
|
||||
import { ReactFlowProvider } from 'reactflow'
|
||||
import { captureClientEvent, captureClientException } from '@/lib/posthog/client'
|
||||
import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
|
||||
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||
import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
|
||||
import { readCollapsedCookie } from '@/stores/sidebar/store'
|
||||
import { ErrorShell } from '@/app/workspace/[workspaceId]/components'
|
||||
|
||||
const logger = createLogger('ErrorBoundary')
|
||||
|
||||
@@ -24,51 +19,19 @@ interface ErrorUIProps {
|
||||
title?: string
|
||||
message?: string
|
||||
onReset?: () => void
|
||||
fullScreen?: boolean
|
||||
}
|
||||
|
||||
export function ErrorUI({
|
||||
title = 'Something went wrong',
|
||||
message = 'An unexpected error occurred. Please try again or refresh the page.',
|
||||
onReset,
|
||||
fullScreen = false,
|
||||
}: ErrorUIProps) {
|
||||
const preventZoomRef = usePreventZoom()
|
||||
|
||||
if (!fullScreen) {
|
||||
return (
|
||||
<div className='flex h-full flex-1 items-center justify-center'>
|
||||
<div className='flex flex-col items-center gap-4 text-center'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<h2 className='text-[var(--text-primary)] text-md'>{title}</h2>
|
||||
<p className='max-w-[300px] text-[var(--text-tertiary)] text-small'>{message}</p>
|
||||
</div>
|
||||
<Button variant='default' size='sm' onClick={onReset ?? (() => window.location.reload())}>
|
||||
<RefreshCw className='mr-1.5 size-[14px]' />
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={preventZoomRef} className='flex h-screen w-full flex-col bg-[var(--surface-1)]'>
|
||||
<Sidebar isCollapsed={readCollapsedCookie()} />
|
||||
|
||||
<div className='relative flex flex-1'>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
|
||||
<div className='pointer-events-none flex flex-col items-center gap-4'>
|
||||
<h3 className='text-[var(--text-primary)] text-md'>{title}</h3>
|
||||
<p className='max-w-sm text-center text-[var(--text-tertiary)] text-sm'>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReactFlowProvider>
|
||||
<Panel />
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
</div>
|
||||
<ErrorShell title={title} description={message}>
|
||||
<Chip variant='primary' onClick={onReset ?? (() => window.location.reload())}>
|
||||
Try again
|
||||
</Chip>
|
||||
</ErrorShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { StatusPage, StatusPageContent } from './status-page'
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface StatusPageContentProps {
|
||||
title: string
|
||||
description: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/** Shared centered content for full-page not-found and fatal-error states. */
|
||||
export function StatusPageContent({ title, description, children }: StatusPageContentProps) {
|
||||
return (
|
||||
<div className='flex w-full max-w-[410px] flex-col items-center gap-3 text-center'>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
{title}
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>{description}</p>
|
||||
<div className='mt-3 flex flex-wrap items-center justify-center gap-2'>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface StatusPageProps extends StatusPageContentProps {
|
||||
id?: string
|
||||
}
|
||||
|
||||
/** Shared landing-shell page frame for route-specific not-found states. */
|
||||
export function StatusPage({ title, description, children, id = 'main-content' }: StatusPageProps) {
|
||||
return (
|
||||
<main
|
||||
id={id}
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center px-20 py-24 max-sm:px-5 max-lg:px-8'
|
||||
>
|
||||
<StatusPageContent title={title} description={description}>
|
||||
{children}
|
||||
</StatusPageContent>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user