Dashboards
{'No organs connected. Add a connection when your project needs it.
' if instance is not None and not snapshot.get("boards") else ''} @@ -372,8 +366,10 @@ def _render_html(snapshot: dict) -> str: {catalogue} {footer} +{'diff --git a/STARTER.md b/STARTER.md index e329935..4f2bbad 100644 --- a/STARTER.md +++ b/STARTER.md @@ -27,6 +27,19 @@ The example GitHub identity and `example.invalid` address are placeholders, not repositories or a deployed service. This example body map is a fixture, 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 +configured board inside the page, with an external-link fallback. Paused organs +remain navigable without resuming their feed observation. + +An unconnected organ opens its existing Dashboards description and setup prompt, +with a **Copy setup prompt** button. Setup-incomplete organs use the same resume +prompt as their overview card. These views share their text with Dashboards; +selecting one does not connect an organ or request its feed. Links such as +`#organ-brain` can be bookmarked, and Back/Forward switch the selected view. + ## Initialize a user-owned configuration directory If you already have a canonical Mind `repos.yaml`, the offline initializer saves diff --git a/scripts/instance_navigation.js b/scripts/instance_navigation.js new file mode 100644 index 0000000..edc4f57 --- /dev/null +++ b/scripts/instance_navigation.js @@ -0,0 +1,42 @@ +// The shell keeps its navigation while selecting an instance-owned destination. +(() => { + 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')]; + function select(focus = false) { + const view = views.find(v => '#' + v.id === location.hash); + overview.hidden = !!view; + for (const panel of views) panel.hidden = panel !== view; + const selected = view ? '#' + view.id : '#scientist'; + for (const link of links) { + if (link.hash === selected) link.setAttribute('aria-current', 'page'); + else link.removeAttribute('aria-current'); + } + const frame = view && view.querySelector('iframe'); + 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); + } + nav.querySelector('[aria-current="page"]').scrollIntoView({block: 'nearest', inline: 'nearest'}); + } + history.scrollRestoration = 'manual'; + window.addEventListener('hashchange', () => select(true)); + window.addEventListener('popstate', () => select(true)); + nav.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. + if (link.hash !== location.hash) history.pushState(null, '', link.hash); + select(true); + }); + select(); +})(); diff --git a/scripts/instance_navigation.py b/scripts/instance_navigation.py new file mode 100644 index 0000000..431f9b8 --- /dev/null +++ b/scripts/instance_navigation.py @@ -0,0 +1,82 @@ +"""Instance-owned cockpit navigation; optional organs never gain implicit URLs.""" +from html import escape + +# Match the existing cockpit's icon/label order (including Scientist first). +# This is presentation only: membership and destinations come from the profile. +ICONS = { + 'scientist': '๐ญ', 'brain': '๐ง ', 'mind': '๐ญ', 'cortex': '๐ฌ', + 'broca': '๐ฃ๏ธ', 'memory': '๐', 'eyes': '๐๏ธ', 'ears': '๐', + 'heart': 'โค๏ธ', 'hands': '๐ ๏ธ', 'pulse': 'โฑ๏ธ', 'insight': '๐ก', + 'dna': '๐งฌ', 'nerves': 'โก', 'gut': 'โป๏ธ', +} + + +def inactive_content(organ, *, pending=False): + """One verbatim description/prompt for both the overview and organ page.""" + key = organ['key'] + if pending: + prompt = (f"Resume setup for {organ['name']} in my existing Mind task. " + "Read my profile and canonical map first. Validate the owner setup and published feed, " + "then preview connect_organ.py before applying. Preserve existing records.") + label = f'Resume {key} setup' + else: + prompt = (f"Help me add {organ['name']} ({organ['role']}) to this Scientist. " + "Read my existing profile, canonical map and Mind task. Confirm the purpose, repository " + "identity and public board location; preview organ_lifecycle.py setup before applying. " + "Keep it setup incomplete until connect_organ.py validates its published feed.") + label = f'Add {key}' + note = '
Not connected. No feed requested.
' if pending else '' + return (f'{note}{escape(organ["role"])}
' + f'' + f'') + + +def render(instance): + """Return navigation and local destinations without contacting any board.""" + entries = {o['key']: (o, state) for state in ('boards', 'setup', 'available') + for o in instance.get(state, [])} + links = ['{status}
{content}Not connected. No feed requested.
' - f'{_html.escape(pending["role"])}
{_html.escape(available["role"])}
No organs connected. Add a connection when your project needs it.
' if instance is not None and not snapshot.get("boards") else ''} @@ -372,8 +366,10 @@ def _render_html(snapshot: dict) -> str: {catalogue} {footer} +{'