This commit is contained in:
Chris Wanstrath 2026-01-30 18:21:37 -08:00
parent 2dea6d948f
commit 1c51427034
2 changed files with 99 additions and 19 deletions

View File

@ -75,6 +75,7 @@ Add `toes.tool` to your app's `package.json`:
- Receive `?app=<name>` query parameter for the currently selected app - Receive `?app=<name>` query parameter for the currently selected app
- Iframes are cached per tool+app combination (never recreated once loaded) - Iframes are cached per tool+app combination (never recreated once loaded)
- Tool state persists across tab switches - Tool state persists across tab switches
- **App paths**: When accessing app files, tools must use `APPS_DIR/<app>/current` (not just `APPS_DIR/<app>`) to resolve through the version symlink
### CLI Flags ### CLI Flags

View File

@ -1,5 +1,5 @@
import { Hype } from '@because/hype' import { Hype } from '@because/hype'
import { define, stylesToCSS } from '@because/forge' import { createThemes, define, stylesToCSS } from '@because/forge'
import { readdir, stat } from 'fs/promises' import { readdir, stat } from 'fs/promises'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { join, extname, basename } from 'path' import { join, extname, basename } from 'path'
@ -8,18 +8,53 @@ const APPS_DIR = process.env.APPS_DIR!
const app = new Hype({ prettyHTML: false }) const app = new Hype({ prettyHTML: false })
// Theme
const theme = createThemes({
light: {
bg: '#ffffff',
text: '#1a1a1a',
textMuted: '#666666',
border: '#dddddd',
borderSubtle: '#eeeeee',
borderStrong: '#333333',
hover: '#f5f5f5',
surface: '#f5f5f5',
link: '#0066cc',
icon: '#666666',
codeBg: '#fafafa',
error: '#d32f2f',
errorBg: '#ffebee',
},
dark: {
bg: '#1a1a1a',
text: '#e5e5e5',
textMuted: '#999999',
border: '#404040',
borderSubtle: '#333333',
borderStrong: '#555555',
hover: '#2a2a2a',
surface: '#252525',
link: '#5c9eff',
icon: '#888888',
codeBg: '#1e1e1e',
error: '#ff6b6b',
errorBg: '#3d1f1f',
},
})
// Styles // Styles
const Container = define('CodeBrowserContainer', { const Container = define('CodeBrowserContainer', {
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
padding: '20px', padding: '20px',
maxWidth: '1200px', maxWidth: '1200px',
margin: '0 auto', margin: '0 auto',
color: theme('text'),
}) })
const Header = define('Header', { const Header = define('Header', {
marginBottom: '20px', marginBottom: '20px',
paddingBottom: '10px', paddingBottom: '10px',
borderBottom: '2px solid #333', borderBottom: `2px solid ${theme('borderStrong')}`,
}) })
const Title = define('Title', { const Title = define('Title', {
@ -29,7 +64,7 @@ const Title = define('Title', {
}) })
const AppName = define('AppName', { const AppName = define('AppName', {
color: '#666', color: theme('textMuted'),
fontSize: '18px', fontSize: '18px',
marginTop: '5px', marginTop: '5px',
}) })
@ -38,20 +73,20 @@ const FileList = define('FileList', {
listStyle: 'none', listStyle: 'none',
padding: 0, padding: 0,
margin: '20px 0', margin: '20px 0',
border: '1px solid #ddd', border: `1px solid ${theme('border')}`,
borderRadius: '4px', borderRadius: '4px',
overflow: 'hidden', overflow: 'hidden',
}) })
const FileItem = define('FileItem', { const FileItem = define('FileItem', {
padding: '10px 15px', padding: '10px 15px',
borderBottom: '1px solid #eee', borderBottom: `1px solid ${theme('borderSubtle')}`,
states: { states: {
':last-child': { ':last-child': {
borderBottom: 'none', borderBottom: 'none',
}, },
':hover': { ':hover': {
backgroundColor: '#f5f5f5', backgroundColor: theme('hover'),
}, },
} }
}) })
@ -59,7 +94,7 @@ const FileItem = define('FileItem', {
const FileLink = define('FileLink', { const FileLink = define('FileLink', {
base: 'a', base: 'a',
textDecoration: 'none', textDecoration: 'none',
color: '#0066cc', color: theme('link'),
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '8px', gap: '8px',
@ -70,9 +105,17 @@ const FileLink = define('FileLink', {
} }
}) })
const FileIcon = define('FileIcon', {
base: 'svg',
width: '18px',
height: '18px',
flexShrink: 0,
fill: theme('icon'),
})
const CodeBlock = define('CodeBlock', { const CodeBlock = define('CodeBlock', {
margin: '20px 0', margin: '20px 0',
border: '1px solid #ddd', border: `1px solid ${theme('border')}`,
borderRadius: '4px', borderRadius: '4px',
overflow: 'auto', overflow: 'auto',
selectors: { selectors: {
@ -81,6 +124,7 @@ const CodeBlock = define('CodeBlock', {
padding: '15px', padding: '15px',
overflow: 'auto', overflow: 'auto',
whiteSpace: 'pre', whiteSpace: 'pre',
backgroundColor: theme('codeBg'),
}, },
'& pre code': { '& pre code': {
whiteSpace: 'pre', whiteSpace: 'pre',
@ -91,16 +135,16 @@ const CodeBlock = define('CodeBlock', {
const CodeHeader = define('CodeHeader', { const CodeHeader = define('CodeHeader', {
padding: '10px 15px', padding: '10px 15px',
backgroundColor: '#f5f5f5', backgroundColor: theme('surface'),
borderBottom: '1px solid #ddd', borderBottom: `1px solid ${theme('border')}`,
fontWeight: 'bold', fontWeight: 'bold',
fontSize: '14px', fontSize: '14px',
}) })
const Error = define('Error', { const Error = define('Error', {
color: '#d32f2f', color: theme('error'),
padding: '20px', padding: '20px',
backgroundColor: '#ffebee', backgroundColor: theme('errorBg'),
borderRadius: '4px', borderRadius: '4px',
margin: '20px 0', margin: '20px 0',
}) })
@ -108,7 +152,7 @@ const Error = define('Error', {
const BackLink = define('BackLink', { const BackLink = define('BackLink', {
base: 'a', base: 'a',
textDecoration: 'none', textDecoration: 'none',
color: '#0066cc', color: theme('link'),
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
gap: '5px', gap: '5px',
@ -120,7 +164,36 @@ const BackLink = define('BackLink', {
}, },
}) })
app.get('/styles.css', c => c.text(stylesToCSS(), 200, { const FolderIcon = () => (
<FileIcon viewBox="0 0 24 24">
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
</FileIcon>
)
const FileIconSvg = () => (
<FileIcon viewBox="0 0 24 24">
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h6v6h6v10H6z" />
</FileIcon>
)
const themeScript = `
(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');
});
})();
`
const baseStyles = `
body {
background: ${theme('bg')};
margin: 0;
}
`
app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, {
'Content-Type': 'text/css; charset=utf-8', 'Content-Type': 'text/css; charset=utf-8',
})) }))
@ -189,6 +262,7 @@ app.get('/', async c => {
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
</head> </head>
<body> <body>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<Container> <Container>
<Header> <Header>
<Title>Code Browser</Title> <Title>Code Browser</Title>
@ -200,7 +274,7 @@ app.get('/', async c => {
) )
} }
const appPath = join(APPS_DIR, appName) const appPath = join(APPS_DIR, appName, 'current')
try { try {
await stat(appPath) await stat(appPath)
@ -214,6 +288,7 @@ app.get('/', async c => {
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
</head> </head>
<body> <body>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<Container> <Container>
<Header> <Header>
<Title>Code Browser</Title> <Title>Code Browser</Title>
@ -240,6 +315,7 @@ app.get('/', async c => {
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
</head> </head>
<body> <body>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<Container> <Container>
<Header> <Header>
<Title>Code Browser</Title> <Title>Code Browser</Title>
@ -263,17 +339,19 @@ app.get('/', async c => {
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{`${appName}/${filePath}`}</title> <title>{`${appName}/${filePath}`}</title>
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" media="(prefers-color-scheme: light)" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="(prefers-color-scheme: dark)" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
</head> </head>
<body> <body>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<Container> <Container>
<Header> <Header>
<Title>Code Browser</Title> <Title>Code Browser</Title>
<AppName>{appName}/{filePath}</AppName> <AppName>{appName}/{filePath}</AppName>
</Header> </Header>
<BackLink href={`/?app=${appName}${parentPath ? `&file=${parentPath}` : ''}`}> <BackLink href={`/?app=${appName}${parentPath ? `&file=${parentPath}` : ''}`}>
Back Back
</BackLink> </BackLink>
<CodeBlock> <CodeBlock>
<CodeHeader>{basename(fullPath)}</CodeHeader> <CodeHeader>{basename(fullPath)}</CodeHeader>
@ -298,6 +376,7 @@ app.get('/', async c => {
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
</head> </head>
<body> <body>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<Container> <Container>
<Header> <Header>
<Title>Code Browser</Title> <Title>Code Browser</Title>
@ -305,14 +384,14 @@ app.get('/', async c => {
</Header> </Header>
{filePath && ( {filePath && (
<BackLink href={`/?app=${appName}${parentPath ? `&file=${parentPath}` : ''}`}> <BackLink href={`/?app=${appName}${parentPath ? `&file=${parentPath}` : ''}`}>
Back Back
</BackLink> </BackLink>
)} )}
<FileList> <FileList>
{files.map(file => ( {files.map(file => (
<FileItem> <FileItem>
<FileLink href={`/?app=${appName}&file=${file.path}`}> <FileLink href={`/?app=${appName}&file=${file.path}`}>
{file.isDirectory ? '📁' : '📄'} {file.isDirectory ? <FolderIcon /> : <FileIconSvg />}
<span>{file.name}</span> <span>{file.name}</span>
</FileLink> </FileLink>
</FileItem> </FileItem>