code
This commit is contained in:
parent
2dea6d948f
commit
1c51427034
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user