NotesWhat is notes.io?

Notes brand slogan

Notes - notes.io

import { useState, useEffect, useRef, useCallback } from 'react'

// ── Check if running inside Electron ─────────────────────────────────────────
const IS_ELECTRON = typeof window !== 'undefined' && !!window.domvia
const api = IS_ELECTRON ? window.domvia : null

// ── Design Tokens ─────────────────────────────────────────────────────────────
const C = {
bg: '#0C0C0F',
surface: '#111115',
card: '#17171D',
cardHover: '#1D1D25',
border: '#22222E',
borderBright: '#35354A',
accent: '#4F8EF7',
accentHover: '#6BA3FA',
accentGlow: 'rgba(79,142,247,0.15)',
accentSoft: 'rgba(79,142,247,0.08)',
green: '#3DD68C',
greenSoft: 'rgba(61,214,140,0.1)',
orange: '#F97316',
orangeSoft: 'rgba(249,115,22,0.1)',
pink: '#E879F9',
pinkSoft: 'rgba(232,121,249,0.1)',
yellow: '#FBBF24',
red: '#F87171',
redSoft: 'rgba(248,113,113,0.1)',
text: '#E8E8F0',
textSoft: '#A0A0BC',
textMuted: '#52526E',
textDim: '#6E6E8A',
}

// ── Global CSS ─────────────────────────────────────────────────────────────────
const STYLES = `
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body, #root { height: 100%; overflow: hidden; }
body {
background: ${C.bg}; color: ${C.text};
font-family: 'Geist', -apple-system, sans-serif;
font-size: 13px; line-height: 1.5;
-webkit-font-smoothing: antialiased;
user-select: none;
}
.selectable { user-select: text; }
.mono { font-family: 'Geist Mono', 'Fira Code', monospace; }

::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: ${C.border}; border-radius: 99px; }

/* Animations */
@keyframes fadeUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
@keyframes scaleIn { from { opacity:0; transform:scale(0.96); } to { opacity:1; transform:scale(1); } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0; } }
@keyframes slideIn { from { transform:translateX(-10px); opacity:0; } to { transform:translateX(0); opacity:1; } }
@keyframes glow { 0%,100% { box-shadow:0 0 12px rgba(79,142,247,0.3); } 50% { box-shadow:0 0 28px rgba(79,142,247,0.6); } }
@keyframes shimmer { 0% { background-position:-200% 0; } 100% { background-position:200% 0; } }

.fadeUp { animation: fadeUp 0.35s cubic-bezier(0.16,1,0.3,1) both; }
.fadeIn { animation: fadeIn 0.25s ease both; }
.scaleIn { animation: scaleIn 0.25s cubic-bezier(0.16,1,0.3,1) both; }
.spin { animation: spin 0.8s linear infinite; }
.pulse-dot { animation: pulse 1.8s ease-in-out infinite; }
.blink { animation: blink 1s step-end infinite; }
.slideIn { animation: slideIn 0.3s cubic-bezier(0.16,1,0.3,1) both; }

.d1 { animation-delay:0.04s; } .d2 { animation-delay:0.08s; }
.d3 { animation-delay:0.12s; } .d4 { animation-delay:0.16s; }
.d5 { animation-delay:0.20s; } .d6 { animation-delay:0.24s; }

/* Buttons */
.btn {
display:inline-flex; align-items:center; gap:6px; padding:7px 14px;
border-radius:8px; border:none; cursor:pointer;
font-family:'Geist',sans-serif; font-size:12px; font-weight:500;
transition:all 0.15s; white-space:nowrap; outline:none;
}
.btn:active { transform:scale(0.97); }
.btn-primary { background:${C.accent}; color:#fff; }
.btn-primary:hover { background:${C.accentHover}; box-shadow:0 4px 16px ${C.accentGlow}; }
.btn-ghost { background:transparent; color:${C.textSoft}; border:1px solid ${C.border}; }
.btn-ghost:hover { border-color:${C.borderBright}; color:${C.text}; background:${C.card}; }
.btn-green { background:${C.green}; color:#000; font-weight:600; }
.btn-green:hover { filter:brightness(1.1); box-shadow:0 4px 14px ${C.greenSoft}; }
.btn-danger { background:${C.redSoft}; color:${C.red}; border:1px solid rgba(248,113,113,0.2); }
.btn-danger:hover { background:rgba(248,113,113,0.2); }
.btn-orange { background:${C.orange}; color:#fff; }
.btn-orange:hover { filter:brightness(1.1); }
.btn-sm { padding:5px 11px; font-size:11px; border-radius:7px; }
.btn-xs { padding:3px 8px; font-size:11px; border-radius:6px; }
.btn-icon { padding:6px; border-radius:7px; background:${C.card}; border:1px solid ${C.border}; color:${C.textMuted}; }
.btn-icon:hover { border-color:${C.accent}; color:${C.accent}; background:${C.accentSoft}; }
.btn-icon-sm { padding:4px; border-radius:6px; }

/* Inputs */
input, textarea, select {
width:100%; padding:8px 11px;
background:${C.surface}; border:1px solid ${C.border};
border-radius:8px; color:${C.text};
font-family:'Geist',sans-serif; font-size:12px;
outline:none; transition:all 0.15s;
}
input:focus, textarea:focus, select:focus {
border-color:${C.accent}; box-shadow:0 0 0 3px ${C.accentGlow};
background:${C.card};
}
input::placeholder, textarea::placeholder { color:${C.textMuted}; }
label { display:block; font-size:11px; color:${C.textMuted}; margin-bottom:4px; font-weight:500; }
select option { background:${C.surface}; color:${C.text}; }
textarea { resize:vertical; min-height:80px; }

/* Layout */
.card { background:${C.card}; border:1px solid ${C.border}; border-radius:10px; }
.card-hover:hover { border-color:${C.borderBright}; background:${C.cardHover}; }
.divider { height:1px; background:${C.border}; }

/* Badges */
.badge { display:inline-flex; align-items:center; gap:3px; padding:2px 7px; border-radius:99px; font-size:10px; font-weight:600; }
.badge-blue { background:${C.accentSoft}; color:${C.accent}; border:1px solid rgba(79,142,247,0.2); }
.badge-green { background:${C.greenSoft}; color:${C.green}; border:1px solid rgba(61,214,140,0.2); }
.badge-orange { background:${C.orangeSoft}; color:${C.orange}; border:1px solid rgba(249,115,22,0.2); }
.badge-pink { background:${C.pinkSoft}; color:${C.pink}; border:1px solid rgba(232,121,249,0.2); }
.badge-red { background:${C.redSoft}; color:${C.red}; border:1px solid rgba(248,113,113,0.2); }
.badge-dim { background:rgba(255,255,255,0.04); color:${C.textMuted}; border:1px solid ${C.border}; }

/* Nav */
.nav-item {
display:flex; align-items:center; gap:8px; padding:7px 10px;
border-radius:7px; cursor:pointer; font-size:12px; font-weight:500;
color:${C.textMuted}; transition:all 0.12s;
}
.nav-item:hover { background:${C.accentSoft}; color:${C.textSoft}; }
.nav-item.active { background:${C.accentSoft}; color:${C.accent}; border:1px solid rgba(79,142,247,0.15); }

/* Tabs */
.tabs { display:flex; background:${C.surface}; border-radius:8px; padding:2px; gap:1px; }
.tab { flex:1; padding:5px 12px; border-radius:6px; border:none; cursor:pointer; font-family:'Geist',sans-serif; font-size:12px; font-weight:500; color:${C.textMuted}; background:transparent; transition:all 0.12s; }
.tab.active { background:${C.card}; color:${C.text}; box-shadow:0 1px 6px rgba(0,0,0,0.3); }

/* Modal */
.modal-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.75); backdrop-filter:blur(8px); z-index:9000; display:flex; align-items:center; justify-content:center; padding:20px; }
.modal-box { background:${C.card}; border:1px solid ${C.border}; border-radius:14px; padding:22px; width:100%; max-width:520px; max-height:88vh; overflow-y:auto; animation:scaleIn 0.2s cubic-bezier(0.16,1,0.3,1); }

/* Context menu */
.ctx-menu { position:fixed; background:${C.card}; border:1px solid ${C.border}; border-radius:9px; padding:4px; z-index:9999; min-width:160px; box-shadow:0 12px 40px rgba(0,0,0,0.6); animation:scaleIn 0.15s ease; }
.ctx-item { padding:7px 12px; border-radius:6px; cursor:pointer; font-size:12px; color:${C.textSoft}; display:flex; align-items:center; gap:8px; transition:background 0.1s; }
.ctx-item:hover { background:${C.accentSoft}; color:${C.text}; }
.ctx-item.danger { color:${C.red}; }
.ctx-item.danger:hover { background:${C.redSoft}; }

/* Toast */
.toast-stack { position:fixed; bottom:20px; right:20px; display:flex; flex-direction:column; gap:7px; z-index:9999; }
.toast { background:${C.card}; border:1px solid ${C.border}; border-radius:10px; padding:11px 16px; display:flex; align-items:center; gap:9px; font-size:12px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.5); animation:slideIn 0.25s cubic-bezier(0.16,1,0.3,1); min-width:240px; border-left:2.5px solid var(--tc,${C.accent}); }

/* Scrollable */
.scroll { overflow:auto; }
.scroll-y { overflow-y:auto; overflow-x:hidden; }

/* Code editor */
.code-area { background:#080810; border:1px solid ${C.border}; border-radius:10px; font-family:'Geist Mono',monospace; font-size:12px; line-height:1.7; color:#C8D8FF; overflow:auto; }
.code-line { display:flex; }
.code-ln { min-width:42px; padding:0 10px; color:${C.textMuted}; text-align:right; border-right:1px solid ${C.border}; user-select:none; font-size:11px; flex-shrink:0; }
.code-content { padding:0 14px; flex:1; white-space:pre; }
.kw { color:#89AAFF; } .str { color:#A8E6A3; } .cmt { color:${C.textMuted}; font-style:italic; }
.fn { color:#FFD580; } .num { color:#F9A97C; } .op { color:#FF7CA3; }

/* File tree */
.file-item { display:flex; align-items:center; gap:6px; padding:4px 8px; border-radius:6px; cursor:pointer; font-size:11px; color:${C.textSoft}; transition:background 0.1s; white-space:nowrap; }
.file-item:hover { background:${C.accentSoft}; color:${C.text}; }
.file-item.active { background:${C.accentSoft}; color:${C.accent}; }

/* Canvas */
.canvas-bg { background: repeating-conic-gradient(${C.surface} 0% 25%, ${C.bg} 0% 50%) 0 0 / 20px 20px; }
.canvas-el { position:absolute; cursor:pointer; border:1.5px solid ${C.border}; border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:11px; color:${C.textMuted}; transition:border-color 0.1s; background:${C.card}; }
.canvas-el:hover, .canvas-el.sel { border-color:${C.accent}; color:${C.text}; box-shadow:0 0 0 2px ${C.accentGlow}; }

/* Switch */
.sw { width:34px; height:19px; background:${C.border}; border-radius:99px; cursor:pointer; position:relative; transition:background 0.2s; flex-shrink:0; }
.sw.on { background:${C.accent}; }
.sw::after { content:''; position:absolute; top:2px; left:2px; width:15px; height:15px; background:#fff; border-radius:50%; transition:transform 0.2s; box-shadow:0 1px 3px rgba(0,0,0,0.3); }
.sw.on::after { transform:translateX(15px); }

/* Table */
.tbl { width:100%; border-collapse:collapse; }
.tbl th { padding:8px 12px; text-align:left; font-size:10px; font-weight:600; color:${C.textMuted}; text-transform:uppercase; letter-spacing:0.07em; border-bottom:1px solid ${C.border}; background:${C.surface}; position:sticky; top:0; }
.tbl td { padding:9px 12px; font-size:12px; border-bottom:1px solid ${C.border}40; vertical-align:middle; }
.tbl tr:last-child td { border-bottom:none; }
.tbl tr:hover td { background:${C.accentSoft}; }

/* Sidebar resize handle */
.resize-handle { width:3px; cursor:col-resize; background:transparent; transition:background 0.2s; flex-shrink:0; }
.resize-handle:hover { background:${C.accent}; }

/* Title bar drag area (macOS) */
.titlebar-drag { -webkit-app-region: drag; }
.titlebar-no-drag { -webkit-app-region: no-drag; }

/* AI streaming cursor */
.ai-cur::after { content:'▌'; animation:blink 1s step-end infinite; color:${C.accent}; margin-left:1px; }

/* Syntax highlight for generated code */
.hl-keyword { color:#79B8FF; }
.hl-string { color:#9ECE6A; }
.hl-comment { color:#565F89; font-style:italic; }
.hl-fn { color:#E0AF68; }
.hl-type { color:#BB9AF7; }
`

// ── Icons (micro SVG) ─────────────────────────────────────────────────────────
const PATHS = {
apps: ["M3 3h7v7H3z","M14 3h7v7h-7z","M3 14h7v7H3z","M14 14h7v7h-7z"],
ai: ["M9.5 2A2.5 2.5 0 0112 4.5v15a2.5 2.5 0 01-5 0V4.5A2.5 2.5 0 019.5 2z","M14.5 8A2.5 2.5 0 0117 10.5v9a2.5 2.5 0 01-5 0v-9A2.5 2.5 0 0114.5 8z","M4.5 13A2.5 2.5 0 017 15.5v4a2.5 2.5 0 01-5 0v-4A2.5 2.5 0 014.5 13z"],
editor: ["M12 20h9","M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"],
visual: ["M3 3h18v18H3z","M3 9h18","M9 9v12"],
db: ["M12 2C6.48 2 2 4.02 2 6.5v11C2 19.98 6.48 22 12 22s10-2.02 10-4.5v-11C22 4.02 17.52 2 12 2z","M2 6.5C2 8.98 6.48 11 12 11s10-2.02 10-4.5","M2 12c0 2.48 4.48 4.5 10 4.5s10-2.02 10-4.5"],
files: ["M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z","M14 2v6h6","M16 13H8","M16 17H8","M10 9H8"],
terminal: ["M4 17l6-6-6-6","M12 19h8"],
deploy: ["M12 2l10 6v8l-10 6L2 16V8l10-6z","M12 22V12","M22 8l-10 4L2 8"],
settings: ["M12 15a3 3 0 100-6 3 3 0 000 6z","M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"],
plus: ["M12 5v14","M5 12h14"],
trash: ["M3 6h18","M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"],
edit: ["M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7","M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"],
copy: ["M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2","M16 8h2a2 2 0 012 2v8a2 2 0 01-2 2h-8a2 2 0 01-2-2v-2"],
check: "M20 6L9 17l-5-5",
x: "M18 6L6 18M6 6l12 12",
search: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0",
folder: ["M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"],
file: ["M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z","M14 2v6h6"],
send: "M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z",
zap: "M13 2L3 14h9l-1 8 10-12h-9l1-8z",
refresh: ["M23 4v6h-6","M1 20v-6h6","M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"],
eye: ["M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z","M12 9a3 3 0 100 6 3 3 0 000-6z"],
code: ["M16 18l6-6-6-6","M8 6l-6 6 6 6"],
chevron: "M6 9l6 6 6-6",
chevronR: "M9 18l6-6-6-6",
cpu: ["M18 4H6a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2z","M9 9h6v6H9z","M9 1v3","M15 1v3","M9 20v3","M15 20v3","M1 9h3","M1 15h3","M20 9h3","M20 15h3"],
spark: ["M12 3l1.5 4.5H18l-3.75 2.75L15.75 15 12 12.25 8.25 15l1.5-4.75L6 7.5h4.5z"],
run: ["M5 3l14 9-14 9V3z"],
stop: ["M6 6h12v12H6z"],
save: ["M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z","M17 21v-8H7v8","M7 3v5h8"],
download: ["M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4","M7 10l5 5 5-5","M12 15V3"],
upload: ["M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4","M17 8l-5-5-5 5","M12 3v12"],
link: ["M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71","M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"],
warn: ["M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z","M12 9v4","M12 17h.01"],
info: ["M12 22a10 10 0 110-20 10 10 0 010 20z","M12 8v4","M12 16h.01"],
pkg: ["M12 2l10 6.5v7L12 22 2 15.5v-7L12 2z","M12 22V9","M22 8.5l-10 6.5L2 8.5"],
star: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z",
}

function Icon({ name, size=14, color, sw=1.6 }) {
const d = PATHS[name] || 'M12 12h.01'
const paths = Array.isArray(d) ? d : [d]
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
stroke={color || C.textMuted} strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round"
style={{ flexShrink: 0 }}>
{paths.map((p, i) => <path key={i} d={p} />)}
</svg>
)
}

// ── Toast hook ────────────────────────────────────────────────────────────────
function useToast() {
const [toasts, setToasts] = useState([])
const show = useCallback((msg, type='info', dur=3000) => {
const id = Date.now() + Math.random()
const colors = { info:C.accent, success:C.green, warn:C.orange, error:C.red }
const icons = { info:'ℹ', success:'✓', warn:'⚠', error:'✕' }
setToasts(p => [...p, { id, msg, color: colors[type]||C.accent, icon: icons[type]||'ℹ' }])
setTimeout(() => setToasts(p => p.filter(t => t.id !== id)), dur)
}, [])
return { toasts, show }
}

function ToastStack({ toasts }) {
return (
<div className="toast-stack">
{toasts.map(t => (
<div key={t.id} className="toast" style={{ '--tc': t.color }}>
<span style={{ color: t.color, fontSize: 14 }}>{t.icon}</span>
<span className="selectable">{t.msg}</span>
</div>
))}
</div>
)
}

// ── Modal ─────────────────────────────────────────────────────────────────────
function Modal({ title, sub, onClose, children, wide }) {
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal-box" style={{ maxWidth: wide ? 720 : 520 }}>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:18 }}>
<div>
<div style={{ fontSize:16, fontWeight:700, letterSpacing:-0.3 }}>{title}</div>
{sub && <div style={{ fontSize:11, color:C.textMuted, marginTop:2 }}>{sub}</div>}
</div>
<button className="btn btn-icon btn-icon-sm btn" onClick={onClose}>
<Icon name="x" size={13} />
</button>
</div>
{children}
</div>
</div>
)
}

// ── Helpers ───────────────────────────────────────────────────────────────────
const uid = () => Math.random().toString(36).slice(2, 9)
const sleep = ms => new Promise(r => setTimeout(r, ms))
const ext = f => f.split('.').pop().toLowerCase()
const fileIcon = f => {
const e = ext(f)
if (['js','jsx','ts','tsx'].includes(e)) return '⚡'
if (['css','scss'].includes(e)) return '🎨'
if (['json'].includes(e)) return '📋'
if (['md'].includes(e)) return '📄'
if (['png','jpg','svg','ico'].includes(e)) return '🖼️'
if (['html'].includes(e)) return '🌐'
return '📝'
}

// ── Syntax highlighter (simple) ───────────────────────────────────────────────
function highlight(code) {
if (!code) return ''
return code
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/(//.*)/g,'<span class="hl-comment">$1</span>')
.replace(/b(import|export|from|const|let|var|function|return|if|else|for|while|class|new|async|await|default)b/g,'<span class="hl-keyword">$1</span>')
.replace(/(".*?"|'.*?'|`.*?`)/g,'<span class="hl-string">$1</span>')
.replace(/b([A-Z][A-Za-z]+)b/g,'<span class="hl-type">$1</span>')
}

// ── INITIAL PROJECT DATA ──────────────────────────────────────────────────────
const STARTER_FILES = {
'src/App.jsx': `import { useState } from 'react'nnexport default function App() {n const [count, setCount] = useState(0)n return (n <div className="app">n <h1>Hello Domvia 👋</h1>n <button onClick={() => setCount(c => c + 1)}>n Count: {count}n </button>n </div>n )n}`,
'src/main.jsx': `import { createRoot } from 'react-dom/client'nimport App from './App'ncreateRoot(document.getElementById('root')).render(<App />)`,
'src/index.css': `body { font-family: sans-serif; margin: 0; padding: 20px; background: #111; color: #fff; }n.app { max-width: 600px; margin: 0 auto; padding: 40px; }nbutton { padding: 10px 20px; background: #4F8EF7; color: white; border: none; border-radius: 8px; cursor: pointer; }`,
'index.html': `<!DOCTYPE html>n<html>n <head><title>My App</title></head>n <body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body>n</html>`,
'package.json': `{n "name": "my-app",n "version": "1.0.0",n "dependencies": {n "react": "^18.3.0",n "react-dom": "^18.3.0"n }n}`,
}

// ── VISUAL BUILDER COMPONENTS ─────────────────────────────────────────────────
const COMP_PALETTE = [
{ type:'Button', icon:'⬛', w:120, h:38, props:{ label:'Button', variant:'primary' } },
{ type:'Input', icon:'▭', w:200, h:38, props:{ placeholder:'Enter text…' } },
{ type:'Text', icon:'T', w:200, h:32, props:{ content:'Hello World', size:'lg' } },
{ type:'Card', icon:'▢', w:240, h:120, props:{ title:'Card Title' } },
{ type:'Image', icon:'🖼', w:200, h:150, props:{ src:'', alt:'Image' } },
{ type:'Table', icon:'⊞', w:320, h:160, props:{ columns:3, rows:4 } },
{ type:'Form', icon:'📋', w:280, h:200, props:{ fields:['Name','Email'] } },
{ type:'Chart', icon:'📊', w:300, h:180, props:{ type:'bar' } },
{ type:'Badge', icon:'◉', w:80, h:28, props:{ label:'Status', color:'green' } },
{ type:'Divider', icon:'─', w:300, h:20, props:{} },
{ type:'Nav', icon:'☰', w:300, h:44, props:{ brand:'My App' } },
{ type:'Modal', icon:'⬜', w:280, h:180, props:{ title:'Modal' } },
]

// ── MAIN APP ──────────────────────────────────────────────────────────────────
export default function App() {
const [page, setPage] = useState('apps')
const [projects, setProjects] = useState([
{ id:'p1', name:'My CRM', desc:'Customer management system', icon:'📊', color:C.accent, status:'active', files:{...STARTER_FILES}, created:'May 2026' },
{ id:'p2', name:'HR Portal', desc:'Employee onboarding tool', icon:'👥', color:C.green, status:'active', files:{...STARTER_FILES}, created:'May 2026' },
])
const [activeProject, setActiveProject] = useState(null)
const [ollamaStatus, setOllamaStatus] = useState('checking') // checking|ok|error
const [ollamaModels, setOllamaModels] = useState([])
const [activeModel, setActiveModel] = useState('llama3')
const [modal, setModal] = useState(null)
const { toasts, show: toast } = useToast()

// Check Ollama on mount
useEffect(() => {
checkOllama()
loadProjects()
}, [])

const checkOllama = async () => {
setOllamaStatus('checking')
if (!api) { setOllamaStatus('no-electron'); return }
const res = await api.ollama.list()
if (res.ok) {
setOllamaStatus('ok')
setOllamaModels(res.models)
if (res.models.length > 0) setActiveModel(res.models[0].name)
} else {
setOllamaStatus('error')
}
}

const loadProjects = async () => {
if (!api) return
const res = await api.projects.load()
if (res.ok && res.data.length > 0) setProjects(res.data)
}

const saveProjects = async (newProjects) => {
if (!api) return
await api.projects.save(newProjects)
}

const createProject = (name, icon, color) => {
const p = { id: uid(), name, desc:'New project', icon, color, status:'active', files:{...STARTER_FILES}, created:'Now' }
const next = [p, ...projects]
setProjects(next)
saveProjects(next)
toast(`"${name}" created!`, 'success')
setModal(null)
}

const deleteProject = (id) => {
const next = projects.filter(p => p.id !== id)
setProjects(next)
saveProjects(next)
toast('Project deleted', 'warn')
}

const NAV = [
{ id:'apps', icon:'apps', label:'Projects' },
{ id:'ai', icon:'ai', label:'AI Builder' },
{ id:'editor', icon:'editor', label:'Code Editor' },
{ id:'visual', icon:'visual', label:'Visual Builder' },
{ id:'db', icon:'db', label:'Database' },
{ id:'files', icon:'files', label:'File Manager' },
{ id:'terminal',icon:'terminal',label:'Terminal' },
{ id:'deploy', icon:'deploy', label:'Deploy' },
{ id:'settings',icon:'settings',label:'Settings' },
]

return (
<div style={{ display:'flex', height:'100vh', overflow:'hidden', background:C.bg }}>
<style>{STYLES}</style>

{/* Sidebar */}
<aside style={{ width:200, background:C.surface, borderRight:`1px solid ${C.border}`, display:'flex', flexDirection:'column', padding:'12px 8px', flexShrink:0 }}>
{/* Logo + Ollama status */}
<div style={{ padding:'6px 8px 16px', display:'flex', alignItems:'center', gap:8 }}>
<div style={{ width:28, height:28, borderRadius:8, background:`linear-gradient(135deg,${C.accent},${C.pink})`, display:'flex', alignItems:'center', justifyContent:'center', fontSize:14, flexShrink:0 }}>◈</div>
<div>
<div style={{ fontSize:13, fontWeight:700, letterSpacing:-0.3 }}>Domvia</div>
<div style={{ fontSize:9, color: ollamaStatus==='ok' ? C.green : ollamaStatus==='checking' ? C.yellow : C.red }}>
{ollamaStatus==='ok' ? `● AI: ${activeModel}` : ollamaStatus==='checking' ? '● checking…' : '● Ollama offline'}
</div>
</div>
</div>

{NAV.map(n => (
<div key={n.id} className={`nav-item ${page===n.id?'active':''}`} onClick={() => setPage(n.id)}>
<Icon name={n.icon} size={14} color={page===n.id ? C.accent : C.textMuted} />
<span>{n.label}</span>
</div>
))}

<div style={{ marginTop:'auto' }}>
<div className="divider" style={{ margin:'8px 0' }} />
{/* Active project pill */}
{activeProject && (
<div style={{ padding:'7px 10px', borderRadius:8, background:C.accentSoft, border:`1px solid rgba(79,142,247,0.15)`, fontSize:11, color:C.accent, marginBottom:6, overflow:'hidden' }}>
<div style={{ fontWeight:600, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>
{activeProject.icon} {activeProject.name}
</div>
<div style={{ color:C.textMuted, fontSize:10 }}>active project</div>
</div>
)}
<div style={{ fontSize:10, color:C.textMuted, padding:'4px 10px' }}>
{projects.length} project{projects.length!==1?'s':''}
</div>
</div>
</aside>

{/* Main area */}
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0 }}>
{page==='apps' && <ProjectsPage projects={projects} setActiveProject={setActiveProject} activeProject={activeProject} onCreate={() => setModal('newProject')} onDelete={deleteProject} toast={toast} />}
{page==='ai' && <AIPage project={activeProject} model={activeModel} ollamaStatus={ollamaStatus} toast={toast} />}
{page==='editor' && <CodeEditorPage project={activeProject} setProjects={setProjects} projects={projects} toast={toast} />}
{page==='visual' && <VisualBuilderPage project={activeProject} toast={toast} />}
{page==='db' && <DatabasePage toast={toast} />}
{page==='files' && <FilesPage project={activeProject} toast={toast} />}
{page==='terminal' && <TerminalPage toast={toast} />}
{page==='deploy' && <DeployPage project={activeProject} toast={toast} />}
{page==='settings' && <SettingsPage ollamaModels={ollamaModels} activeModel={activeModel} setActiveModel={setActiveModel} ollamaStatus={ollamaStatus} checkOllama={checkOllama} toast={toast} />}
</div>

{/* Modals */}
{modal==='newProject' && (
<NewProjectModal onClose={() => setModal(null)} onCreate={createProject} />
)}

<ToastStack toasts={toasts} />
</div>
)
}

// ── PROJECTS PAGE ─────────────────────────────────────────────────────────────
function ProjectsPage({ projects, setActiveProject, activeProject, onCreate, onDelete, toast }) {
return (
<div style={{ padding:'24px 28px', overflow:'auto', height:'100%' }}>
<div className="fadeUp" style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:24 }}>
<div>
<h1 style={{ fontSize:20, fontWeight:700, letterSpacing:-0.4 }}>Projects</h1>
<div style={{ fontSize:11, color:C.textMuted, marginTop:2 }}>{projects.length} projects · Click to set active</div>
</div>
<button className="btn btn-primary" onClick={onCreate}>
<Icon name="plus" size={13} color="#fff" /> New Project
</button>
</div>

<div className="fadeUp d1" style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(260px, 1fr))', gap:12 }}>
{projects.map((p, i) => (
<div key={p.id} className="card card-hover"
style={{ padding:18, cursor:'pointer', borderColor: activeProject?.id===p.id ? C.accent+'60' : C.border, animationDelay:`${i*0.04}s` }}
onClick={() => { setActiveProject(p); toast(`"${p.name}" is now active`, 'success') }}>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:12 }}>
<div style={{ width:40, height:40, borderRadius:10, background:`${p.color}18`, border:`1px solid ${p.color}30`, display:'flex', alignItems:'center', justifyContent:'center', fontSize:20 }}>
{p.icon}
</div>
<div style={{ display:'flex', gap:6, alignItems:'center' }}>
{activeProject?.id===p.id && <span className="badge badge-blue">Active</span>}
<button className="btn btn-icon btn-icon-sm btn"
onClick={e => { e.stopPropagation(); onDelete(p.id) }}
style={{ color:C.red, borderColor:'transparent' }}>
<Icon name="trash" size={12} color={C.red} />
</button>
</div>
</div>
<div style={{ fontWeight:600, fontSize:13, marginBottom:4 }}>{p.name}</div>
<div style={{ fontSize:11, color:C.textMuted, marginBottom:12 }}>{p.desc}</div>
<div style={{ display:'flex', justifyContent:'space-between', fontSize:10, color:C.textMuted }}>
<span>📁 {Object.keys(p.files||{}).length} files</span>
<span>{p.created}</span>
</div>
</div>
))}

{/* New card */}
<div className="card" style={{ padding:18, cursor:'pointer', border:`1.5px dashed ${C.border}`, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:8, minHeight:140, opacity:0.5, transition:'opacity 0.2s' }}
onMouseEnter={e=>e.currentTarget.style.opacity=1} onMouseLeave={e=>e.currentTarget.style.opacity=0.5}
onClick={onCreate}>
<div style={{ width:36, height:36, borderRadius:9, background:C.accentSoft, display:'flex', alignItems:'center', justifyContent:'center' }}>
<Icon name="plus" size={18} color={C.accent} />
</div>
<div style={{ fontSize:12, color:C.textDim, fontWeight:600 }}>New Project</div>
</div>
</div>
</div>
)
}

// ── AI PAGE (Real Ollama) ─────────────────────────────────────────────────────
function AIPage({ project, model, ollamaStatus, toast }) {
const [messages, setMessages] = useState([
{ role:'assistant', content:`Hi! I'm Domvia AI running on **${model || 'Ollama'}** locally on your machine — completely private, no internet needed.nnI can:n• Generate full React components and pagesn• Write backend API routesn• Design database schemasn• Debug your coden• Build entire app features from a descriptionnnWhat would you like to build?` }
])
const [input, setInput] = useState('')
const [loading, setLoading] = useState(false)
const [streamBuffer, setStreamBuffer] = useState('')
const bottomRef = useRef(null)
const inputRef = useRef(null)

const SYSTEM = `You are Domvia AI, an expert full-stack software engineer. You help users build web applications using React, Node.js, and modern JavaScript. When asked to generate code, produce complete, working code. Format code in markdown code blocks. Be concise but thorough.`

const SUGGESTIONS = [
'Generate a complete React dashboard with sidebar navigation',
'Create a REST API with Express for a todo app',
'Build a login form with validation and error handling',
'Write a PostgreSQL schema for an e-commerce app',
'Create a dark mode toggle with localStorage persistence',
]

useEffect(() => { bottomRef.current?.scrollIntoView({ behavior:'smooth' }) }, [messages, streamBuffer])

const send = async (text) => {
const msg = (text || input).trim()
if (!msg || loading) return
setInput('')
const userMessages = [...messages, { role:'user', content:msg }]
setMessages(userMessages)
setLoading(true)
setStreamBuffer('')

if (!IS_ELECTRON || ollamaStatus !== 'ok') {
// Fallback simulation
await sleep(800)
const reply = `I'd generate that using ${model}, but Ollama isn't connected yet. Start Ollama with:nn```bashnollama serven```nnThen pull a model:n```bashnollama pull llama3n````
setMessages(p => [...p, { role:'assistant', content:reply }])
setLoading(false)
return
}

// Real Ollama streaming
let full = ''
api.ollama.removeListeners()
api.ollama.onToken(token => {
full += token
setStreamBuffer(full)
})
api.ollama.onDone(() => {
setMessages(p => [...p, { role:'assistant', content:full }])
setStreamBuffer('')
setLoading(false)
api.ollama.removeListeners()
})
api.ollama.onError(e => {
toast(`AI error: ${e}`, 'error')
setLoading(false)
setStreamBuffer('')
api.ollama.removeListeners()
})

await api.ollama.stream(model, userMessages.map(m => ({ role:m.role, content:m.content })), SYSTEM)
}

const renderMessage = (content) => {
// Simple markdown-ish rendering
const parts = content.split(/(```[sS]*?```)/g)
return parts.map((part, i) => {
if (part.startsWith('```')) {
const lines = part.slice(3).split('n')
const lang = lines[0]
const code = lines.slice(1, -1).join('n')
return (
<div key={i} style={{ margin:'10px 0', position:'relative' }}>
{lang && <div style={{ fontSize:10, color:C.textMuted, background:C.surface, padding:'4px 14px', borderRadius:'8px 8px 0 0', borderBottom:`1px solid ${C.border}` }}>{lang}</div>}
<div className="code-area selectable" style={{ padding:'12px 14px', borderRadius: lang ? '0 0 8px 8px' : 8, fontSize:11 }}>
<pre style={{ margin:0, whiteSpace:'pre-wrap', wordBreak:'break-word' }}
dangerouslySetInnerHTML={{ __html: highlight(code) }} />
</div>
<button className="btn btn-xs btn-ghost" style={{ position:'absolute', top:lang?28:6, right:8 }}
onClick={() => { navigator.clipboard?.writeText(code); toast('Copied!', 'success') }}>
<Icon name="copy" size={11} color={C.textMuted} />
</button>
</div>
)
}
return (
<span key={i} style={{ whiteSpace:'pre-wrap', lineHeight:1.7 }}
dangerouslySetInnerHTML={{ __html: part
.replace(/**(.*?)**/g, '<strong>$1</strong>')
.replace(/`(.*?)`/g, `<code style="background:${C.surface};padding:1px 5px;border-radius:4px;font-family:Geist Mono,monospace;font-size:11px">$1</code>`)
}} />
)
})
}

return (
<div style={{ display:'flex', flexDirection:'column', height:'100%', overflow:'hidden' }}>
{/* Header */}
<div style={{ padding:'14px 20px', borderBottom:`1px solid ${C.border}`, display:'flex', alignItems:'center', gap:10, flexShrink:0 }}>
<div style={{ width:28, height:28, borderRadius:8, background:`linear-gradient(135deg,${C.accent},${C.pink})`, display:'flex', alignItems:'center', justifyContent:'center', fontSize:14 }}>◈</div>
<div>
<div style={{ fontWeight:600, fontSize:13 }}>AI Code Builder</div>
<div style={{ fontSize:10, color: ollamaStatus==='ok' ? C.green : C.red }}>
{ollamaStatus==='ok' ? `● ${model} · local · private` : '● Ollama not running — start it first'}
</div>
</div>
<div style={{ marginLeft:'auto', display:'flex', gap:8 }}>
<button className="btn btn-ghost btn-sm" onClick={() => setMessages([messages[0]])}>Clear</button>
</div>
</div>

{/* Messages */}
<div className="scroll-y" style={{ flex:1, padding:'16px 20px', display:'flex', flexDirection:'column', gap:14 }}>
{messages.map((m, i) => (
<div key={i} style={{ display:'flex', gap:10, flexDirection: m.role==='user' ? 'row-reverse' : 'row', alignItems:'flex-start' }}>
<div style={{ width:26, height:26, borderRadius:'50%', flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', fontSize:11, fontWeight:700,
background: m.role==='user' ? C.accentSoft : `linear-gradient(135deg,${C.accent},${C.pink})`,
color: m.role==='user' ? C.accent : '#fff', border: m.role==='user' ? `1px solid rgba(79,142,247,0.3)` : 'none' }}>
{m.role==='user' ? 'U' : '◈'}
</div>
<div style={{ maxWidth:'80%', padding:'10px 14px', borderRadius:10,
background: m.role==='user' ? C.accent : C.card,
border:`1px solid ${m.role==='user' ? C.accent : C.border}`,
color: m.role==='user' ? '#fff' : C.text, fontSize:12 }}
className="selectable">
{renderMessage(m.content)}
</div>
</div>
))}

{/* Streaming */}
{streamBuffer && (
<div style={{ display:'flex', gap:10, alignItems:'flex-start' }}>
<div style={{ width:26, height:26, borderRadius:'50%', background:`linear-gradient(135deg,${C.accent},${C.pink})`, display:'flex', alignItems:'center', justifyContent:'center', fontSize:11, color:'#fff', flexShrink:0 }}>◈</div>
<div style={{ maxWidth:'80%', padding:'10px 14px', borderRadius:10, background:C.card, border:`1px solid ${C.border}`, fontSize:12, color:C.text }} className="selectable ai-cur">
{renderMessage(streamBuffer)}
</div>
</div>
)}

{loading && !streamBuffer && (
<div style={{ display:'flex', gap:10, alignItems:'center' }}>
<div style={{ width:26, height:26, borderRadius:'50%', background:`linear-gradient(135deg,${C.accent},${C.pink})`, display:'flex', alignItems:'center', justifyContent:'center', fontSize:11, color:'#fff', flexShrink:0 }}>◈</div>
<div style={{ display:'flex', gap:4, padding:'12px 14px', background:C.card, border:`1px solid ${C.border}`, borderRadius:10 }}>
{[0,1,2].map(i => <div key={i} className="pulse-dot" style={{ width:6, height:6, borderRadius:'50%', background:C.accent, animationDelay:`${i*0.2}s` }} />)}
</div>
</div>
)}

{/* Suggestions */}
{messages.length <= 1 && (
<div style={{ display:'flex', flexDirection:'column', gap:6, marginTop:8 }}>
<div style={{ fontSize:10, color:C.textMuted, fontWeight:600, letterSpacing:0.06, marginBottom:2 }}>SUGGESTIONS</div>
{SUGGESTIONS.map(s => (
<div key={s} style={{ padding:'8px 12px', background:C.card, border:`1px solid ${C.border}`, borderRadius:8, cursor:'pointer', fontSize:12, color:C.textSoft, transition:'all 0.12s' }}
onMouseEnter={e => { e.currentTarget.style.borderColor=C.accent; e.currentTarget.style.color=C.text }}
onMouseLeave={e => { e.currentTarget.style.borderColor=C.border; e.currentTarget.style.color=C.textSoft }}
onClick={() => send(s)}>
→ {s}
</div>
))}
</div>
)}
<div ref={bottomRef} />
</div>

{/* Input */}
<div style={{ padding:'12px 20px', borderTop:`1px solid ${C.border}`, flexShrink:0 }}>
<div style={{ display:'flex', gap:8, background:C.card, border:`1px solid ${C.borderBright}`, borderRadius:10, padding:'10px 14px' }}>
<textarea ref={inputRef} className="selectable"
placeholder={ollamaStatus==='ok' ? `Message ${model}…` : 'Start Ollama first: ollama serve'}
value={input} onChange={e => setInput(e.target.value)} rows={2}
style={{ background:'transparent', border:'none', resize:'none', flex:1, fontSize:12, lineHeight:1.6, minHeight:40 }}
onKeyDown={e => { if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); send() } }}
/>
<button className="btn btn-primary btn-sm" style={{ alignSelf:'flex-end' }} onClick={() => send()} disabled={loading || !input.trim()}>
<Icon name="send" size={13} color="#fff" />
</button>
</div>
<div style={{ fontSize:10, color:C.textMuted, marginTop:5, paddingLeft:2 }}>
Enter to send · Shift+Enter for new line · Runs locally via Ollama
</div>
</div>
</div>
)
}

// ── CODE EDITOR PAGE ──────────────────────────────────────────────────────────
function CodeEditorPage({ project, setProjects, projects, toast }) {
const files = project?.files || STARTER_FILES
const fileList = Object.keys(files)
const [activeFile, setActiveFile] = useState(fileList[0] || 'src/App.jsx')
const [content, setContent] = useState(files[activeFile] || '')
const [saved, setSaved] = useState(true)
const [search, setSearch] = useState('')
const [findMode, setFindMode] = useState(false)

useEffect(() => {
if (project) setContent(project.files[activeFile] || '')
}, [activeFile, project])

const save = async () => {
if (!project) { toast('Select a project first', 'warn'); return }
const updated = projects.map(p => p.id===project.id ? {...p, files:{...p.files,[activeFile]:content}} : p)
setProjects(updated)
if (api) await api.projects.save(updated)
setSaved(true)
toast('Saved', 'success')
}

const lines = content.split('n')
const filteredFiles = fileList.filter(f => f.toLowerCase().includes(search.toLowerCase()))

return (
<div style={{ display:'flex', height:'100%', overflow:'hidden' }}>
{/* File sidebar */}
<div style={{ width:180, background:C.surface, borderRight:`1px solid ${C.border}`, display:'flex', flexDirection:'column', flexShrink:0 }}>
<div style={{ padding:'10px 10px 6px', borderBottom:`1px solid ${C.border}` }}>
<div style={{ fontSize:11, fontWeight:600, color:C.textMuted, marginBottom:6, letterSpacing:0.06 }}>
{project ? project.name : 'No project'}
</div>
<div style={{ display:'flex', alignItems:'center', gap:6, background:C.card, border:`1px solid ${C.border}`, borderRadius:6, padding:'5px 8px' }}>
<Icon name="search" size={11} color={C.textMuted} />
<input placeholder="Filter…" value={search} onChange={e=>setSearch(e.target.value)}
style={{ background:'transparent', border:'none', fontSize:11, width:'100%' }} />
</div>
</div>
<div className="scroll-y" style={{ flex:1, padding:'6px 6px' }}>
{filteredFiles.map(f => (
<div key={f} className={`file-item ${activeFile===f?'active':''}`} onClick={() => { setActiveFile(f); setSaved(true) }}>
<span style={{ fontSize:12 }}>{fileIcon(f)}</span>
<span style={{ overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', fontSize:11 }}>{f.split('/').pop()}</span>
</div>
))}
</div>
</div>

{/* Editor area */}
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0 }}>
{/* Toolbar */}
<div style={{ display:'flex', alignItems:'center', gap:8, padding:'8px 14px', borderBottom:`1px solid ${C.border}`, background:C.surface, flexShrink:0 }}>
<span style={{ fontSize:12, fontWeight:500, color:C.textSoft }}>{fileIcon(activeFile)} {activeFile}</span>
{!saved && <span className="badge badge-orange">unsaved</span>}
<div style={{ marginLeft:'auto', display:'flex', gap:6 }}>
<button className="btn btn-ghost btn-sm" onClick={() => setFindMode(p=>!p)}>
<Icon name="search" size={12} color={C.textMuted} /> Find
</button>
<button className="btn btn-primary btn-sm" onClick={save}>
<Icon name="save" size={12} color="#fff" /> Save
</button>
</div>
</div>

{/* Find bar */}
{findMode && (
<div style={{ padding:'6px 14px', background:C.surface, borderBottom:`1px solid ${C.border}`, display:'flex', gap:8, alignItems:'center' }}>
<input placeholder="Find in file…" style={{ width:200, fontSize:11 }} autoFocus />
<input placeholder="Replace…" style={{ width:160, fontSize:11 }} />
<button className="btn btn-ghost btn-xs">Replace All</button>
<button className="btn btn-icon btn-icon-sm btn" onClick={()=>setFindMode(false)}>
<Icon name="x" size={11} />
</button>
</div>
)}

{/* Code */}
<div className="scroll code-area" style={{ flex:1, borderRadius:0, border:'none', padding:'8px 0' }}>
{lines.map((line, i) => (
<div key={i} className="code-line" style={{ minHeight:'1.7em' }}>
<span className="code-ln">{i + 1}</span>
<span className="code-content selectable" dangerouslySetInnerHTML={{ __html: highlight(line) || 'u200b' }} />
</div>
))}
{/* Editable textarea overlay */}
<textarea className="selectable" value={content}
onChange={e => { setContent(e.target.value); setSaved(false) }}
style={{ position:'absolute', inset:0, opacity:0, cursor:'text', resize:'none', width:'100%', height:'100%', padding:0, background:'transparent', border:'none', fontFamily:'Geist Mono,monospace', fontSize:12, lineHeight:'1.7em', paddingLeft:56 }}
onKeyDown={e => { if((e.ctrlKey||e.metaKey)&&e.key==='s'){ e.preventDefault(); save() } }}
/>
</div>

{/* Status bar */}
<div style={{ padding:'4px 14px', background:C.surface, borderTop:`1px solid ${C.border}`, display:'flex', gap:14, fontSize:10, color:C.textMuted, flexShrink:0 }}>
<span>Lines: {lines.length}</span>
<span>Chars: {content.length}</span>
<span>{ext(activeFile).toUpperCase()}</span>
<span style={{ marginLeft:'auto' }}>UTF-8</span>
</div>
</div>
</div>
)
}

// ── VISUAL BUILDER ────────────────────────────────────────────────────────────
function VisualBuilderPage({ project, toast }) {
const [elements, setElements] = useState([
{ id:'v1', type:'Nav', x:0, y:0, w:640, h:48, props:{ brand:'My App' } },
{ id:'v2', type:'Text', x:40, y:70, w:300, h:40, props:{ content:'Welcome back!', size:'xl' } },
{ id:'v3', type:'Card', x:40, y:125, w:280, h:100, props:{ title:'Stats' } },
{ id:'v4', type:'Table', x:40, y:240, w:560, h:140, props:{ columns:4, rows:5 } },
{ id:'v5', type:'Button', x:40, y:396, w:120, h:36, props:{ label:'Save', variant:'primary' } },
])
const [selected, setSelected] = useState(null)
const [dragComp, setDragComp] = useState(null)
const [panelTab, setPanelTab] = useState('components')
const [viewport, setViewport] = useState('desktop')
const canvasRef = useRef(null)

const selEl = elements.find(e => e.id === selected)

const onCanvasDrop = (e) => {
if (!dragComp) return
const rect = canvasRef.current.getBoundingClientRect()
const comp = COMP_PALETTE.find(c => c.type === dragComp)
if (!comp) return
const newEl = { id:uid(), type:dragComp, x:Math.max(0,e.clientX-rect.left-comp.w/2), y:Math.max(0,e.clientY-rect.top-comp.h/2), w:comp.w, h:comp.h, props:{...comp.props} }
setElements(p => [...p, newEl])
setSelected(newEl.id)
toast(`${dragComp} added`, 'success')
setDragComp(null)
}

const updateProp = (key, val) => {
setElements(p => p.map(e => e.id===selected ? {...e, props:{...e.props,[key]:val}} : e))
}

const updatePos = (key, val) => {
setElements(p => p.map(e => e.id===selected ? {...e,[key]:Number(val)} : e))
}

const vwWidth = { desktop:680, tablet:480, mobile:320 }[viewport]

return (
<div style={{ display:'flex', height:'100%', overflow:'hidden' }}>
{/* Left: Component palette */}
<div style={{ width:180, background:C.surface, borderRight:`1px solid ${C.border}`, display:'flex', flexDirection:'column', flexShrink:0 }}>
<div className="tabs" style={{ margin:8 }}>
{['components','layers'].map(t => (
<button key={t} className={`tab ${panelTab===t?'active':''}`} onClick={()=>setPanelTab(t)} style={{ fontSize:10, textTransform:'capitalize' }}>{t}</button>
))}
</div>

{panelTab==='components' && (
<div className="scroll-y" style={{ flex:1, padding:'4px 6px' }}>
<div style={{ fontSize:10, color:C.textMuted, fontWeight:600, letterSpacing:0.07, padding:'4px 6px', marginBottom:4 }}>DRAG TO CANVAS</div>
{COMP_PALETTE.map(c => (
<div key={c.type} style={{ display:'flex', alignItems:'center', gap:8, padding:'6px 8px', borderRadius:7, cursor:'grab', transition:'all 0.12s', border:'1px solid transparent', fontSize:11, color:C.textDim }}
draggable onDragStart={() => setDragComp(c.type)}
onMouseEnter={e => { e.currentTarget.style.background=C.accentSoft; e.currentTarget.style.color=C.text; e.currentTarget.style.borderColor='rgba(79,142,247,0.2)' }}
onMouseLeave={e => { e.currentTarget.style.background=''; e.currentTarget.style.color=''; e.currentTarget.style.borderColor='' }}>
<span style={{ fontSize:14 }}>{c.icon}</span>
<span style={{ fontWeight:500 }}>{c.type}</span>
</div>
))}
</div>
)}

{panelTab==='layers' && (
<div className="scroll-y" style={{ flex:1, padding:'4px 6px' }}>
<div style={{ fontSize:10, color:C.textMuted, fontWeight:600, letterSpacing:0.07, padding:'4px 6px', marginBottom:4 }}>LAYERS ({elements.length})</div>
{[...elements].reverse().map(el => (
<div key={el.id} style={{ display:'flex', alignItems:'center', gap:7, padding:'5px 8px', borderRadius:7, cursor:'pointer', fontSize:11, transition:'all 0.12s',
background: selected===el.id ? C.accentSoft : 'transparent',
color: selected===el.id ? C.accent : C.textDim,
border: `1px solid ${selected===el.id ? 'rgba(79,142,247,0.2)' : 'transparent'}` }}
onClick={() => setSelected(el.id)}>
<span style={{ fontSize:13 }}>{COMP_PALETTE.find(c=>c.type===el.type)?.icon||'▢'}</span>
<span style={{ fontWeight:500, flex:1 }}>{el.type}</span>
<button style={{ background:'none', border:'none', cursor:'pointer', padding:2 }}
onClick={e => { e.stopPropagation(); setElements(p=>p.filter(x=>x.id!==el.id)); setSelected(null) }}>
<Icon name="x" size={11} color={C.textMuted} />
</button>
</div>
))}
</div>
)}
</div>

{/* Canvas */}
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', background:'#080810' }}>
{/* Toolbar */}
<div style={{ display:'flex', alignItems:'center', gap:8, padding:'8px 14px', background:C.surface, borderBottom:`1px solid ${C.border}`, flexShrink:0 }}>
{['desktop','tablet','mobile'].map(v => (
<button key={v} className={`btn btn-sm ${viewport===v?'btn-primary':'btn-ghost'}`} onClick={()=>setViewport(v)}>
{v==='desktop'?'🖥 Desktop':v==='tablet'?'📱 Tablet':'📱 Mobile'}
</button>
))}
<div style={{ marginLeft:'auto', display:'flex', gap:6 }}>
<button className="btn btn-ghost btn-sm" onClick={()=>setElements([])}>Clear</button>
<button className="btn btn-green btn-sm" onClick={()=>toast('Published!','success')}>
<Icon name="deploy" size={12} color="#000" /> Publish
</button>
</div>
</div>

{/* Canvas area */}
<div className="scroll" style={{ flex:1, display:'flex', alignItems:'flex-start', justifyContent:'center', padding:20 }}>
<div ref={canvasRef} className="canvas-bg"
style={{ width:vwWidth, minHeight:500, position:'relative', borderRadius:10, border:`1px solid ${C.border}`, overflow:'hidden', transition:'width 0.3s' }}
onDragOver={e=>e.preventDefault()} onDrop={onCanvasDrop}
onClick={e=>{ if(e.target===canvasRef.current) setSelected(null) }}>
{elements.map(el => (
<div key={el.id} className={`canvas-el ${selected===el.id?'sel':''}`}
style={{ left:el.x, top:el.y, width:el.w, height:el.h }}
onClick={e => { e.stopPropagation(); setSelected(el.id) }}>
<span style={{ fontSize:11, fontWeight:500, color:selected===el.id?C.accent:C.textDim }}>
{COMP_PALETTE.find(c=>c.type===el.type)?.icon} {el.type}
{el.props.label||el.props.content||el.props.title ? ` · ${(el.props.label||el.props.content||el.props.title).slice(0,18)}` : ''}
</span>
{selected===el.id && (
<button style={{ position:'absolute', top:-7, right:-7, width:16, height:16, borderRadius:'50%', background:C.red, border:'none', cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center' }}
onClick={e=>{ e.stopPropagation(); setElements(p=>p.filter(x=>x.id!==el.id)); setSelected(null) }}>
<span style={{ fontSize:9, color:'#fff', fontWeight:700 }}>×</span>
</button>
)}
</div>
))}
{elements.length===0 && (
<div style={{ position:'absolute', inset:0, display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column', gap:8, color:C.textMuted }}>
<div style={{ fontSize:32 }}>⬛</div>
<div style={{ fontSize:12 }}>Drag components here</div>
</div>
)}
</div>
</div>
</div>

{/* Right: Properties */}
<div style={{ width:220, background:C.surface, borderLeft:`1px solid ${C.border}`, padding:14, overflow:'auto', flexShrink:0 }}>
{selEl ? (
<>
<div style={{ fontWeight:700, fontSize:13, marginBottom:14 }}>{selEl.type} Properties</div>
<div style={{ display:'flex', flexDirection:'column', gap:10 }}>
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:6 }}>
{[['x','X'],['y','Y'],['w','W'],['h','H']].map(([k,l]) => (
<div key={k}><label>{l}</label><input type="number" value={selEl[k]} onChange={e=>updatePos(k,e.target.value)}/></div>
))}
</div>
<div className="divider"/>
{Object.entries(selEl.props).map(([k,v]) => (
<div key={k}>
<label>{k}</label>
<input value={String(v)} onChange={e=>updateProp(k,e.target.value)}/>
</div>
))}
<button className="btn btn-danger btn-sm" style={{ justifyContent:'center', marginTop:4 }}
onClick={() => { setElements(p=>p.filter(e=>e.id!==selected)); setSelected(null) }}>
<Icon name="trash" size={12} color={C.red}/> Remove
</button>
</div>
</>
) : (
<div style={{ textAlign:'center', padding:'40px 0', color:C.textMuted, fontSize:11 }}>
Select an element<br/>to edit properties
</div>
)}
</div>
</div>
)
}

// ── DATABASE PAGE ─────────────────────────────────────────────────────────────
function DatabasePage({ toast }) {
const [tables, setTables] = useState([
{ name:'users', fields:[{name:'id',type:'uuid',pk:true},{name:'name',type:'text'},{name:'email',type:'text',unique:true},{name:'role',type:'enum'},{name:'created_at',type:'timestamp'}] },
{ name:'projects', fields:[{name:'id',type:'uuid',pk:true},{name:'title',type:'text'},{name:'owner_id',type:'uuid',fk:true},{name:'status',type:'enum'},{name:'updated_at',type:'timestamp'}] },
{ name:'tasks', fields:[{name:'id',type:'uuid',pk:true},{name:'title',type:'text'},{name:'project_id',type:'uuid',fk:true},{name:'done',type:'boolean'},{name:'due_date',type:'timestamp'}] },
])
const [active, setActive] = useState('users')
const [modal, setModal] = useState(null)
const [newField, setNewField] = useState({ name:'', type:'text' })
const [newTable, setNewTable] = useState('')

const tbl = tables.find(t => t.name === active)
const TYPE_COLORS = { uuid:C.pink, text:C.accent, integer:C.green, boolean:C.yellow, timestamp:C.orange, enum:C.accentHover, float:C.green }

const addField = () => {
if (!newField.name) return
setTables(p => p.map(t => t.name===active ? {...t, fields:[...t.fields,{name:newField.name,type:newField.type}]} : t))
toast(`Column "${newField.name}" added`, 'success')
setNewField({ name:'', type:'text' }); setModal(null)
}

const addTable = () => {
if (!newTable) return
setTables(p => [...p, { name:newTable, fields:[{name:'id',type:'uuid',pk:true},{name:'created_at',type:'timestamp'}] }])
setActive(newTable); setNewTable(''); setModal(null)
toast(`Table "${newTable}" created`, 'success')
}

const generateSQL = () => {
if (!tbl) return ''
const cols = tbl.fields.map(f => ` ${f.name} ${f.type.toUpperCase()}${f.pk?' PRIMARY KEY':''}${f.unique?' UNIQUE':''}`).join(',n')
return `CREATE TABLE ${tbl.name} (n${cols}n);`
}

return (
<div style={{ display:'flex', height:'100%', overflow:'hidden' }}>
{/* Tables list */}
<div style={{ width:180, background:C.surface, borderRight:`1px solid ${C.border}`, display:'flex', flexDirection:'column', flexShrink:0 }}>
<div style={{ padding:'10px 10px 6px', borderBottom:`1px solid ${C.border}` }}>
<div style={{ fontWeight:600, fontSize:11, color:C.textMuted, marginBottom:8 }}>TABLES</div>
<button className="btn btn-primary btn-sm" style={{ width:'100%', justifyContent:'center' }} onClick={()=>setModal('addTable')}>
<Icon name="plus" size={12} color="#fff"/> Add Table
</button>
</div>
<div className="scroll-y" style={{ flex:1, padding:'6px' }}>
{tables.map(t => (
<div key={t.name} className={`nav-item ${active===t.name?'active':''}`} onClick={()=>setActive(t.name)}
style={{ justifyContent:'space-between', fontSize:11 }}>
<div style={{ display:'flex', gap:6, alignItems:'center' }}>
<Icon name="db" size={12} color={active===t.name?C.accent:C.textMuted}/>
<span className="mono">{t.name}</span>
</div>
<span style={{ fontSize:10 }}>{t.fields.length}</span>
</div>
))}
</div>
</div>

{/* Table detail */}
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden' }}>
<div style={{ display:'flex', alignItems:'center', gap:10, padding:'10px 16px', background:C.surface, borderBottom:`1px solid ${C.border}`, flexShrink:0 }}>
<span className="mono" style={{ fontWeight:700, fontSize:14, color:C.accentHover }}>{tbl?.name}</span>
<span style={{ fontSize:11, color:C.textMuted }}>{tbl?.fields.length} columns</span>
<div style={{ marginLeft:'auto', display:'flex', gap:6 }}>
<button className="btn btn-ghost btn-sm" onClick={()=>{ navigator.clipboard?.writeText(generateSQL()); toast('SQL copied!','success') }}>
<Icon name="copy" size={12} color={C.textMuted}/> Copy SQL
</button>
<button className="btn btn-primary btn-sm" onClick={()=>setModal('addField')}>
<Icon name="plus" size={12} color="#fff"/> Add Column
</button>
</div>
</div>

<div className="scroll-y" style={{ flex:1 }}>
<table className="tbl">
<thead><tr><th>Column</th><th>Type</th><th>Constraints</th><th>Actions</th></tr></thead>
<tbody>
{tbl?.fields.map(f => (
<tr key={f.name}>
<td>
<div style={{ display:'flex', alignItems:'center', gap:7 }}>
{f.pk && <span style={{ fontSize:9, background:'rgba(251,191,36,0.15)', color:C.yellow, padding:'1px 5px', borderRadius:4, fontWeight:700 }}>PK</span>}
{f.fk && <span style={{ fontSize:9, background:C.accentSoft, color:C.accent, padding:'1px 5px', borderRadius:4, fontWeight:700 }}>FK</span>}
<span className="mono selectable" style={{ fontWeight:600, fontSize:12 }}>{f.name}</span>
</div>
</td>
<td><span className="mono" style={{ background:`${TYPE_COLORS[f.type]||C.textMuted}15`, color:TYPE_COLORS[f.type]||C.textMuted, padding:'2px 7px', borderRadius:5, fontSize:11, fontWeight:600 }}>{f.type}</span></td>
<td style={{ fontSize:11, color:C.textMuted }}>{f.pk?'PRIMARY KEY':f.fk?'FOREIGN KEY':f.unique?'UNIQUE':'—'}</td>
<td>
{!f.pk && (
<button className="btn btn-icon btn-icon-sm btn" onClick={()=>{
setTables(p=>p.map(t=>t.name===active?{...t,fields:t.fields.filter(x=>x.name!==f.name)}:t))
toast(`Removed "${f.name}"`, 'warn')
}}>
<Icon name="trash" size={11} color={C.red}/>
</button>
)}
</td>
</tr>
))}
</tbody>
</table>

{/* SQL preview */}
<div style={{ margin:16 }}>
<div style={{ fontSize:10, color:C.textMuted, fontWeight:600, letterSpacing:0.07, marginBottom:8 }}>GENERATED SQL</div>
<div className="code-area selectable" style={{ padding:'12px 16px', fontSize:11 }}>
<pre style={{ margin:0, whiteSpace:'pre-wrap' }}>{generateSQL()}</pre>
</div>
</div>
</div>
</div>

{modal==='addField' && (
<Modal title="Add Column" sub={`Table: ${active}`} onClose={()=>setModal(null)}>
<div style={{ display:'flex', flexDirection:'column', gap:12 }}>
<div><label>Column Name</label><input className="mono" placeholder="column_name" value={newField.name} onChange={e=>setNewField(p=>({...p,name:e.target.value}))} autoFocus/></div>
<div><label>Type</label>
<select value={newField.type} onChange={e=>setNewField(p=>({...p,type:e.target.value}))}>
{['text','integer','float','boolean','uuid','timestamp','enum','json','array'].map(t=><option key={t}>{t}</option>)}
</select>
</div>
<div style={{ display:'flex', gap:8, marginTop:4 }}>
<button className="btn btn-primary" style={{ flex:1, justifyContent:'center' }} onClick={addField}>Add Column</button>
<button className="btn btn-ghost" onClick={()=>setModal(null)}>Cancel</button>
</div>
</div>
</Modal>
)}

{modal==='addTable' && (
<Modal title="New Table" onClose={()=>setModal(null)}>
<div style={{ display:'flex', flexDirection:'column', gap:12 }}>
<div><label>Table Name</label><input className="mono" placeholder="table_name" value={newTable} onChange={e=>setNewTable(e.target.value)} autoFocus/></div>
<div style={{ display:'flex', gap:8, marginTop:4 }}>
<button className="btn btn-primary" style={{ flex:1, justifyContent:'center' }} onClick={addTable}>Create Table</button>
<button className="btn btn-ghost" onClick={()=>setModal(null)}>Cancel</button>
</div>
</div>
</Modal>
)}
</div>
)
}

// ── FILES PAGE ────────────────────────────────────────────────────────────────
function FilesPage({ project, toast }) {
const [currentPath, setCurrentPath] = useState('~')
const [entries, setEntries] = useState([])
const [loading, setLoading] = useState(false)
const [preview, setPreview] = useState(null)
const [previewContent, setPreviewContent] = useState('')

useEffect(() => { loadDir(currentPath) }, [currentPath])

const loadDir = async (path) => {
if (!api) {
// Demo data
setEntries([
{ name:'src', isDir:true, path:'~/src' },
{ name:'package.json', isDir:false, path:'~/package.json' },
{ name:'index.html', isDir:false, path:'~/index.html' },
{ name:'vite.config.js', isDir:false, path:'~/vite.config.js' },
{ name:'README.md', isDir:false, path:'~/README.md' },
])
return
}
setLoading(true)
const home = await api.app.getPath('home')
const realPath = path === '~' ? home : path
const res = await api.fs.readDir(realPath)
if (res.ok) setEntries(res.data)
else toast(`Cannot open: ${res.error}`, 'error')
setLoading(false)
}

const openFile = async (entry) => {
if (entry.isDir) { setCurrentPath(entry.path); return }
if (!api) { setPreview(entry.name); setPreviewContent('// File preview requires Electron'); return }
const res = await api.fs.readFile(entry.path)
if (res.ok) { setPreview(entry.name); setPreviewContent(res.data) }
else toast(`Cannot read: ${res.error}`, 'error')
}

const pickFolder = async () => {
if (!api) { toast('File dialogs need Electron', 'warn'); return }
const path = await api.dialog.openDir()
if (path) setCurrentPath(path)
}

return (
<div style={{ display:'flex', height:'100%', overflow:'hidden' }}>
{/* File list */}
<div style={{ width:260, background:C.surface, borderRight:`1px solid ${C.border}`, display:'flex', flexDirection:'column', flexShrink:0 }}>
<div style={{ padding:'10px', borderBottom:`1px solid ${C.border}` }}>
<div style={{ display:'flex', gap:6, marginBottom:8 }}>
<button className="btn btn-ghost btn-sm" onClick={()=>setCurrentPath('~')}>~</button>
<button className="btn btn-primary btn-sm" onClick={pickFolder}>
<Icon name="folder" size={12} color="#fff"/> Open Folder
</button>
</div>
<div style={{ fontSize:10, color:C.textMuted, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }} title={currentPath}>{currentPath}</div>
</div>
<div className="scroll-y" style={{ flex:1, padding:'6px' }}>
{loading && <div style={{ textAlign:'center', padding:20, fontSize:11, color:C.textMuted }}>Loading…</div>}
{!loading && entries.length===0 && <div style={{ textAlign:'center', padding:20, fontSize:11, color:C.textMuted }}>Empty folder</div>}
{entries.map(e => (
<div key={e.name} className={`file-item ${preview===e.name?'active':''}`} onClick={()=>openFile(e)}>
<span style={{ fontSize:13 }}>{e.isDir ? '📁' : fileIcon(e.name)}</span>
<span style={{ flex:1, overflow:'hidden', textOverflow:'ellipsis', fontSize:11 }}>{e.name}</span>
{e.isDir && <Icon name="chevronR" size={11} color={C.textMuted}/>}
</div>
))}
</div>
</div>

{/* Preview */}
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden' }}>
{preview ? (
<>
<div style={{ padding:'10px 16px', background:C.surface, borderBottom:`1px solid ${C.border}`, display:'flex', alignItems:'center', gap:8, flexShrink:0 }}>
<span style={{ fontSize:14 }}>{fileIcon(preview)}</span>
<span style={{ fontSize:12, fontWeight:500, color:C.textSoft }}>{preview}</span>
<div style={{ marginLeft:'auto', display:'flex', gap:6 }}>
<button className="btn btn-ghost btn-sm" onClick={()=>{ navigator.clipboard?.writeText(previewContent); toast('Copied','success') }}>
<Icon name="copy" size={12} color={C.textMuted}/>
</button>
<button className="btn btn-icon btn-icon-sm btn" onClick={()=>setPreview(null)}>
<Icon name="x" size={12}/>
</button>
</div>
</div>
<div className="scroll code-area selectable" style={{ flex:1, borderRadius:0, border:'none', padding:'12px 0', fontSize:11 }}>
{previewContent.split('n').map((line,i) => (
<div key={i} className="code-line" style={{ minHeight:'1.7em' }}>
<span className="code-ln">{i+1}</span>
<span className="code-content" dangerouslySetInnerHTML={{ __html: highlight(line)||'u200b' }}/>
</div>
))}
</div>
</>
) : (
<div style={{ flex:1, display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column', gap:10, color:C.textMuted }}>
<Icon name="files" size={40} color={C.border}/>
<div style={{ fontSize:13, color:C.textDim }}>Select a file to preview</div>
<button className="btn btn-ghost btn-sm" onClick={pickFolder}>Open a folder</button>
</div>
)}
</div>
</div>
)
}

// ── TERMINAL PAGE ─────────────────────────────────────────────────────────────
function TerminalPage({ toast }) {
const [lines, setLines] = useState([
{ type:'info', text:'Domvia Terminal · Type commands to run on your system' },
{ type:'info', text:'Note: runs via Electron shell bridge' },
{ type:'prompt', text:'' },
])
const [input, setInput] = useState('')
const [cwd, setCwd] = useState('~')
const [history, setHistory]= useState([])
const [histIdx, setHistIdx]= useState(-1)
const bottomRef = useRef(null)
const inputRef = useRef(null)

useEffect(() => { bottomRef.current?.scrollIntoView({ behavior:'smooth' }) }, [lines])

const run = async () => {
const cmd = input.trim()
if (!cmd) return
setHistory(p => [cmd, ...p])
setHistIdx(-1)
setLines(p => [...p.slice(0,-1), { type:'cmd', text:`$ ${cmd}` }])
setInput('')

if (cmd === 'clear') { setLines([{ type:'prompt', text:'' }]); return }
if (cmd.startsWith('cd ')) {
const dir = cmd.slice(3)
setCwd(dir === '~' ? '~' : `${cwd}/${dir}`)
setLines(p => [...p, { type:'prompt', text:'' }])
return
}

if (!api) {
setLines(p => [...p,
{ type:'out', text:'(Electron not detected — commands run in real Electron build)' },
{ type:'prompt', text:'' }
])
return
}

const res = await api.shell.exec(cmd, cwd === '~' ? undefined : cwd)
const output = res.stdout || res.stderr || '(no output)'
const type = res.ok ? 'out' : 'err'
setLines(p => [...p, { type, text: output }, { type:'prompt', text:'' }])
}

const onKey = (e) => {
if (e.key === 'Enter') { run(); return }
if (e.key === 'ArrowUp') {
const idx = Math.min(histIdx+1, history.length-1)
setHistIdx(idx); setInput(history[idx]||'')
}
if (e.key === 'ArrowDown') {
const idx = Math.max(histIdx-1, -1)
setHistIdx(idx); setInput(idx===-1?'':history[idx]||'')
}
}

return (
<div style={{ display:'flex', flexDirection:'column', height:'100%', background:'#060609', overflow:'hidden' }}>
{/* Toolbar */}
<div style={{ display:'flex', alignItems:'center', gap:8, padding:'8px 14px', background:C.surface, borderBottom:`1px solid ${C.border}`, flexShrink:0 }}>
<div style={{ display:'flex', gap:6' }}>
<div style={{ width:10, height:10, borderRadius:'50%', background:'#FF5F56' }}/>
<div style={{ width:10, height:10, borderRadius:'50%', background:'#FFBD2E' }}/>
<div style={{ width:10, height:10, borderRadius:'50%', background:'#27C93F' }}/>
</div>
<span style={{ fontSize:11, color:C.textMuted, marginLeft:6 }}>Terminal · {cwd}</span>
<button className="btn btn-ghost btn-sm" style={{ marginLeft:'auto' }} onClick={()=>setLines([{type:'prompt',text:''}])}>Clear</button>
</div>

{/* Output */}
<div className="scroll-y selectable" style={{ flex:1, padding:'10px 16px', fontFamily:'Geist Mono,monospace', fontSize:12, lineHeight:'1.8' }}
onClick={()=>inputRef.current?.focus()}>
{lines.map((l,i) => (
<div key={i} style={{ color: l.type==='err'?C.red : l.type==='cmd'?C.accentHover : l.type==='info'?C.textMuted : '#A8E6A3', marginBottom:1 }}>
{l.text}
</div>
))}
{/* Live input line */}
<div style={{ display:'flex', alignItems:'center', color:C.green }}>
<span style={{ marginRight:8 }}>$ </span>
<input ref={inputRef} value={input} onChange={e=>setInput(e.target.value)} onKeyDown={onKey}
className="selectable" autoFocus
style={{ background:'transparent', border:'none', color:C.text, fontFamily:'Geist Mono,monospace', fontSize:12, flex:1, outline:'none', caretColor:C.accent }}
placeholder="type a command…"/>
</div>
<div ref={bottomRef}/>
</div>
</div>
)
}

// ── DEPLOY PAGE ───────────────────────────────────────────────────────────────
function DeployPage({ project, toast }) {
const [steps, setSteps] = useState([])
const [deploying, setDeploying] = useState(false)
const [done, setDone] = useState(false)
const [target, setTarget] = useState('linux')
const [envVars, setEnvVars] = useState([{ key:'NODE_ENV', val:'production' },{ key:'PORT', val:'3000' }])

const TARGETS = [
{ id:'linux', icon:'🐧', label:'Linux Server', desc:'SSH deploy to your own server', cmd:'npm run build:linux' },
{ id:'win', icon:'🪟', label:'Windows', desc:'Build .exe installer', cmd:'npm run build:win' },
{ id:'mac', icon:'🍎', label:'macOS', desc:'Build .dmg package', cmd:'npm run build:mac' },
{ id:'docker', icon:'🐳', label:'Docker', desc:'docker-compose up', cmd:'docker-compose up -d'},
]

const BUILD_STEPS = [
'Checking dependencies…',
'Running npm install…',
'Compiling TypeScript…',
'Bundling with Vite…',
'Optimizing assets…',
'Packaging Electron…',
'Creating installer…',
'Done! 🎉',
]

const deploy = async () => {
setDeploying(true); setDone(false); setSteps([])
for (let i=0; i<BUILD_STEPS.length; i++) {
await sleep(600)
setSteps(p => [...p, { text:BUILD_STEPS[i], done:false }])
await sleep(200)
setSteps(p => p.map((s,j) => j===i ? {...s,done:true} : s))
}
setDeploying(false); setDone(true)
toast('Build complete! Check dist-electron/', 'success')
}

return (
<div style={{ padding:'20px 24px', overflow:'auto', height:'100%' }}>
<div className="fadeUp" style={{ marginBottom:20 }}>
<h2 style={{ fontSize:17, fontWeight:700, letterSpacing:-0.3, marginBottom:4 }}>Deploy & Build</h2>
<div style={{ fontSize:11, color:C.textMuted }}>Build Domvia as a desktop app or deploy to your server</div>
</div>

{/* Target grid */}
<div className="fadeUp d1" style={{ display:'grid', gridTemplateColumns:'repeat(2,1fr)', gap:10, marginBottom:20 }}>
{TARGETS.map(t => (
<div key={t.id} className="card card-hover" style={{ padding:'14px 16px', cursor:'pointer', borderColor: target===t.id ? C.accent+'60':C.border, background: target===t.id ? C.accentSoft:C.card }}
onClick={() => setTarget(t.id)}>
<div style={{ display:'flex', gap:10, alignItems:'center', marginBottom:6 }}>
<span style={{ fontSize:22 }}>{t.icon}</span>
<div>
<div style={{ fontWeight:600, fontSize:13, color:target===t.id?C.accent:C.text }}>{t.label}</div>
<div style={{ fontSize:11, color:C.textMuted }}>{t.desc}</div>
</div>
{target===t.id && <div style={{ marginLeft:'auto', width:8, height:8, borderRadius:'50%', background:C.accent }}/>}
</div>
<div className="mono" style={{ fontSize:10, color:C.textMuted, background:C.surface, padding:'4px 8px', borderRadius:5 }}>{t.cmd}</div>
</div>
))}
</div>

{/* Env vars */}
<div className="card fadeUp d2" style={{ padding:16, marginBottom:16 }}>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:12 }}>
<span style={{ fontWeight:600, fontSize:12 }}>Environment Variables</span>
<button className="btn btn-ghost btn-sm" onClick={()=>setEnvVars(p=>[...p,{key:'',val:''}])}>
<Icon name="plus" size:11 color={C.textMuted}/> Add
</button>
</div>
<div style={{ display:'flex', flexDirection:'column', gap:7 }}>
{envVars.map((e,i) => (
<div key={i} style={{ display:'flex', gap:6, alignItems:'center' }}>
<input className="mono" placeholder="KEY" value={e.key} style={{ width:150, fontSize:11 }} onChange={v=>setEnvVars(p=>p.map((x,j)=>j===i?{...x,key:v.target.value}:x))}/>
<span style={{ color:C.border, fontWeight:700 }}>=</span>
<input placeholder="value" value={e.val} style={{ flex:1, fontSize:11 }} onChange={v=>setEnvVars(p=>p.map((x,j)=>j===i?{...x,val:v.target.value}:x))}/>
<button className="btn btn-icon btn-icon-sm btn" onClick={()=>setEnvVars(p=>p.filter((_,j)=>j!==i))}>
<Icon name="x" size={11}/>
</button>
</div>
))}
</div>
</div>

{/* Build */}
<div className="card fadeUp d3" style={{ padding:20, textAlign:'center' }}>
{!deploying && !done && (
<>
<div style={{ fontSize:36, marginBottom:10 }}>🚀</div>
<div style={{ fontWeight:600, fontSize:14, marginBottom:4 }}>Ready to build</div>
<div style={{ fontSize:11, color:C.textMuted, marginBottom:16 }}>Target: {TARGETS.find(t=>t.id===target)?.label}</div>
<button className="btn btn-primary" style={{ padding:'9px 28px' }} onClick={deploy}>
<Icon name="pkg" size={14} color="#fff"/> Build Now
</button>
</>
)}
{(deploying || done) && (
<div style={{ textAlign:'left', maxWidth:360, margin:'0 auto' }}>
{steps.map((s,i) => (
<div key={i} style={{ display:'flex', alignItems:'center', gap:10, padding:'7px 0', borderBottom:`1px solid ${C.border}40` }}>
<div style={{ width:16, height:16, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center' }}>
{s.done ? <Icon name="check" size={14} color={C.green}/> : <div className="spin" style={{ width:12, height:12, borderRadius:'50%', border:`2px solid ${C.border}`, borderTop:`2px solid ${C.accent}` }}/>}
</div>
<span style={{ fontSize:12, color:s.done?C.textSoft:C.textMuted }}>{s.text}</span>
</div>
))}
{done && (
<div style={{ textAlign:'center', marginTop:16 }}>
<div style={{ fontSize:24, marginBottom:8 }}>✅</div>
<div style={{ fontWeight:700, color:C.green, marginBottom:6 }}>Build Complete!</div>
<div style={{ fontSize:11, color:C.textMuted, marginBottom:12 }}>Check <code>dist-electron/</code> for your installer</div>
<button className="btn btn-ghost btn-sm" onClick={()=>{setDone(false);setSteps([])}}>Build Again</button>
</div>
)}
</div>
)}
</div>
</div>
)
}

// ── SETTINGS PAGE ─────────────────────────────────────────────────────────────
function SettingsPage({ ollamaModels, activeModel, setActiveModel, ollamaStatus, checkOllama, toast }) {
const [sw, setSw] = useState({ autosave:true, linting:true, vim:false, telemetry:false })

return (
<div style={{ padding:'20px 24px', overflow:'auto', height:'100%', maxWidth:640 }}>
<div className="fadeUp" style={{ marginBottom:20 }}>
<h2 style={{ fontSize:17, fontWeight:700, letterSpacing:-0.3, marginBottom:4 }}>Settings</h2>
</div>

{/* Ollama */}
<div className="card fadeUp d1" style={{ padding:18, marginBottom:14 }}>
<div style={{ fontWeight:700, fontSize:13, marginBottom:14 }}>🤖 AI / Ollama</div>
<div style={{ display:'flex', alignItems:'center', gap:10, padding:'10px 14px', background:C.surface, borderRadius:8, marginBottom:12, border:`1px solid ${ollamaStatus==='ok'?C.green+'30':C.red+'30'}` }}>
<div style={{ width:8, height:8, borderRadius:'50%', background:ollamaStatus==='ok'?C.green:C.red, flexShrink:0 }} className="pulse-dot"/>
<div style={{ flex:1 }}>
<div style={{ fontSize:12, fontWeight:600 }}>{ollamaStatus==='ok' ? 'Ollama is running' : 'Ollama not detected'}</div>
<div style={{ fontSize:10, color:C.textMuted }}>{ollamaStatus==='ok' ? `${ollamaModels.length} model${ollamaModels.length!==1?'s':''} available` : 'Run: ollama serve'}</div>
</div>
<button className="btn btn-ghost btn-sm" onClick={checkOllama}>
<Icon name="refresh" size={12} color={C.textMuted}/> Check
</button>
</div>

{ollamaModels.length > 0 && (
<div style={{ marginBottom:12 }}>
<label>Active Model</label>
<select value={activeModel} onChange={e=>{ setActiveModel(e.target.value); toast(`Model set to ${e.target.value}`, 'success') }}>
{ollamaModels.map(m => <option key={m.name} value={m.name}>{m.name}</option>)}
</select>
</div>
)}

{ollamaStatus !== 'ok' && (
<div className="code-area" style={{ padding:'10px 14px', fontSize:11 }}>
<div style={{ color:C.textMuted, marginBottom:6 }}># Install & start Ollama:</div>
<div style={{ color:C.green }}>curl -fsSL https://ollama.ai/install.sh | sh</div>
<div style={{ color:C.green }}>ollama serve</div>
<div style={{ color:C.green, marginTop:6 }}>ollama pull llama3</div>
</div>
)}
</div>

{/* Editor prefs */}
<div className="card fadeUp d2" style={{ padding:18, marginBottom:14 }}>
<div style={{ fontWeight:700, fontSize:13, marginBottom:14 }}>⚙️ Editor Preferences</div>
{[
['autosave', 'Auto-save files', 'Save on every change automatically'],
['linting', 'Code linting', 'Highlight syntax errors in real-time'],
['vim', 'Vim keybindings', 'Use Vim motions in the code editor'],
['telemetry','Send usage data', 'Help improve Domvia (anonymous)'],
].map(([k,label,desc]) => (
<div key={k} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'10px 0', borderBottom:`1px solid ${C.border}` }}>
<div>
<div style={{ fontSize:12, fontWeight:500, marginBottom:1 }}>{label}</div>
<div style={{ fontSize:10, color:C.textMuted }}>{desc}</div>
</div>
<div className={`sw ${sw[k]?'on':''}`} onClick={()=>{ setSw(p=>({...p,[k]:!p[k]})); toast(`${label} ${!sw[k]?'enabled':'disabled'}`, 'info') }}/>
</div>
))}
</div>

{/* About */}
<div className="card fadeUp d3" style={{ padding:18 }}>
<div style={{ fontWeight:700, fontSize:13, marginBottom:12 }}>◈ About Domvia</div>
<div style={{ display:'flex', flexDirection:'column', gap:7, fontSize:12, color:C.textMuted }}>
{[['Version','1.0.0'],['AI Engine','Ollama (local)'],['Framework','Electron + React + Vite'],['Platform','Linux · Windows · macOS'],['License','Private']].map(([k,v]) => (
<div key={k} style={{ display:'flex', justifyContent:'space-between' }}>
<span>{k}</span><span style={{ color:C.textSoft, fontWeight:500 }}>{v}</span>
</div>
))}
</div>
</div>
</div>
)
}

// ── NEW PROJECT MODAL ─────────────────────────────────────────────────────────
function NewProjectModal({ onClose, onCreate }) {
const [name, setName] = useState('')
const [icon, setIcon] = useState('📊')
const [color, setColor] = useState(C.accent)

const ICONS = ['📊','👥','📦','🎫','🛒','📝','🔐','📅','🎨','⚡','🌐','🤖']
const COLORS = [C.accent, C.green, C.orange, C.pink, '#F59E0B', '#06B6D4', '#8B5CF6', '#EC4899']

return (
<Modal title="New Project" onClose={onClose}>
<div style={{ display:'flex', flexDirection:'column', gap:14 }}>
<div style={{ display:'flex', gap:10, alignItems:'center', padding:'14px', background:C.surface, borderRadius:10, border:`1px solid ${color}40` }}>
<div style={{ width:44, height:44, borderRadius:12, background:`${color}18`, border:`1px solid ${color}30`, display:'flex', alignItems:'center', justifyContent:'center', fontSize:24 }}>{icon}</div>
<div>
<div style={{ fontWeight:600, fontSize:14 }}>{name || 'Project Name'}</div>
<div style={{ fontSize:11, color:C.textMuted }}>New project</div>
</div>
</div>
<div><label>Project Name</label><input placeholder="My Awesome App" value={name} onChange={e=>setName(e.target.value)} autoFocus/></div>
<div>
<label>Icon</label>
<div style={{ display:'flex', gap:6, flexWrap:'wrap' }}>
{ICONS.map(ic => (
<div key={ic} style={{ width:34, height:34, borderRadius:8, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', fontSize:18, background:icon===ic?`${color}20`:C.surface, border:`1px solid ${icon===ic?color:C.border}`, transition:'all 0.12s' }}
onClick={()=>setIcon(ic)}>{ic}</div>
))}
</div>
</div>
<div>
<label>Color</label>
<div style={{ display:'flex', gap:7 }}>
{COLORS.map(c => (
<div key={c} style={{ width:24, height:24, borderRadius:'50%', background:c, cursor:'pointer', border:`2px solid ${color===c?'#fff':'transparent'}`, transition:'transform 0.12s', transform:color===c?'scale(1.2)':'scale(1)' }}
onClick={()=>setColor(c)}/>
))}
</div>
</div>
<div style={{ display:'flex', gap:8, marginTop:4 }}>
<button className="btn btn-primary" style={{ flex:1, justifyContent:'center' }} disabled={!name.trim()} onClick={()=>onCreate(name,icon,color)}>
Create Project
</button>
<button className="btn btn-ghost" onClick={onClose}>Cancel</button>
</div>
</div>
</Modal>
)
}
     
 
what is notes.io
 

Notes is a web-based application for online taking notes. You can take your notes and share with others people. If you like taking long notes, notes.io is designed for you. To date, over 8,000,000,000+ notes created and continuing...

With notes.io;

  • * You can take a note from anywhere and any device with internet connection.
  • * You can share the notes in social platforms (YouTube, Facebook, Twitter, instagram etc.).
  • * You can quickly share your contents without website, blog and e-mail.
  • * You don't need to create any Account to share a note. As you wish you can use quick, easy and best shortened notes with sms, websites, e-mail, or messaging services (WhatsApp, iMessage, Telegram, Signal).
  • * Notes.io has fabulous infrastructure design for a short link and allows you to share the note as an easy and understandable link.

Fast: Notes.io is built for speed and performance. You can take a notes quickly and browse your archive.

Easy: Notes.io doesn’t require installation. Just write and share note!

Short: Notes.io’s url just 8 character. You’ll get shorten link of your note when you want to share. (Ex: notes.io/q )

Free: Notes.io works for 14 years and has been free since the day it was started.


You immediately create your first note and start sharing with the ones you wish. If you want to contact us, you can use the following communication channels;


Email: [email protected]

Twitter: http://twitter.com/notesio

Instagram: http://instagram.com/notes.io

Facebook: http://facebook.com/notesio



Regards;
Notes.io Team

     
 
Shortened Note Link
 
 
Looding Image
 
     
 
Long File
 
 

For written notes was greater than 18KB Unable to shorten.

To be smaller than 18KB, please organize your notes, or sign in.