diff --git a/apps/env/20260130-000000/index.tsx b/apps/env/20260130-000000/index.tsx
index 1d1347a..36fd28b 100644
--- a/apps/env/20260130-000000/index.tsx
+++ b/apps/env/20260130-000000/index.tsx
@@ -7,9 +7,38 @@ import type { Child } from 'hono/jsx'
const TOES_DIR = process.env.TOES_DIR ?? join(process.env.HOME!, '.toes')
const ENV_DIR = join(TOES_DIR, 'env')
+const GLOBAL_ENV_PATH = join(ENV_DIR, '_global.env')
const app = new Hype({ prettyHTML: false })
+const Badge = define('Badge', {
+ base: 'span',
+ fontSize: '11px',
+ padding: '2px 6px',
+ borderRadius: '3px',
+ backgroundColor: theme('colors-bgSubtle'),
+ color: theme('colors-textMuted'),
+ fontFamily: theme('fonts-sans'),
+ fontWeight: 'normal',
+ marginLeft: '8px',
+})
+
+const Button = define('Button', {
+ base: 'button',
+ padding: '6px 12px',
+ fontSize: '13px',
+ borderRadius: theme('radius-md'),
+ border: `1px solid ${theme('colors-border')}`,
+ backgroundColor: theme('colors-bgElement'),
+ color: theme('colors-text'),
+ cursor: 'pointer',
+ states: {
+ ':hover': {
+ backgroundColor: theme('colors-bgHover'),
+ },
+ },
+})
+
const Container = define('Container', {
fontFamily: theme('fonts-sans'),
padding: '20px',
@@ -19,13 +48,34 @@ const Container = define('Container', {
color: theme('colors-text'),
})
-const EnvList = define('EnvList', {
- listStyle: 'none',
- padding: 0,
- margin: 0,
- border: `1px solid ${theme('colors-border')}`,
+const DangerButton = define('DangerButton', {
+ base: 'button',
+ padding: '6px 12px',
+ fontSize: '13px',
borderRadius: theme('radius-md'),
- overflow: 'hidden',
+ border: 'none',
+ backgroundColor: theme('colors-error'),
+ color: 'white',
+ cursor: 'pointer',
+ states: {
+ ':hover': {
+ opacity: 0.9,
+ },
+ },
+})
+
+const EmptyState = define('EmptyState', {
+ padding: '30px',
+ textAlign: 'center',
+ color: theme('colors-textMuted'),
+ backgroundColor: theme('colors-bgSubtle'),
+ borderRadius: theme('radius-md'),
+})
+
+const EnvActions = define('EnvActions', {
+ display: 'flex',
+ gap: '8px',
+ flexShrink: 0,
})
const EnvItem = define('EnvItem', {
@@ -49,6 +99,15 @@ const EnvKey = define('EnvKey', {
color: theme('colors-text'),
})
+const EnvList = define('EnvList', {
+ listStyle: 'none',
+ padding: 0,
+ margin: 0,
+ border: `1px solid ${theme('colors-border')}`,
+ borderRadius: theme('radius-md'),
+ overflow: 'hidden',
+})
+
const EnvValue = define('EnvValue', {
fontFamily: theme('fonts-mono'),
fontSize: '14px',
@@ -59,42 +118,12 @@ const EnvValue = define('EnvValue', {
whiteSpace: 'nowrap',
})
-const EnvActions = define('EnvActions', {
- display: 'flex',
- gap: '8px',
- flexShrink: 0,
-})
-
-const Button = define('Button', {
- base: 'button',
- padding: '6px 12px',
- fontSize: '13px',
- borderRadius: theme('radius-md'),
- border: `1px solid ${theme('colors-border')}`,
+const ErrorBox = define('ErrorBox', {
+ color: theme('colors-error'),
+ padding: '20px',
backgroundColor: theme('colors-bgElement'),
- color: theme('colors-text'),
- cursor: 'pointer',
- states: {
- ':hover': {
- backgroundColor: theme('colors-bgHover'),
- },
- },
-})
-
-const DangerButton = define('DangerButton', {
- base: 'button',
- padding: '6px 12px',
- fontSize: '13px',
borderRadius: theme('radius-md'),
- border: 'none',
- backgroundColor: theme('colors-error'),
- color: 'white',
- cursor: 'pointer',
- states: {
- ':hover': {
- opacity: 0.9,
- },
- },
+ margin: '20px 0',
})
const Form = define('Form', {
@@ -107,6 +136,12 @@ const Form = define('Form', {
borderRadius: theme('radius-md'),
})
+const Hint = define('Hint', {
+ fontSize: '12px',
+ color: theme('colors-textMuted'),
+ marginTop: '10px',
+})
+
const Input = define('Input', {
base: 'input',
flex: 1,
@@ -125,27 +160,54 @@ const Input = define('Input', {
},
})
-const ErrorBox = define('ErrorBox', {
- color: theme('colors-error'),
- padding: '20px',
- backgroundColor: theme('colors-bgElement'),
- borderRadius: theme('radius-md'),
- margin: '20px 0',
+const Tab = define('Tab', {
+ base: 'a',
+ padding: '8px 16px',
+ fontSize: '13px',
+ fontFamily: theme('fonts-sans'),
+ color: theme('colors-textMuted'),
+ textDecoration: 'none',
+ borderBottom: '2px solid transparent',
+ cursor: 'pointer',
+ states: {
+ ':hover': {
+ color: theme('colors-text'),
+ },
+ },
})
-const EmptyState = define('EmptyState', {
- padding: '30px',
- textAlign: 'center',
- color: theme('colors-textMuted'),
- backgroundColor: theme('colors-bgSubtle'),
- borderRadius: theme('radius-md'),
+const TabActive = define('TabActive', {
+ base: 'a',
+ padding: '8px 16px',
+ fontSize: '13px',
+ fontFamily: theme('fonts-sans'),
+ color: theme('colors-text'),
+ textDecoration: 'none',
+ borderBottom: `2px solid ${theme('colors-primary')}`,
+ fontWeight: 'bold',
+ cursor: 'default',
})
+const TabBar = define('TabBar', {
+ display: 'flex',
+ gap: '4px',
+ borderBottom: `1px solid ${theme('colors-border')}`,
+ marginBottom: '15px',
+})
+
+interface EnvVar {
+ key: string
+ value: string
+}
+
interface LayoutProps {
title: string
children: Child
}
+const appEnvPath = (appName: string) =>
+ join(ENV_DIR, `${appName}.env`)
+
function Layout({ title, children }: LayoutProps) {
return (
@@ -166,29 +228,6 @@ function Layout({ title, children }: LayoutProps) {
)
}
-const clientScript = `
-document.querySelectorAll('[data-reveal]').forEach(btn => {
- btn.addEventListener('click', () => {
- const valueEl = btn.closest('[data-env-item]').querySelector('[data-value]');
- const hidden = valueEl.dataset.hidden;
- if (hidden) {
- valueEl.textContent = hidden;
- valueEl.dataset.hidden = '';
- btn.textContent = 'Hide';
- } else {
- valueEl.dataset.hidden = valueEl.textContent;
- valueEl.textContent = '••••••••';
- btn.textContent = 'Reveal';
- }
- });
-});
-`
-
-interface EnvVar {
- key: string
- value: string
-}
-
function ensureEnvDir() {
if (!existsSync(ENV_DIR)) {
mkdirSync(ENV_DIR, { recursive: true })
@@ -229,9 +268,23 @@ function writeEnvFile(path: string, vars: EnvVar[]) {
writeFileSync(path, content)
}
-function appEnvPath(appName: string): string {
- return join(ENV_DIR, `${appName}.env`)
-}
+const clientScript = `
+document.querySelectorAll('[data-reveal]').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const valueEl = btn.closest('[data-env-item]').querySelector('[data-value]');
+ const hidden = valueEl.dataset.hidden;
+ if (hidden) {
+ valueEl.textContent = hidden;
+ valueEl.dataset.hidden = '';
+ btn.textContent = 'Hide';
+ } else {
+ valueEl.dataset.hidden = valueEl.textContent;
+ valueEl.textContent = '••••••••';
+ btn.textContent = 'Reveal';
+ }
+ });
+});
+`
app.get('/ok', c => c.text('ok'))
@@ -250,17 +303,67 @@ app.get('/', async c => {
)
}
+ const tab = c.req.query('tab') === 'global' ? 'global' : 'app'
+ const appUrl = `/?app=${appName}`
+ const globalUrl = `/?app=${appName}&tab=global`
+
+ if (tab === 'global') {
+ const globalVars = parseEnvFile(GLOBAL_ENV_PATH)
+
+ return c.html(
+
+
+ App
+ Global
+
+ {globalVars.length === 0 ? (
+ No global environment variables
+ ) : (
+
+ {globalVars.map(v => (
+
+ {v.key}
+ {'••••••••'}
+
+
+
+
+
+ ))}
+
+ )}
+
+ Global vars are available to all apps. Changes take effect on next app restart.
+
+ )
+ }
+
const appVars = parseEnvFile(appEnvPath(appName))
+ const globalVars = parseEnvFile(GLOBAL_ENV_PATH)
+ const globalKeys = new Set(globalVars.map(v => v.key))
return c.html(
- {appVars.length === 0 ? (
+
+ App
+ Global
+
+ {appVars.length === 0 && globalKeys.size === 0 ? (
No environment variables
) : (
{appVars.map(v => (
- {v.key}
+
+ {v.key}
+ {globalKeys.has(v.key) && overrides global}
+
{'••••••••'}
@@ -270,6 +373,18 @@ app.get('/', async c => {
))}
+ {globalVars.filter(v => !appVars.some(a => a.key === v.key)).map(v => (
+
+
+ {v.key}
+ global
+
+ {'••••••••'}
+
+
+
+
+ ))}
)}