Skip to content
Open
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
250 changes: 214 additions & 36 deletions dashboard/ringside.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,12 @@
line-height: 1.5;
margin: 0;
}
button, select {
button {
color: inherit;
font: inherit;
cursor: pointer;
}
button { cursor: pointer; }
button:focus-visible,
select:focus-visible,
iframe:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
Expand Down Expand Up @@ -176,10 +175,13 @@
}
.artifact-tools button:hover { border-color: var(--accent); }
.artifact-tools button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
select,
.artifact-tools select {
appearance: none;
-webkit-appearance: none;
.dd {
position: relative;
display: grid;
min-width: 0;
max-width: 100%;
}
.dd-trigger {
padding: 7px 30px 7px 12px;
border: 1px solid var(--hairline);
border-radius: 7px;
Expand All @@ -190,9 +192,49 @@
color: var(--ink);
font-size: 13px;
line-height: 1.3;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.dd-trigger:disabled { opacity: .55; cursor: default; }
.dd-menu {
position: absolute;
top: calc(100% + 5px);
left: 0;
z-index: 40;
min-width: 100%;
max-width: min(92vw, 640px);
padding: 4px;
background: var(--surface);
border: 1px solid var(--hairline);
border-radius: 9px;
box-shadow: 0 14px 34px rgba(9, 14, 22, .28);
overflow-y: auto;
overscroll-behavior: contain;
}
.dd-menu:focus { outline: none; }
.dd-menu.align-right { left: auto; right: 0; }
.dd-option {
position: relative;
padding: 7px 12px 7px 28px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.dd-option.active { background: var(--hairline); }
.dd-option[aria-selected="true"]::before {
content: "✓";
position: absolute;
left: 9px;
color: var(--accent);
font-weight: 700;
}
select:focus-visible,
summary.run-head:focus-visible,
.run-switch:focus-visible {
outline: 2px solid var(--accent);
Expand Down Expand Up @@ -523,18 +565,17 @@
color: var(--muted);
font-size: 13px;
}
.artifact-tools select,
.artifact-tools .dd-trigger,
.artifact-tools button {
min-height: 30px;
background: var(--surface);
background-color: var(--surface);
border: 1px solid var(--hairline);
border-radius: 6px;
color: var(--ink);
font-size: 12px;
}
.artifact-tools select {
min-width: 0;
padding: 0 8px;
.artifact-tools .dd-trigger {
padding: 0 28px 0 10px;
}
.artifact-tools button {
padding: 0 10px;
Expand Down Expand Up @@ -605,9 +646,9 @@
<div class="artifact-layout">
<section class="artifact-main" aria-label="Artifact preview">
<div class="artifact-tools">
<select id="artifact-picker" class="mono" aria-label="Artifact"></select>
<div id="artifact-picker" class="dd mono" data-label="Artifact"></div>
<div id="artifact-status" class="artifact-status">No artifact selected.</div>
<select id="artifact-version" class="mono" aria-label="Artifact version"></select>
<div id="artifact-version" class="dd mono" data-label="Artifact version"></div>
<button id="open-folder" type="button" title="Open this run's files in Finder">Open folder</button>
</div>
<div id="frame-wrap" class="frame-wrap">
Expand Down Expand Up @@ -657,9 +698,9 @@
updateMessage: document.getElementById("update-message"),
dismissUpdate: document.getElementById("dismiss-update"),
runs: document.getElementById("runs"),
artifactPicker: document.getElementById("artifact-picker"),
artifactPicker: createDropdown(document.getElementById("artifact-picker"), pickArtifact),
artifactStatus: document.getElementById("artifact-status"),
artifactVersion: document.getElementById("artifact-version"),
artifactVersion: createDropdown(document.getElementById("artifact-version"), pickVersion),
frameWrap: document.getElementById("frame-wrap")
};

Expand Down Expand Up @@ -910,18 +951,156 @@
while (have.childNodes.length > desired.length) have.lastChild.remove();
}

function syncSelect(select, desired, value) {
while (select.options.length > desired.length) select.remove(select.options.length - 1);
desired.forEach((want, index) => {
let option = select.options[index];
if (!option) {
option = document.createElement("option");
select.add(option);
// Native <select> popups on macOS anchor the checked option at the
// control, so a long run list spills off the top of the screen. This
// listbox always opens downward, capped to the space below the trigger.
function createDropdown(root, onChange) {
const trigger = document.createElement("button");
trigger.type = "button";
trigger.className = "dd-trigger";
trigger.setAttribute("aria-haspopup", "listbox");
trigger.setAttribute("aria-expanded", "false");
const menu = document.createElement("div");
menu.className = "dd-menu";
menu.setAttribute("role", "listbox");
menu.tabIndex = -1;
menu.hidden = true;
if (root.dataset.label) {
trigger.setAttribute("aria-label", root.dataset.label);
menu.setAttribute("aria-label", root.dataset.label);
}
root.append(trigger, menu);

let options = [];
let value = "";
let activeIndex = -1;
let typeBuffer = "";
let typeStamp = 0;

function renderTrigger() {
const current = options.find(option => option.value === value) || options[0];
const text = current ? current.text : "";
if (trigger.textContent !== text) trigger.textContent = text;
}

function renderMenu() {
while (menu.children.length > options.length) menu.lastChild.remove();
options.forEach((want, index) => {
let node = menu.children[index];
if (!node) {
node = document.createElement("div");
node.className = "dd-option";
node.setAttribute("role", "option");
node.id = `${root.id}-opt-${index}`;
menu.appendChild(node);
}
if (node.textContent !== want.text) node.textContent = want.text;
node.setAttribute("aria-selected", String(want.value === value));
node.classList.toggle("active", index === activeIndex);
});
}

function setActive(index, scroll) {
if (!options.length) return;
activeIndex = Math.max(0, Math.min(options.length - 1, index));
[...menu.children].forEach((node, i) => node.classList.toggle("active", i === activeIndex));
menu.setAttribute("aria-activedescendant", `${root.id}-opt-${activeIndex}`);
if (scroll) menu.children[activeIndex]?.scrollIntoView({block: "nearest"});
}

function openMenu() {
if (trigger.disabled || !options.length || !menu.hidden) return;
const room = window.innerHeight - trigger.getBoundingClientRect().bottom - 18;
menu.style.maxHeight = Math.max(160, room) + "px";
menu.hidden = false;
trigger.setAttribute("aria-expanded", "true");
menu.classList.remove("align-right");
if (menu.getBoundingClientRect().right > window.innerWidth - 8) menu.classList.add("align-right");
const selected = options.findIndex(option => option.value === value);
setActive(selected >= 0 ? selected : 0, true);
menu.focus({preventScroll: true});
}

function closeMenu(refocus) {
if (menu.hidden) return;
menu.hidden = true;
trigger.setAttribute("aria-expanded", "false");
if (refocus) trigger.focus();
}

function commit(index) {
const option = options[index];
closeMenu(true);
if (!option || option.value === value) return;
value = option.value;
renderTrigger();
renderMenu();
onChange(value);
}

trigger.addEventListener("click", () => (menu.hidden ? openMenu() : closeMenu()));
trigger.addEventListener("keydown", event => {
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
openMenu();
}
});
menu.addEventListener("keydown", event => {
if (event.key === "ArrowDown") { event.preventDefault(); setActive(activeIndex + 1, true); }
else if (event.key === "ArrowUp") { event.preventDefault(); setActive(activeIndex - 1, true); }
else if (event.key === "Home") { event.preventDefault(); setActive(0, true); }
else if (event.key === "End") { event.preventDefault(); setActive(options.length - 1, true); }
else if (event.key === "Enter" || event.key === " ") { event.preventDefault(); commit(activeIndex); }
else if (event.key === "Escape") { event.preventDefault(); closeMenu(true); }
else if (event.key === "Tab") { closeMenu(); }
else if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
const clean = text => text.replace("● ", "").toLowerCase();
const now = Date.now();
typeBuffer = (now - typeStamp < 600 ? typeBuffer : "") + event.key.toLowerCase();
typeStamp = now;
const from = typeBuffer.length > 1 ? activeIndex : activeIndex + 1;
let hit = options.findIndex((option, index) => index >= from && clean(option.text).startsWith(typeBuffer));
if (hit < 0) hit = options.findIndex(option => clean(option.text).startsWith(typeBuffer));
if (hit >= 0) setActive(hit, true);
}
if (option.value !== want.value) option.value = want.value;
if (option.textContent !== want.text) option.textContent = want.text;
});
if (value !== undefined && select.value !== value) select.value = value;
menu.addEventListener("mousemove", event => {
const node = event.target.closest('[role="option"]');
if (!node) return;
const index = [...menu.children].indexOf(node);
if (index >= 0 && index !== activeIndex) setActive(index, false);
});
menu.addEventListener("click", event => {
const node = event.target.closest('[role="option"]');
if (node) commit([...menu.children].indexOf(node));
});
root.addEventListener("focusout", event => {
if (!root.contains(event.relatedTarget)) closeMenu();
});
document.addEventListener("pointerdown", event => {
if (!menu.hidden && !root.contains(event.target)) closeMenu();
});

return {
sync(desired, desiredValue) {
const activeValue = activeIndex >= 0 ? options[activeIndex]?.value : undefined;
options = desired;
if (desiredValue !== undefined) value = desiredValue;
if (!menu.hidden) {
if (!options.length) closeMenu();
else {
const keep = options.findIndex(option => option.value === activeValue);
activeIndex = keep >= 0 ? keep : Math.min(Math.max(activeIndex, 0), options.length - 1);
}
}
renderTrigger();
renderMenu();
},
set disabled(flag) {
trigger.disabled = flag;
if (flag) closeMenu();
}
};
}

function sizeFrame(frame) {
Expand Down Expand Up @@ -1532,15 +1711,15 @@
};
});
}
syncSelect(els.artifactPicker, options, state.artifacts.length ? state.artifactName : "");
els.artifactPicker.sync(options, state.artifacts.length ? state.artifactName : "");
els.artifactPicker.disabled = !state.artifacts.length;
}

function renderArtifactControls() {
const artifact = selectedArtifact();
if (!artifact) {
els.artifactStatus.textContent = state.libraryError ? "reconnecting…" : "No artifact selected.";
syncSelect(els.artifactVersion, []);
els.artifactVersion.sync([]);
els.artifactVersion.disabled = true;
return;
}
Expand All @@ -1564,7 +1743,7 @@
text: `Round ${total - index} — ${outcome} · ${formatVersionTime(version.finished_at)}`
});
});
syncSelect(els.artifactVersion, options, state.artifactVersion);
els.artifactVersion.sync(options, state.artifactVersion);
els.artifactVersion.disabled = false;
}

Expand Down Expand Up @@ -1686,12 +1865,12 @@
renderRunningNow();
loadArtifactFrame(false);
}
els.artifactVersion.addEventListener("change", () => {
state.artifactVersion = els.artifactVersion.value || "live";
function pickVersion(version) {
state.artifactVersion = version || "live";
state.frameKey = "";
renderArtifactControls();
loadArtifactFrame(true);
});
}
document.getElementById("open-folder").addEventListener("click", () => {
const artifact = selectedArtifact();
if (!artifact) return;
Expand All @@ -1702,8 +1881,7 @@
fetch(`/api/open-folder?artifact=${encodeURIComponent(artifact.name)}&run=${encodeURIComponent(runId)}`).catch(() => {});
});

els.artifactPicker.addEventListener("change", () => {
const name = els.artifactPicker.value;
function pickArtifact(name) {
if (!name) return;
state.artifactName = name;
state.artifactManual = true;
Expand All @@ -1716,7 +1894,7 @@
renderRunningNow();
renderLive();
loadArtifactFrame(true);
});
}
document.addEventListener("keydown", event => {
if (event.key === "Escape" && state.expanded.size) closeWorker();
});
Expand Down
Loading