edit code

This commit is contained in:
Chris Wanstrath 2026-02-01 23:32:10 -08:00
parent f3040abc5d
commit f14a731cae

View File

@ -88,6 +88,9 @@ const CodeHeader = define('CodeHeader', {
borderBottom: `1px solid ${theme('colors-border')}`, borderBottom: `1px solid ${theme('colors-border')}`,
fontWeight: 'bold', fontWeight: 'bold',
fontSize: '14px', fontSize: '14px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}) })
const ErrorBox = define('ErrorBox', { const ErrorBox = define('ErrorBox', {
@ -190,6 +193,46 @@ const DownloadButton = define('DownloadButton', {
}, },
}) })
const EditButton = define('EditButton', {
base: 'button',
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
padding: '6px 12px',
backgroundColor: theme('colors-bgElement'),
color: theme('colors-text'),
border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-md'),
fontSize: '13px',
cursor: 'pointer',
states: {
':hover': {
backgroundColor: theme('colors-bgHover'),
},
},
})
const EditLink = define('EditLink', {
base: 'a',
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
padding: '6px 12px',
backgroundColor: theme('colors-bgElement'),
color: theme('colors-text'),
border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-md'),
fontSize: '13px',
cursor: 'pointer',
textDecoration: 'none',
states: {
':hover': {
backgroundColor: theme('colors-bgHover'),
},
},
})
const FolderIcon = () => ( const FolderIcon = () => (
<FileIcon viewBox="0 0 24 24"> <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" /> <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" />
@ -206,6 +249,7 @@ interface LayoutProps {
title: string title: string
children: Child children: Child
highlight?: boolean highlight?: boolean
editable?: boolean
} }
const fileMemoryScript = ` const fileMemoryScript = `
@ -233,7 +277,7 @@ const fileMemoryScript = `
})(); })();
` `
function Layout({ title, children, highlight }: LayoutProps) { function Layout({ title, children, highlight, editable }: LayoutProps) {
return ( return (
<html> <html>
<head> <head>
@ -241,13 +285,27 @@ function Layout({ title, children, highlight }: LayoutProps) {
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title> <title>{title}</title>
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
{highlight && ( {highlight && !editable && (
<> <>
<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.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)" /> <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>
</> </>
)} )}
{editable && (
<>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" media="(prefers-color-scheme: light)" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" media="(prefers-color-scheme: dark)" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-tsx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markdown.min.js"></script>
</>
)}
</head> </head>
<body> <body>
<script dangerouslySetInnerHTML={{ __html: fileMemoryScript }} /> <script dangerouslySetInnerHTML={{ __html: fileMemoryScript }} />
@ -255,7 +313,7 @@ function Layout({ title, children, highlight }: LayoutProps) {
<Container> <Container>
{children} {children}
</Container> </Container>
{highlight && <script dangerouslySetInnerHTML={{ __html: 'hljs.highlightAll();' }} />} {highlight && !editable && <script dangerouslySetInnerHTML={{ __html: 'hljs.highlightAll();' }} />}
</body> </body>
</html> </html>
) )
@ -284,6 +342,26 @@ app.get('/raw', async c => {
return new Response(file) return new Response(file)
}) })
app.post('/save', async c => {
const appName = c.req.query('app')
const version = c.req.query('version') || 'current'
const filePath = c.req.query('file')
if (!appName || !filePath) {
return c.text('Missing app or file parameter', 400)
}
const fullPath = join(APPS_DIR, appName, version, filePath)
const content = await c.req.text()
try {
await Bun.write(fullPath, content)
return c.text('OK')
} catch (err) {
return c.text(`Failed to save: ${err}`, 500)
}
})
async function listFiles(appPath: string, subPath: string = '') { async function listFiles(appPath: string, subPath: string = '') {
const fullPath = join(appPath, subPath) const fullPath = join(appPath, subPath)
const entries = await readdir(fullPath, { withFileTypes: true }) const entries = await readdir(fullPath, { withFileTypes: true })
@ -373,6 +451,29 @@ function getLanguage(filename: string): string {
return langMap[ext] || 'plaintext' return langMap[ext] || 'plaintext'
} }
function getPrismLanguage(filename: string): string {
const ext = extname(filename).toLowerCase()
const langMap: Record<string, string> = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.json': 'json',
'.css': 'css',
'.html': 'html',
'.md': 'markdown',
'.sh': 'bash',
'.yml': 'yaml',
'.yaml': 'yaml',
'.py': 'python',
'.rb': 'ruby',
'.go': 'go',
'.rs': 'rust',
'.sql': 'sql',
}
return langMap[ext] || 'plaintext'
}
app.get('/', async c => { app.get('/', async c => {
const appName = c.req.query('app') const appName = c.req.query('app')
@ -478,12 +579,91 @@ app.get('/', async c => {
// Text file - show with syntax highlighting // Text file - show with syntax highlighting
const content = readFileSync(fullPath, 'utf-8') const content = readFileSync(fullPath, 'utf-8')
const language = getLanguage(filename) const language = getLanguage(filename)
const prismLang = getPrismLanguage(filename)
const edit = c.req.query('edit') === '1'
if (edit) {
const editorScript = `
import { CodeJar } from 'https://cdn.jsdelivr.net/npm/codejar@4.2.0/dist/codejar.js';
const editor = document.getElementById('editor');
const saveBtn = document.getElementById('save-btn');
const status = document.getElementById('save-status');
let dirty = false;
const highlight = (el) => {
Prism.highlightElement(el);
};
const jar = CodeJar(editor, highlight, { tab: ' ', addClosing: false });
// Initial highlight
highlight(editor);
jar.onUpdate(() => {
if (!dirty) {
dirty = true;
saveBtn.textContent = 'Save *';
}
});
saveBtn.onclick = async () => {
if (!dirty) return;
saveBtn.disabled = true;
status.textContent = 'Saving...';
try {
const res = await fetch('/save?app=${appName}${versionParam}&file=${filePath}', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: jar.toString()
});
if (!res.ok) throw new Error('Save failed');
dirty = false;
saveBtn.textContent = 'Save';
saveBtn.disabled = false;
status.textContent = 'Saved!';
setTimeout(() => { status.textContent = ''; }, 2000);
} catch (err) {
saveBtn.disabled = false;
status.textContent = 'Error: ' + err.message;
}
};
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveBtn.click();
}
});
`
return c.html(
<Layout title={`${appName}/${filePath}`} editable>
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
<CodeBlock>
<CodeHeader>
<span>{filename}</span>
<div style="display:flex;align-items:center;gap:8px">
<span id="save-status" style="font-size:12px;font-weight:normal;color:var(--colors-textMuted)"></span>
<EditButton id="save-btn">Save</EditButton>
<EditLink href={`/?app=${appName}${versionParam}&file=${filePath}`}>Done</EditLink>
</div>
</CodeHeader>
<pre id="editor" class={`language-${prismLang}`} contenteditable style="margin:0;padding:15px;min-height:300px;outline:none">{content}</pre>
</CodeBlock>
<script type="module" dangerouslySetInnerHTML={{ __html: editorScript }} />
</Layout>
)
}
return c.html( return c.html(
<Layout title={`${appName}/${filePath}`} highlight> <Layout title={`${appName}/${filePath}`} highlight>
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} /> <PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
<CodeBlock> <CodeBlock>
<CodeHeader>{filename}</CodeHeader> <CodeHeader>
<span>{filename}</span>
<EditLink href={`/?app=${appName}${versionParam}&file=${filePath}&edit=1`}>Edit</EditLink>
</CodeHeader>
<pre><code class={`language-${language}`}>{content}</code></pre> <pre><code class={`language-${language}`}>{content}</code></pre>
</CodeBlock> </CodeBlock>
</Layout> </Layout>