Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions create-katalystwp/server/ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ function SessionItem({ s, selectedId, onSelect, onDelete, onArchive, onRestore,
</div>`;
}

function Sidebar({ sessions, envs, selectedId, now, onSelect, onNewEnv, onEnvAction, onSettings, onHealth, onDeleteSession, onArchiveSession, onRestoreSession }) {
function Sidebar({ sessions, envs, selectedId, now, onSelect, onNewEnv, onEnvAction, onSettings, onHealth, onCloseDrawer, onDeleteSession, onArchiveSession, onRestoreSession }) {
const [expanded, setExpanded] = useState(() => new Set());
// Which envs currently have their "Archived (N)" reveal expanded.
const [archOpen, setArchOpen] = useState(() => new Set());
Expand Down Expand Up @@ -177,6 +177,7 @@ function Sidebar({ sessions, envs, selectedId, now, onSelect, onNewEnv, onEnvAct
<div class="side-head-btns">
<button class="btn small ghost" onClick=${onHealth} title="System health">📊</button>
<button class="btn small ghost" onClick=${onSettings} title="Settings">⚙</button>
<button class="mobile-only btn small ghost" onClick=${onCloseDrawer} title="Close">✕</button>
</div>
</div>
<div class="side-section grow">
Expand Down Expand Up @@ -252,7 +253,7 @@ function Bubble({ it }) {
return html`<div class="raw"><pre>${clipText(it.text)}</pre></div>`;
}

function SessionView({ session, now, onChanged, onBack, onDelete, onArchive, onRestore }) {
function SessionView({ session, now, onChanged, onMenu, onDelete, onArchive, onRestore }) {
const [items, setItems] = useState([]);
const [partial, setPartial] = useState('');
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -348,7 +349,7 @@ function SessionView({ session, now, onChanged, onBack, onDelete, onArchive, onR
<section class="main">
<header class="bar">
<div class="bar-title">
<button class="back btn small ghost" onClick=${onBack} title="Back to list">‹</button>
<button class="mobile-only btn small ghost" onClick=${onMenu} title="Environments & sessions">☰</button>
<${StatusDot} status=${session.status} />
${editing
? html`<input class="rename" value=${draft}
Expand Down Expand Up @@ -1120,6 +1121,10 @@ function App() {
const [envs, setEnvs] = useState([]);
const [presets, setPresets] = useState([]);
const [selectedId, setSelectedId] = useState(null);
// On phones the sidebar is an off-canvas drawer (see the mobile media query).
// It starts open there so first load shows the env/session list, not an
// empty pane; on desktop the class is inert (sidebar is always visible).
const [drawerOpen, setDrawerOpen] = useState(() => window.matchMedia('(max-width: 760px)').matches);
const [newSession, setNewSession] = useState(null); // null | { preselect? }
const [showNewEnv, setShowNewEnv] = useState(false);
const [logEnvId, setLogEnvId] = useState(null);
Expand Down Expand Up @@ -1243,14 +1248,18 @@ function App() {
};

return html`
<div class=${`layout ${selected ? 'has-selection' : ''}`}>
<div class=${`layout ${drawerOpen ? 'drawer-open' : ''}`}>
<${Sidebar} sessions=${sessions} envs=${envs} selectedId=${selectedId} now=${now}
onSelect=${setSelectedId} onNewEnv=${() => setShowNewEnv(true)}
onSelect=${(id) => { setSelectedId(id); setDrawerOpen(false); }} onNewEnv=${() => setShowNewEnv(true)}
onEnvAction=${envAction} onSettings=${() => setShowSettings(true)} onHealth=${() => setShowHealth(true)}
onCloseDrawer=${() => setDrawerOpen(false)}
onDeleteSession=${deleteSession} onArchiveSession=${archiveSession} onRestoreSession=${restoreSession} />
<div class="scrim" onClick=${() => setDrawerOpen(false)}></div>
${selected
? html`<${SessionView} session=${selected} key=${selected.id} now=${now} onChanged=${refresh} onBack=${() => setSelectedId(null)} onDelete=${() => deleteSession(selected)} onArchive=${() => archiveSession(selected)} onRestore=${() => restoreSession(selected)} />`
: html`<section class="main empty"><div class="muted">
? html`<${SessionView} session=${selected} key=${selected.id} now=${now} onChanged=${refresh} onMenu=${() => setDrawerOpen(true)} onDelete=${() => deleteSession(selected)} onArchive=${() => archiveSession(selected)} onRestore=${() => restoreSession(selected)} />`
: html`<section class="main empty">
<button class="mobile-only menu-btn btn small ghost" onClick=${() => setDrawerOpen(true)} title="Environments & sessions">☰</button>
<div class="muted">
${envs.length === 0
? html`No environments yet. <button class="btn" onClick=${() => setShowNewEnv(true)}>Create an environment</button> to begin.`
: html`Select a session, or <button class="btn" onClick=${() => setNewSession({})}>start a new one</button>.`}
Expand Down
35 changes: 21 additions & 14 deletions create-katalystwp/server/ui/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text);
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
#root, .layout { height: 100vh; }
/* dvh keeps the composer above mobile browser chrome; vh is the fallback */
#root, .layout { height: 100vh; height: 100dvh; }
.layout { display: grid; grid-template-columns: 420px 1fr; }
.muted { color: var(--muted); }
.pad { padding: 12px; }
Expand Down Expand Up @@ -83,7 +84,8 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-word; font: 12.5px/1.5
.bar-title { display: flex; align-items: center; gap: 7px; min-width: 0; }
.bar-title strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-meta { font-size: 12px; }
.back { display: none; } /* shown only on narrow screens (see media query) */
.mobile-only { display: none; } /* shown only on narrow screens (see media query) */
.scrim { display: none; } /* drawer backdrop; only exists visually on mobile */
.ssh { padding: 6px 16px; border-bottom: 1px solid var(--line); font-size: 12px; cursor: pointer; }
.ssh:hover { background: var(--panel2); }
.transcript { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 10px; }
Expand Down Expand Up @@ -128,7 +130,8 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-word; font: 12.5px/1.5
.dot.stopped { background: var(--grey); }

/* modal */
.modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; z-index: 10; }
/* z-index sits above the mobile sidebar drawer (20) so modals opened from it are usable */
.modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; z-index: 30; }
.modal { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 20px; width: 460px; max-width: 92vw; }
.modal h3 { margin: 0 0 12px; }
.modal label { display: block; margin: 12px 0 4px; font-size: 13px; color: var(--muted); }
Expand Down Expand Up @@ -212,25 +215,29 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-word; font: 12.5px/1.5

/* ---- mobile / narrow screens -------------------------------------------- */
@media (max-width: 760px) {
/* one pane visible at a time, each filling the viewport so its inner list /
transcript scrolls (a single-column grid would size panes to content). */
/* The main pane fills the viewport; the sidebar becomes an off-canvas
drawer over it (app.js toggles .drawer-open — hamburger opens, scrim /
✕ / picking a session closes). The session view stays mounted while the
drawer is open, so its live stream keeps flowing. */
.layout { display: block; }
.sidebar, .main { height: 100vh; }
.sidebar { border-right: 0; }
/* Single-column: the sidebar IS the list; selecting a session shows it
full-width with a back button. */
.main { display: none; }
.layout.has-selection .sidebar { display: none; }
.layout.has-selection .main { display: flex; }
.back { display: inline-block; }
.main { height: 100vh; height: 100dvh; }
.sidebar { position: fixed; top: 0; bottom: 0; left: 0; width: min(85vw, 340px);
height: 100vh; height: 100dvh; z-index: 20;
transform: translateX(-105%); transition: transform .22s ease; }
.layout.drawer-open .sidebar { transform: translateX(0); box-shadow: 14px 0 40px rgba(0,0,0,.55); }
.layout.drawer-open .scrim { display: block; position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 19; }
.mobile-only { display: inline-block; }
.main.empty .menu-btn { position: absolute; top: 12px; left: 12px; }

.bar { flex-wrap: wrap; gap: 4px 12px; }
.bar-meta { width: 100%; }
.ssh { white-space: nowrap; overflow-x: auto; }
.bubble, .tool, .stderr { max-width: 100%; }
.transcript { padding: 12px; }
.composer { padding: 10px 12px; }
.modal { width: 100%; }
/* ≥16px inputs stop iOS Safari from zooming the page on focus */
.composer textarea, .modal input, .modal select, .modal textarea { font-size: 16px; }
.modal { width: 100%; max-height: 92vh; max-height: 92dvh; overflow-y: auto; }
/* roomier tap targets for the env action links */
.env-actions { gap: 16px; }
.lnk { padding: 4px 0; }
Expand Down