Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1bd474f
feat(outpost): unified Tasks surface, rail 7 to 6
IBondarenko-iwg Jul 10, 2026
70df273
feat(outpost): re-home context panels into panes
IBondarenko-iwg Jul 10, 2026
9f04b92
feat(outpost): Overview cockpit hosts controls, session, git and panic
IBondarenko-iwg Jul 10, 2026
1e842e3
chore(outpost): remove contextual sidebar chassis
IBondarenko-iwg Jul 10, 2026
6fe2c17
feat(outpost): drop installed-workflow pills from topbar
IBondarenko-iwg Jul 13, 2026
d9d544c
style(mothership): restyle dashboard tabs as filter chips
IBondarenko-iwg Jul 13, 2026
889a694
test(e2e): cover Tasks surface chips, detail panel and action guard
IBondarenko-iwg Jul 13, 2026
a60861c
style(shell): center and widen the topbar search pill
IBondarenko-iwg Jul 13, 2026
83cd5c9
feat(outpost): tasks surface and pane layout polish
IBondarenko-iwg Jul 13, 2026
dce6335
feat(outpost): decision status counts become filter chips
IBondarenko-iwg Jul 13, 2026
1565312
feat(shell): dispatch ticker feed mode - scroll, pause, reduced-motio…
IBondarenko-iwg Jul 17, 2026
1aac280
feat(server): mothership ticker feed derived from instance polling
IBondarenko-iwg Jul 17, 2026
475cba1
feat(outpost): stakeholder persona type on tasks, product and decisio…
IBondarenko-iwg Jul 17, 2026
0357057
feat(outpost): regroup theme presets - Default, Calm, collapsible Cla…
IBondarenko-iwg Jul 17, 2026
604128d
feat(settings): reduce-motion override toggle
IBondarenko-iwg Jul 17, 2026
23a6d99
test(e2e): ticker feed and reduce-motion specs
IBondarenko-iwg Jul 17, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@
</div>
</main>

<footer class="shell-ticker">
@* Ticker starts mode-less (static) — dashboard.js switches to the feed once
lines exist; pages without the ticker script keep a working footer. *@
<footer class="shell-ticker" id="shell-ticker" role="button" tabindex="0"
aria-label="Dispatch ticker — click to toggle between summary and event feed">
<span class="shell-ticker-line" id="last-refresh">Loading...</span>
<div class="shell-ticker-feed" aria-live="off"><div class="shell-ticker-feed-track" id="ticker-feed-track"></div></div>
<span class="shell-ticker-byline">Dotbot Dashboard v1.0</span>
</footer>
</div>
Expand Down
21 changes: 9 additions & 12 deletions src/server-dotnet/src/Dotbot.Server/wwwroot/css/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -89,25 +89,22 @@ body {
margin-left: auto;
}

/* === Tab Bar === */
/* === View filter chips (#606 — sentence-case chips replace uppercase tabs,
* design spec #10; same switching JS, restyle only) === */
.tab-bar-container {
display: flex;
gap: 0;
padding: 0 20px;
background: var(--bezel-dark);
border-bottom: 1px solid var(--bezel-edge);
gap: 6px;
padding: 12px 20px 12px 0;
flex-shrink: 0;
}
.tab {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 10px 20px;
padding: 3px 12px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
border: 1px solid var(--bezel-edge);
border-radius: 3px;
color: var(--color-muted);
cursor: pointer;
transition: all 150ms ease;
Expand All @@ -118,8 +115,8 @@ body {
}
.tab.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
text-shadow: 0 0 8px var(--primary-30);
border-color: var(--color-primary);
background: var(--primary-10);
}

/* === Main Content ===
Expand Down
122 changes: 122 additions & 0 deletions src/server-dotnet/src/Dotbot.Server/wwwroot/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
wireTabSwitching();
wireFilters();
wireSorting();
initTicker();
fetchData();
refreshTimer = setInterval(fetchData, 30000);
});
Expand All @@ -83,6 +84,7 @@
}
allInstances = await resp.json();
buildDerivedData();
buildTickerLines();
render();
updateRefreshTime();
} catch (err) {
Expand Down Expand Up @@ -845,4 +847,124 @@
const el = document.getElementById('last-refresh');
el.textContent = `Last refresh: ${formatDashboardTime(new Date())}`;
}

// --- Dispatch ticker (#607 interim: derived from instance polling;
// replaced by the #599 outpost-to-mothership event feed) ---
const TICKER_MODE_KEY = 'dotbot:shell:tickerMode';
const TICKER_MIN_CYCLE_S = 20;
const TICKER_PX_PER_S = 60;
let tickerLines = [];
let tickerRendered = '';
let tickerRebuildQueued = false;
// Explicit click choice — authoritative over localStorage so the stored
// read can't force feed mode back on when storage is unavailable.
let userTickerMode = null;

function initTicker() {
const el = document.getElementById('shell-ticker');
if (!el) return;
el.addEventListener('click', () => {
const feed = el.dataset.mode === 'feed';
if (feed) {
delete el.dataset.mode;
} else {
el.dataset.mode = 'feed';
}
userTickerMode = feed ? 'static' : 'feed';
try { localStorage.setItem(TICKER_MODE_KEY, userTickerMode); } catch { /* ignore */ }
tickerRendered = '';
renderTicker();
el.blur();
});
el.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); }
});
window.matchMedia('(prefers-reduced-motion: reduce)')
.addEventListener('change', () => { tickerRendered = ''; renderTicker(); });
}

function buildTickerLines() {
const recent = [...allInstances]
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.slice(0, 8);
tickerLines = recent.map(inst => {
const title = `"${inst.questionTitle}"`;
if (inst.respondedCount >= inst.totalRecipients && inst.totalRecipients > 0) {
return `question ${title} responded — M`;
}
if (inst.respondedCount > 0) {
return `question ${title} ${inst.respondedCount}/${inst.totalRecipients} responded · stop`;
}
return `question ${title} dispatched · stop`;
});
tickerLines.push(`refreshed ${formatDashboardTime(new Date())} · stop`);

// Expanded is the dashboard default (Decision #3) — set behaviourally,
// not in markup, so shared-layout pages without this script keep a
// working static footer. Respect a stored user preference.
const el = document.getElementById('shell-ticker');
if (el && !el.dataset.mode && tickerLines.length > 1 && userTickerMode !== 'static') {
let stored = null;
try { stored = localStorage.getItem(TICKER_MODE_KEY); } catch { /* ignore */ }
if (stored !== 'static') el.dataset.mode = 'feed';
}
queueTickerRender();
}

/** Defer rebuilds to the animation loop boundary (translateX is back at 0
* there, so replaceChildren is seamless) — a mid-cycle rebuild every 30s
* fetch would snap the scroll and the feed tail would never be seen. */
function queueTickerRender() {
const el = document.getElementById('shell-ticker');
const track = document.getElementById('ticker-feed-track');
if (!el || !track || el.dataset.mode !== 'feed') return;
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduced || !track.childElementCount) {
renderTicker();
return;
}
if (tickerRebuildQueued) return;
tickerRebuildQueued = true;
track.addEventListener('animationiteration', () => {
tickerRebuildQueued = false;
renderTicker();
}, { once: true });
}

function renderTicker() {
const el = document.getElementById('shell-ticker');
const track = document.getElementById('ticker-feed-track');
if (!el || !track || el.dataset.mode !== 'feed') return;

const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const content = tickerLines.join(' · ') || 'no transmissions. standing by.';
const signature = `${reduced ? 'R' : 'S'}|${content}`;
if (signature === tickerRendered) return;
tickerRendered = signature;

track.replaceChildren();
track.style.removeProperty('--ticker-duration');

const addEntry = (text) => {
const span = document.createElement('span');
span.className = 'shell-ticker-entry'; // same DOM as ticker.js appendEntry
span.textContent = text; // textContent — titles are user content (XSS)
track.appendChild(span);
};

if (reduced) {
addEntry(tickerLines[0] || content);
return;
}

const containerWidth = el.querySelector('.shell-ticker-feed').clientWidth || 800;
addEntry(content);
while (track.scrollWidth < containerWidth) addEntry(content);
const blockCount = track.childElementCount;
for (let i = 0; i < blockCount; i++) {
track.appendChild(track.children[i].cloneNode(true));
}
const duration = Math.max(TICKER_MIN_CYCLE_S, (track.scrollWidth / 2) / TICKER_PX_PER_S);
track.style.setProperty('--ticker-duration', `${duration}s`);
}
})();
83 changes: 72 additions & 11 deletions src/shared/css/dotbot-shell.css
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/* DOTBOT Shared Navigation Shell
* One shell, many rooms: the v4 navigation frame shared by Mothership and Outpost.
* Spec: docs/DOTBOT-v4-DESIGN-IDEAS.md §7 — 44px top bar · 56px icon rail (pinnable
* Spec: docs/DOTBOT-v4-DESIGN-IDEAS.md #7 — 44px top bar · 56px icon rail (pinnable
* to 200px labelled mode) · 28px dispatch ticker · content capped at 1280px.
*
* Consumption order: dotbot-tokens.css → dotbot-crt.css → dotbot-shell.css
* Shell chrome only — no page-content components live here.
* Shell chrome only — no page-content components live here. One deliberate
* exception: the persona type base rule (#607), scoped under .shell so it
* cannot leak into non-shell consumers (studio-ui imports this file).
* Pure token consumer: zero raw colors, so all theme presets work via the
* runtime --color-*-rgb injection (src/ui/static/modules/theme.js).
*
* Deferred hooks:
* Ticker scroll animation + pause/expand behaviour → #607
* Command palette overlay → #553
* Persona type split (data-persona) → #607
* Responsive/mobile pass below 1200px → #552 (content cap goes
* fluid via minmax below; rail/topbar geometry intentionally unchanged)
*/
Expand All @@ -25,7 +25,7 @@
--shell-rail-w-pinned: 200px;
--shell-ticker-h: 28px;
--shell-content-max: 1280px;
/* Scanline intensity: 6% on chrome, 0% on content (spec §11) */
/* Scanline intensity: 6% on chrome, 0% on content (spec #11) */
--shell-scanline-alpha: 0.06;

display: grid;
Expand All @@ -46,7 +46,7 @@
}

/* The shell owns its own chrome texture; suppress the global crt.css overlay
* so content panels stay at 0% (spec §11). No-op until .shell markup exists. */
* so content panels stay at 0% (spec #11). No-op until .shell markup exists. */
body:has(.shell)::after {
content: none;
}
Expand Down Expand Up @@ -112,14 +112,19 @@ body:has(.shell)::after {
color: var(--color-primary);
}

/* Search pill — the visible palette affordance. Behaviour lands with #553. */
/* Search pill — the visible palette affordance (behaviour lands with #553).
* Centered mid-bar per the spec #7.3 sketch; clamp keeps it prominent on wide
* screens and lets it compress before identity values would truncate. */
.shell-search {
display: flex;
align-items: center;
gap: 8px;
flex: 0 1 clamp(180px, 24vw, 340px);
min-width: 140px;
margin-left: auto;
margin-right: auto;
padding: 5px 12px;
min-width: 220px;
overflow: hidden;
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-muted);
Expand Down Expand Up @@ -276,8 +281,9 @@ body:has(.shell)::after {
}

/* === Dispatch ticker (28px) ============================================
* Geometry + typography only. Scroll animation, pause-on-hover, collapse
* behaviour and event wiring land with #607. */
* Two modes (#607): static (default when no data-mode attribute — harness
* and JS-free consumers keep rendering the summary line) and feed (the
* scrolling dispatch log). Click toggles; app JS owns feed content. */

.shell-ticker {
grid-area: ticker;
Expand All @@ -292,6 +298,15 @@ body:has(.shell)::after {
font-size: 11px;
letter-spacing: 0.05em;
color: var(--color-muted);
cursor: pointer;
}

.shell-ticker[data-mode="feed"] .shell-ticker-line {
display: none;
}

.shell-ticker:not([data-mode="feed"]) .shell-ticker-feed {
display: none;
}

.shell-ticker-line {
Expand All @@ -300,6 +315,33 @@ body:has(.shell)::after {
text-overflow: ellipsis;
}

.shell-ticker-feed {
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
}

.shell-ticker-feed-track {
display: inline-flex;
gap: 64px;
padding-right: 64px;
will-change: transform;
animation: ticker-scroll var(--ticker-duration, 30s) linear infinite;
}

/* Pause in place — never reset position on hover/focus (spec: "stops on
* focus" read as stops moving; animation:none would snap to the start) */
.shell-ticker:hover .shell-ticker-feed-track,
.shell-ticker:focus-within .shell-ticker-feed-track {
animation-play-state: paused;
}

@keyframes ticker-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}

.shell-ticker-byline {
margin-left: auto;
flex: 0 0 auto;
Expand All @@ -308,7 +350,9 @@ body:has(.shell)::after {

/* === Reduced motion ====================================================
* First prefers-reduced-motion implementation in the codebase (#551
* Decision #7): keep color and shape, drop the movement. */
* Decision #7): keep color and shape, drop the movement. The body
* attribute variant is the in-app "Reduce motion" settings override —
* identical scope to the OS flag, zero exceptions. */

@media (prefers-reduced-motion: reduce) {
.shell *,
Expand All @@ -319,6 +363,23 @@ body:has(.shell)::after {
}
}

body[data-motion="reduced"] .shell *,
body[data-motion="reduced"] .shell *::before,
body[data-motion="reduced"] .shell *::after {
transition: none !important;
animation: none !important;
}

/* === Persona type split (#607, PRD 5.7) ================================
* Stakeholder surfaces read in the UI face at 14px; data values re-mono
* per app (each app's own stylesheet lists its data classes). Operator
* surfaces keep the 13px mono base — no attribute, no rules. */

.shell [data-persona="stakeholder"] {
font-family: var(--font-ui);
font-size: 14px;
}

/* === Responsive ========================================================
* Below the content cap the inner column goes fluid; rail and topbar
* geometry stay fixed. Full mobile pass is #552 scope. */
Expand Down
2 changes: 1 addition & 1 deletion src/shared/css/harness.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ <h1 style="font-size:18px; letter-spacing:0.05em;">SHELL HARNESS</h1>
</div>

<script>
// Rail item sets per app variant (glyphs from the design spec §7/§8/§9)
// Rail item sets per app variant (glyphs from the design spec #7/#8/#9)
var RAILS = {
mothership: [
{ icon: '□', label: 'Fleet' },
Expand Down
Loading
Loading