versions app
This commit is contained in:
parent
7574a8702e
commit
d45144478d
1
apps/versions/20260130-000000/.npmrc
Normal file
1
apps/versions/20260130-000000/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
registry=https://npm.nose.space
|
||||||
38
apps/versions/20260130-000000/bun.lock
Normal file
38
apps/versions/20260130-000000/bun.lock
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "versions",
|
||||||
|
"dependencies": {
|
||||||
|
"@because/forge": "*",
|
||||||
|
"@because/hype": "*",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@because/forge": ["@because/forge@0.0.1", "https://npm.nose.space/@because/forge/-/forge-0.0.1.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-QS5CK51gcWma91i4uECWe4HPJeNHcE+Af4SQHOcfEovyzOEa7VOTAjei+jIWr2i+abGWqQCEC9wIuFgPgyr2Bg=="],
|
||||||
|
|
||||||
|
"@because/hype": ["@because/hype@0.0.1", "https://npm.nose.space/@because/hype/-/hype-0.0.1.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-i92DNUXJOwt3J8dN1x8sh7i86blelcTCk8XDpwD839Ic8oe710lkDSVXJ7xYZb/i8YtzGhRg+L6eXDhaRiU2Pw=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.8", "https://npm.nose.space/@types/bun/-/bun-1.3.8.tgz", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.1.0", "https://npm.nose.space/@types/node/-/node-25.1.0.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.8", "https://npm.nose.space/bun-types/-/bun-types-1.3.8.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.11.7", "https://npm.nose.space/hono/-/hono-4.11.7.tgz", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
||||||
|
|
||||||
|
"kleur": ["kleur@4.1.5", "https://npm.nose.space/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "https://npm.nose.space/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "https://npm.nose.space/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
252
apps/versions/20260130-000000/index.tsx
Normal file
252
apps/versions/20260130-000000/index.tsx
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
import { Hype } from '@because/hype'
|
||||||
|
import { createThemes, define, stylesToCSS } from '@because/forge'
|
||||||
|
import { readdir, readlink, stat } from 'fs/promises'
|
||||||
|
import { join } from 'path'
|
||||||
|
import type { Child } from 'hono/jsx'
|
||||||
|
|
||||||
|
const APPS_DIR = process.env.APPS_DIR!
|
||||||
|
|
||||||
|
const app = new Hype({ prettyHTML: false })
|
||||||
|
|
||||||
|
const theme = createThemes({
|
||||||
|
light: {
|
||||||
|
bg: '#ffffff',
|
||||||
|
text: '#1a1a1a',
|
||||||
|
textMuted: '#666666',
|
||||||
|
border: '#dddddd',
|
||||||
|
borderSubtle: '#eeeeee',
|
||||||
|
borderStrong: '#333333',
|
||||||
|
hover: '#f5f5f5',
|
||||||
|
link: '#0066cc',
|
||||||
|
error: '#d32f2f',
|
||||||
|
errorBg: '#ffebee',
|
||||||
|
accent: '#2e7d32',
|
||||||
|
accentBg: '#e8f5e9',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
bg: '#1a1a1a',
|
||||||
|
text: '#e5e5e5',
|
||||||
|
textMuted: '#999999',
|
||||||
|
border: '#404040',
|
||||||
|
borderSubtle: '#333333',
|
||||||
|
borderStrong: '#555555',
|
||||||
|
hover: '#2a2a2a',
|
||||||
|
link: '#5c9eff',
|
||||||
|
error: '#ff6b6b',
|
||||||
|
errorBg: '#3d1f1f',
|
||||||
|
accent: '#81c784',
|
||||||
|
accentBg: '#1b3d1f',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Container = define('Container', {
|
||||||
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||||
|
padding: '20px',
|
||||||
|
maxWidth: '800px',
|
||||||
|
margin: '0 auto',
|
||||||
|
color: theme('text'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const Header = define('Header', {
|
||||||
|
marginBottom: '20px',
|
||||||
|
paddingBottom: '10px',
|
||||||
|
borderBottom: `2px solid ${theme('borderStrong')}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const Title = define('Title', {
|
||||||
|
margin: 0,
|
||||||
|
fontSize: '24px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
})
|
||||||
|
|
||||||
|
const Subtitle = define('Subtitle', {
|
||||||
|
color: theme('textMuted'),
|
||||||
|
fontSize: '18px',
|
||||||
|
marginTop: '5px',
|
||||||
|
})
|
||||||
|
|
||||||
|
const VersionList = define('VersionList', {
|
||||||
|
listStyle: 'none',
|
||||||
|
padding: 0,
|
||||||
|
margin: '20px 0',
|
||||||
|
border: `1px solid ${theme('border')}`,
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
})
|
||||||
|
|
||||||
|
const VersionItem = define('VersionItem', {
|
||||||
|
padding: '12px 15px',
|
||||||
|
borderBottom: `1px solid ${theme('borderSubtle')}`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
states: {
|
||||||
|
':last-child': {
|
||||||
|
borderBottom: 'none',
|
||||||
|
},
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: theme('hover'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const VersionLink = define('VersionLink', {
|
||||||
|
base: 'a',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: theme('link'),
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '15px',
|
||||||
|
states: {
|
||||||
|
':hover': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Badge = define('Badge', {
|
||||||
|
fontSize: '12px',
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: theme('accentBg'),
|
||||||
|
color: theme('accent'),
|
||||||
|
fontWeight: 'bold',
|
||||||
|
})
|
||||||
|
|
||||||
|
const ErrorBox = define('ErrorBox', {
|
||||||
|
color: theme('error'),
|
||||||
|
padding: '20px',
|
||||||
|
backgroundColor: theme('errorBg'),
|
||||||
|
borderRadius: '4px',
|
||||||
|
margin: '20px 0',
|
||||||
|
})
|
||||||
|
|
||||||
|
const initScript = `
|
||||||
|
(function() {
|
||||||
|
var theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
document.body.setAttribute('data-theme', theme);
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
||||||
|
document.body.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
||||||
|
});
|
||||||
|
function sendHeight() {
|
||||||
|
var container = document.querySelector('.Container');
|
||||||
|
if (!container) return;
|
||||||
|
var rect = container.getBoundingClientRect();
|
||||||
|
window.parent.postMessage({ type: 'resize-iframe', height: rect.bottom + 20 }, '*');
|
||||||
|
}
|
||||||
|
sendHeight();
|
||||||
|
setTimeout(sendHeight, 50);
|
||||||
|
new ResizeObserver(sendHeight).observe(document.body);
|
||||||
|
})();
|
||||||
|
`
|
||||||
|
|
||||||
|
const baseStyles = `
|
||||||
|
body {
|
||||||
|
background: ${theme('bg')};
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
children: Child
|
||||||
|
}
|
||||||
|
|
||||||
|
function Layout({ title, subtitle, children }: LayoutProps) {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{title}</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: initScript }} />
|
||||||
|
<Container>
|
||||||
|
<Header>
|
||||||
|
<Title>Versions</Title>
|
||||||
|
{subtitle && <Subtitle>{subtitle}</Subtitle>}
|
||||||
|
</Header>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, {
|
||||||
|
'Content-Type': 'text/css; charset=utf-8',
|
||||||
|
}))
|
||||||
|
|
||||||
|
async function getVersions(appPath: string): Promise<{ name: string; isCurrent: boolean }[]> {
|
||||||
|
const entries = await readdir(appPath, { withFileTypes: true })
|
||||||
|
|
||||||
|
let currentTarget = ''
|
||||||
|
try {
|
||||||
|
currentTarget = await readlink(join(appPath, 'current'))
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.filter(e => e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name))
|
||||||
|
.map(e => ({ name: e.name, isCurrent: e.name === currentTarget }))
|
||||||
|
.sort((a, b) => b.name.localeCompare(a.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(ts: string): string {
|
||||||
|
return `${ts.slice(0, 4)}-${ts.slice(4, 6)}-${ts.slice(6, 8)} ${ts.slice(9, 11)}:${ts.slice(11, 13)}:${ts.slice(13, 15)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/', async c => {
|
||||||
|
const appName = c.req.query('app')
|
||||||
|
|
||||||
|
if (!appName) {
|
||||||
|
return c.html(
|
||||||
|
<Layout title="Versions">
|
||||||
|
<ErrorBox>Please specify an app name with ?app=<name></ErrorBox>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const appPath = join(APPS_DIR, appName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stat(appPath)
|
||||||
|
} catch {
|
||||||
|
return c.html(
|
||||||
|
<Layout title="Versions">
|
||||||
|
<ErrorBox>App "{appName}" not found</ErrorBox>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = await getVersions(appPath)
|
||||||
|
|
||||||
|
if (versions.length === 0) {
|
||||||
|
return c.html(
|
||||||
|
<Layout title="Versions" subtitle={appName}>
|
||||||
|
<ErrorBox>No versions found</ErrorBox>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the code tool's port from environment or use a relative path
|
||||||
|
const codeToolUrl = process.env.CODE_TOOL_URL || '/tool/code'
|
||||||
|
|
||||||
|
return c.html(
|
||||||
|
<Layout title="Versions" subtitle={appName}>
|
||||||
|
<VersionList>
|
||||||
|
{versions.map(v => (
|
||||||
|
<VersionItem>
|
||||||
|
<VersionLink href={`${codeToolUrl}?app=${appName}&version=${v.name}`} target="_top">
|
||||||
|
{formatTimestamp(v.name)}
|
||||||
|
</VersionLink>
|
||||||
|
{v.isCurrent && <Badge>current</Badge>}
|
||||||
|
</VersionItem>
|
||||||
|
))}
|
||||||
|
</VersionList>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app.defaults
|
||||||
25
apps/versions/20260130-000000/package.json
Normal file
25
apps/versions/20260130-000000/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "versions",
|
||||||
|
"module": "index.tsx",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"toes": "bun run --watch index.tsx",
|
||||||
|
"start": "bun toes",
|
||||||
|
"dev": "bun run --hot index.tsx"
|
||||||
|
},
|
||||||
|
"toes": {
|
||||||
|
"tool": true,
|
||||||
|
"icon": "📦"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@because/hype": "*",
|
||||||
|
"@because/forge": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
30
apps/versions/20260130-000000/tsconfig.json
Normal file
30
apps/versions/20260130-000000/tsconfig.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
1
apps/versions/current
Symbolic link
1
apps/versions/current
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
20260130-000000
|
||||||
Loading…
Reference in New Issue
Block a user