Skip to content
Merged
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
10 changes: 8 additions & 2 deletions STARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@ not a complete Mind installation.

## Navigate before connecting organs

The instance preview includes the familiar Scientist/organ icon bar above the
overview. Scientist returns to the overview; each connected organ opens its
The instance preview uses the familiar cockpit shell: an instance-owned
repository name and `/ Cockpit` home link, a grey **IDLE** button (awaiting work),
a horizontally scrollable organ icon bar, and a selected-board toolbar. The shell
stays visible while the board content scrolls. **Reload** refreshes the selected
configured board or local view; **New tab** opens that same destination. IDLE is a
starter-shell placeholder, not an aggregated health verdict.

Scientist returns to the overview; each connected organ opens its
configured board inside the page, with an external-link fallback. Paused organs
remain navigable without resuming their feed observation.

Expand Down
34 changes: 27 additions & 7 deletions scripts/instance_navigation.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
// The shell keeps its navigation while selecting an instance-owned destination.
// Instance-owned cockpit shell. Selecting/reloading a view never activates an organ.
(() => {
const nav = document.querySelector('.instance-nav');
if (!nav) return;
const overview = document.getElementById('scientist');
const views = [...document.querySelectorAll('.instance-view')];
const links = [...nav.querySelectorAll('a')];
const heading = document.getElementById('cockpit-heading');
const status = document.getElementById('cockpit-status');
const external = document.getElementById('cockpit-external');
const scroller = nav.querySelector('.nav-scroll');
function selectedView() { return views.find(v => '#' + v.id === location.hash); }
function hint() {
nav.querySelector('.nav-hint').hidden = scroller.scrollWidth <= scroller.clientWidth;
}
function select(focus = false) {
const view = views.find(v => '#' + v.id === location.hash);
const view = selectedView();
overview.hidden = !!view;
for (const panel of views) panel.hidden = panel !== view;
const selected = view ? '#' + view.id : '#scientist';
Expand All @@ -15,28 +23,40 @@
else link.removeAttribute('aria-current');
}
const frame = view && view.querySelector('iframe');
heading.textContent = view ? view.querySelector('h1').textContent : 'Scientist';
status.textContent = view ? view.querySelector('.organ-state').textContent : 'Overview';
const local = new URL(location.href); local.hash = selected;
external.href = frame ? frame.dataset.boardSrc : local.href;
external.setAttribute('aria-label', `Open ${heading.textContent} in a new tab`);
if (frame && !frame.dataset.loaded) {
// Replace the child entry so Back/Forward belongs to the shell routes.
frame.contentWindow.location.replace(frame.dataset.boardSrc);
frame.dataset.loaded = 'true';
}
if (focus) {
const heading = view ? view.querySelector('h1') : overview.querySelector('h1');
if (heading) { heading.setAttribute('tabindex', '-1'); heading.focus({preventScroll: true}); }
window.scrollTo(0, 0);
const target = frame ? heading : (view || overview).querySelector('h1');
if (target) { target.setAttribute('tabindex', '-1'); target.focus({preventScroll: true}); }
(view || overview).scrollTop = 0;
}
nav.querySelector('[aria-current="page"]').scrollIntoView({block: 'nearest', inline: 'nearest'});
hint();
}
history.scrollRestoration = 'manual';
window.addEventListener('hashchange', () => select(true));
window.addEventListener('popstate', () => select(true));
nav.addEventListener('click', event => {
document.querySelector('.instance-shell-header').addEventListener('click', event => {
const link = event.target.closest('a');
if (!link || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
event.stopPropagation(); // The shared section-link handler must not scroll behind the shell.
event.stopPropagation(); // Shared section-link handling must not scroll the shell.
if (link.hash !== location.hash) history.pushState(null, '', link.hash);
select(true);
});
document.getElementById('cockpit-reload').addEventListener('click', () => {
const view = selectedView(), frame = view && view.querySelector('iframe');
if (frame) frame.contentWindow.location.replace(frame.dataset.boardSrc);
else location.reload();
});
window.addEventListener('resize', hint);
select();
})();
59 changes: 48 additions & 11 deletions scripts/instance_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,28 +55,65 @@ def render(instance):
f'<iframe title="{name} board" data-board-src="{url}" loading="lazy"></iframe>')
else:
content = inactive_content(organ, pending=state == 'setup')
panels.append(f'<section class="instance-view" id="organ-{key}" hidden '
panels.append(f'<section class="instance-view{' embedded-board' if state == 'boards' else ''}" id="organ-{key}" hidden '
f'aria-labelledby="organ-{key}-heading"><h1 id="organ-{key}-heading" tabindex="-1">'
f'{name}</h1><p class="organ-state">{status}</p>{content}</section>')
identity = escape(instance.get('repo') or instance.get('name') or 'Scientist')
nav = ('<nav class="instance-nav" aria-label="Organ boards"><div class="nav-scroll"><ul>'
+ ''.join(links) + '</ul></div></nav>')
+ ''.join(links) + '</ul></div><p class="nav-hint" hidden>Scroll for more boards →</p></nav>')
nav = (f'<header class="instance-shell-header"><div class="cockpit-topbar"><div class="cockpit-wrap">'
f'<a class="cockpit-home" href="#scientist">{identity} <span>/ Cockpit</span></a>'
'<button class="cockpit-idle" type="button" disabled title="Awaiting work" '
'aria-label="Idle — awaiting work"><span aria-hidden="true">●</span> IDLE</button>'
f'</div></div>{nav}</header>'
'<div class="cockpit-toolbar"><div><h1 id="cockpit-heading" tabindex="-1">Scientist</h1>'
'<p id="cockpit-status" role="status">Overview</p></div><div class="cockpit-actions">'
'<button class="cockpit-action" id="cockpit-reload" type="button" aria-label="Reload board" '
'title="Reload board">↻<span>Reload</span></button>'
'<a class="cockpit-action" id="cockpit-external" href="#scientist" target="_blank" '
'rel="noopener" aria-label="Open board in a new tab" title="Open board in a new tab">'
'↗<span>New tab</span></a></div></div>')
return nav, ''.join(panels)


CSS = """
.instance-nav{position:sticky;top:0;z-index:20;background:var(--bg);border-bottom:1px solid var(--line);margin-bottom:1rem}
.instance-nav .nav-scroll{overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:thin;padding:.45rem .5rem}
body.instance-cockpit{max-width:none;width:100%;height:100vh;height:100dvh;padding:0;margin:0;display:flex;flex-direction:column;overflow:hidden;--shell-bg:#fff;--shell-alt:#f6f6fa;--shell-text:#1c1e26;--shell-soft:#555a6e;--shell-line:#e4e4ee;--shell-brand:#7c4dff;--shell-grey:#9095a8}
@media(prefers-color-scheme:dark){body.instance-cockpit{--shell-bg:#14151a;--shell-alt:#1b1d24;--shell-text:#e8e8ef;--shell-soft:#a0a4b8;--shell-line:#2a2d38;--shell-brand:#9d7aff;--shell-grey:#6b7085}}
.instance-shell-header,.cockpit-toolbar{flex:none;background:var(--shell-bg);color:var(--shell-text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
.instance-shell-header{padding-top:env(safe-area-inset-top)}
.cockpit-topbar{border-bottom:1px solid var(--shell-line)}
.cockpit-wrap{max-width:1160px;margin:0 auto;padding:0 1rem;min-height:3.25rem;display:flex;align-items:center;justify-content:space-between;gap:.5rem;box-sizing:border-box}
.cockpit-home{color:var(--shell-text);font-size:.95rem;font-weight:750;text-decoration:none;min-height:44px;display:flex;align-items:center;gap:.3em;min-width:0;overflow-wrap:anywhere}
.cockpit-home span{color:var(--shell-soft);font-weight:600;white-space:nowrap}
.cockpit-idle{border:1px solid var(--shell-line);border-radius:8px;background:var(--shell-alt);color:var(--shell-soft);min-height:44px;padding:.35rem .65rem;font-family:inherit;font-size:.78rem;font-weight:700;letter-spacing:.04em;white-space:nowrap;opacity:.7}
.cockpit-idle span{color:var(--shell-grey);margin-right:.35em}
.instance-nav{background:var(--shell-alt);border-bottom:1px solid var(--shell-line)}
.instance-nav .nav-scroll{max-width:1160px;margin:auto;overflow-x:scroll;overscroll-behavior-x:contain;scrollbar-width:thin;scrollbar-color:var(--shell-grey) var(--shell-alt);padding:.45rem .5rem;box-sizing:border-box}
.instance-nav .nav-scroll::-webkit-scrollbar{height:8px}
.instance-nav .nav-scroll::-webkit-scrollbar-track{background:var(--shell-alt)}
.instance-nav .nav-scroll::-webkit-scrollbar-thumb{background:var(--shell-grey);border-radius:4px}
.instance-nav ul{display:flex;gap:.3rem;list-style:none;width:max-content;margin:0 auto;padding:0}
.instance-nav .board-link{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.15rem;min-width:76px;min-height:64px;padding:.4rem .65rem;border:1px solid transparent;border-radius:12px;color:var(--muted);font-size:.8rem;font-weight:650;text-decoration:none;box-sizing:border-box}
.instance-nav .board-link:hover{background:var(--panel,var(--bg));color:var(--fg)}
.instance-nav .board-link[aria-current=page]{color:var(--accent);border-color:var(--accent)}
.instance-nav .board-link{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.15rem;min-width:76px;min-height:64px;padding:.4rem .65rem;border:1px solid transparent;border-radius:12px;color:var(--shell-soft);font-size:.8rem;font-weight:650;text-decoration:none;box-sizing:border-box}
.instance-nav .board-link:hover{background:var(--shell-bg);color:var(--shell-text)}
.instance-nav .board-link[aria-current=page]{background:var(--shell-bg);color:var(--shell-brand);border-color:var(--shell-brand)}
.instance-nav .nav-icon{font-size:1.55rem;line-height:1.25}
.instance-nav :focus-visible,.instance-view :focus-visible{outline:3px solid var(--accent);outline-offset:3px}
.instance-view[hidden],#scientist[hidden]{display:none!important}
.instance-view,#scientist{scroll-margin-top:7rem}
.instance-nav .nav-hint{text-align:right;color:var(--shell-soft);font-size:.7rem;padding:0 .75rem .2rem;margin:0}
.instance-cockpit :focus-visible{outline:3px solid var(--shell-brand);outline-offset:3px}
.cockpit-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.3rem max(.65rem,env(safe-area-inset-left));border-bottom:1px solid var(--shell-line)}
.cockpit-toolbar h1{font-size:1rem;margin:0;line-height:1.5}
.cockpit-toolbar p{font-size:.75rem;color:var(--shell-soft);margin:0}
.cockpit-actions{display:flex;gap:.35rem;flex:none}
.cockpit-action{display:inline-flex;align-items:center;justify-content:center;gap:.35rem;min-width:44px;min-height:44px;padding:.35rem .6rem;border:1px solid var(--shell-line);border-radius:8px;background:var(--shell-bg);color:var(--shell-text);font:inherit;font-size:.8rem;cursor:pointer;text-decoration:none;box-sizing:border-box}
.cockpit-action:hover{border-color:var(--shell-brand);text-decoration:none}
.instance-stage{flex:1;min-height:0;min-width:0;display:flex;overflow:hidden;padding-bottom:env(safe-area-inset-bottom)}
.instance-stage>#scientist,.instance-stage>.instance-view{width:100%;max-width:var(--board-max);margin:0 auto;overflow-y:auto;box-sizing:border-box;padding:1rem var(--board-gutter) 4rem;scroll-margin:0}
.instance-stage>[hidden]{display:none!important}
.instance-view h1{font-size:1.8rem;margin-bottom:.3rem}
.organ-state{color:var(--muted)}
.instance-view textarea{width:100%;box-sizing:border-box;min-height:10rem;font:inherit;padding:.75rem;background:var(--bg);color:var(--fg);border:1px solid var(--line);border-radius:6px;resize:vertical}
button.organ-setup-copy{width:auto;height:auto;min-height:44px;padding:.5rem .8rem;margin-top:.5rem;font:inherit}
.instance-view iframe{display:block;width:100%;height:75vh;height:75dvh;min-height:360px;border:1px solid var(--line);background:var(--bg)}
.instance-stage>.embedded-board{max-width:none;padding:0;overflow:hidden;display:flex}
.embedded-board>h1,.embedded-board>p{display:none}
.instance-view iframe{display:block;flex:1;width:100%;height:100%;min-width:0;border:0;background:var(--bg)}
@media(max-width:420px){.cockpit-wrap{padding-inline:.65rem}.instance-nav .board-link{min-width:70px}.cockpit-action span{display:none}}
"""
6 changes: 3 additions & 3 deletions scripts/organism_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,16 +358,16 @@ def _render_html(snapshot: dict) -> str:
return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>{_html.escape(name)} Dashboard</title><style>{t.css(BOARD_KEY)}{_EXTRA_CSS}{navigation_css}</style></head>
<body>{navigation}{'<main id="scientist">' if instance is not None else ""}{banner}{panel}
<body{' class="instance-cockpit"' if instance is not None else ""}>{navigation}{'<div class="instance-stage">' if instance is not None else ""}{'<main id="scientist">' if instance is not None else ""}{banner}{panel}
{projects_html}
<h2 id="dashboards-heading">Dashboards</h2>
{'<p>No organs connected. Add a connection when your project needs it.</p>' if instance is not None and not snapshot.get("boards") else ''}
<div id="dashboards" class="organ-grid" aria-label="Organ summaries">{''.join(rows)}</div>
{catalogue}
<noscript><p>Publication snapshot. Open an organ for its latest evidence.</p></noscript>
{footer}
{'</main>' if instance is not None else ""}{views}
{'<noscript><style>.instance-view[hidden]{display:block!important}</style></noscript>' if instance is not None else ""}
{'</main>' if instance is not None else ""}{views}{'</div>' if instance is not None else ""}
{'<noscript><style>body.instance-cockpit{height:auto;overflow:auto}.instance-stage{display:block;overflow:visible}.instance-stage>.instance-view[hidden]{display:block!important}.embedded-board>p{display:block}.embedded-board iframe{display:none}</style></noscript>' if instance is not None else ""}
<script type="application/json" id="scientist-snapshot">{data}</script>
<script>{t.JS}</script><script>{script}</script><script>{navigation_script}</script></body></html>"""

Expand Down
Loading
Loading