diff --git a/src/css/presenter/base.css b/src/css/presenter/base.css
index f2b02548..503513af 100644
--- a/src/css/presenter/base.css
+++ b/src/css/presenter/base.css
@@ -53,6 +53,7 @@ body {
}
#statusbar {
+ position: relative;
flex-shrink: 0;
background: var(--bg);
border-top: 1px solid var(--overlay);
@@ -98,11 +99,12 @@ body {
/* ── Status-bar tooltips: driven by data-tooltip (not title) so the slow, unstyled
native browser tooltip never doubles up with this one. Anchored above the bar,
- since #statusbar sits at the bottom of the viewport. ── */
-.hud-btn[data-tooltip] {
+ since #statusbar sits at the bottom of the viewport. Not scoped to .hud-btn:
+ plain status spans (#slide-info, #step-info, #ws-dot) use it too. ── */
+[data-tooltip] {
position: relative;
}
-.hud-btn[data-tooltip]::after {
+[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 8px);
@@ -121,8 +123,8 @@ body {
transition: opacity 0.1s ease;
z-index: 40;
}
-.hud-btn[data-tooltip]:hover::after,
-.hud-btn[data-tooltip]:focus-visible::after {
+[data-tooltip]:hover::after,
+[data-tooltip]:focus-visible::after {
opacity: 1;
transition-delay: 0.35s;
}
@@ -131,14 +133,27 @@ body {
opacity: 0;
transition-delay: 0s;
}
-/* Edge buttons: keep the bubble on-screen instead of centering off the bar. */
-#btn-theme[data-tooltip]::after {
+/* Edge elements: keep the bubble on-screen instead of centering off the bar. */
+#btn-theme[data-tooltip]::after,
+#btn-prev[data-tooltip]::after {
left: 0;
transform: none;
}
#log-indicator[data-tooltip]::after {
left: auto;
right: 0;
+ transform: none;
+}
+/* #ws-dot is narrower than its tooltip text, so right-aligning to its own
+ edge (like #log-indicator above) leaves the bubble hanging off to the left.
+ position:static borrows #statusbar's edge instead. */
+#ws-dot[data-tooltip] {
+ position: static;
+}
+#ws-dot[data-tooltip]::after {
+ left: auto;
+ right: 0;
+ transform: none;
}
.hud-sep {
@@ -166,6 +181,10 @@ body {
font-size: 0.85rem;
}
+#step-info {
+ margin-left: 0.6rem;
+}
+
/* ── Log indicator: present only when the deck has messages (JS sets data-level to
the highest level). Static builds export no logs, so it never appears there. The
icon shape and colour track that level; clicking re-opens the message banner. ── */
@@ -194,17 +213,28 @@ body {
display: block;
}
+/* The badge X is stroked in --bg, not a contrasting colour, so it reads as
+ a cutout in the red circle rather than a separate mark. */
#ws-dot {
- display: inline-block;
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: var(--red);
- transition: background 0.3s;
+ display: inline-flex;
+ align-items: center;
+ color: var(--subtext);
vertical-align: middle;
}
-#ws-dot.connected {
- background: var(--green);
+#ws-dot .ws-badge {
+ fill: var(--red);
+}
+#ws-dot .ws-badge-x {
+ stroke: var(--bg);
+}
+#ws-dot .ws-ic-on {
+ display: none;
+}
+#ws-dot.connected .ws-ic-off {
+ display: none;
+}
+#ws-dot.connected .ws-ic-on {
+ display: block;
}
/* ── Media zone theme-swap ────────────────────────────────────────────────── */
diff --git a/src/css/presenter/edit.css b/src/css/presenter/edit.css
index ff548d8a..0dbc6535 100644
--- a/src/css/presenter/edit.css
+++ b/src/css/presenter/edit.css
@@ -70,3 +70,14 @@
text-overflow: ellipsis;
white-space: nowrap;
}
+
+/* Keyboard-driven highlight (keyboard.ts, arrows/j/k while the menu is open) —
+ same accent/bg swap as #picker-list's own [role="option"].active. Declared
+ last so it reliably overrides the base rules above. */
+.edit-row.active {
+ background: var(--accent);
+ color: var(--bg);
+}
+.edit-row.active .edit-row-name {
+ opacity: 0.75;
+}
diff --git a/src/inkflow/bundles/presenter.css b/src/inkflow/bundles/presenter.css
index 8a66b738..34c8fceb 100644
--- a/src/inkflow/bundles/presenter.css
+++ b/src/inkflow/bundles/presenter.css
@@ -78,6 +78,7 @@ body {
height: 100%;
}
#statusbar {
+ position: relative;
flex-shrink: 0;
background: var(--bg);
border-top: 1px solid var(--overlay);
@@ -116,10 +117,10 @@ body {
[data-theme=light] .hud-btn:hover {
background: rgba(0, 0, 0, 0.08);
}
-.hud-btn[data-tooltip] {
+[data-tooltip] {
position: relative;
}
-.hud-btn[data-tooltip]::after {
+[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 8px);
@@ -138,8 +139,8 @@ body {
transition: opacity 0.1s ease;
z-index: 40;
}
-.hud-btn[data-tooltip]:hover::after,
-.hud-btn[data-tooltip]:focus-visible::after {
+[data-tooltip]:hover::after,
+[data-tooltip]:focus-visible::after {
opacity: 1;
transition-delay: 0.35s;
}
@@ -147,13 +148,23 @@ body {
opacity: 0;
transition-delay: 0s;
}
-#btn-theme[data-tooltip]::after {
+#btn-theme[data-tooltip]::after,
+#btn-prev[data-tooltip]::after {
left: 0;
transform: none;
}
#log-indicator[data-tooltip]::after {
left: auto;
right: 0;
+ transform: none;
+}
+#ws-dot[data-tooltip] {
+ position: static;
+}
+#ws-dot[data-tooltip]::after {
+ left: auto;
+ right: 0;
+ transform: none;
}
.hud-sep {
width: 1px;
@@ -176,6 +187,9 @@ body {
color: var(--text);
font-size: 0.85rem;
}
+#step-info {
+ margin-left: 0.6rem;
+}
#log-indicator:not([data-level]) {
display: none;
}
@@ -201,16 +215,25 @@ body {
display: block;
}
#ws-dot {
- display: inline-block;
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: var(--red);
- transition: background 0.3s;
+ display: inline-flex;
+ align-items: center;
+ color: var(--subtext);
vertical-align: middle;
}
-#ws-dot.connected {
- background: var(--green);
+#ws-dot .ws-badge {
+ fill: var(--red);
+}
+#ws-dot .ws-badge-x {
+ stroke: var(--bg);
+}
+#ws-dot .ws-ic-on {
+ display: none;
+}
+#ws-dot.connected .ws-ic-off {
+ display: none;
+}
+#ws-dot.connected .ws-ic-on {
+ display: block;
}
[data-inkflow-theme] {
display: block;
@@ -822,6 +845,13 @@ kbd {
text-overflow: ellipsis;
white-space: nowrap;
}
+.edit-row.active {
+ background: var(--accent);
+ color: var(--bg);
+}
+.edit-row.active .edit-row-name {
+ opacity: 0.75;
+}
/* src/css/presenter/overview.css */
#overview {
diff --git a/src/inkflow/bundles/presenter.js b/src/inkflow/bundles/presenter.js
index b7798fbf..d67e4381 100644
--- a/src/inkflow/bundles/presenter.js
+++ b/src/inkflow/bundles/presenter.js
@@ -21,6 +21,7 @@
_pickerActive: 0,
_overviewActive: 0,
_overviewCols: 1,
+ _editActive: 0,
ws: null,
windowLink: null,
_syncingFromServer: false,
@@ -323,21 +324,32 @@
editMenu.appendChild(row);
}
}
+ function editMenuSetActive(i) {
+ const rows = Array.from(
+ editMenu.querySelectorAll(".edit-row")
+ );
+ if (rows.length === 0) return;
+ state._editActive = Math.max(0, Math.min(rows.length - 1, i));
+ rows.forEach((row, idx) => {
+ row.classList.toggle("active", idx === state._editActive);
+ });
+ rows[state._editActive]?.scrollIntoView({ block: "nearest" });
+ }
+ function editMenuCommit() {
+ const files = state.slides[state.slideIndex]?.editableFiles ?? [];
+ const file = files[state._editActive];
+ if (file) actOn(file);
+ closeMenu();
+ }
function onDocClick(e) {
const t = e.target;
if (!btnEdit.contains(t) && !editMenu.contains(t)) closeMenu();
}
- function onKeydown(e) {
- if (e.key === "Escape") {
- closeMenu();
- btnEdit.focus();
- }
- }
function openMenu() {
editMenu.classList.add("open");
btnEdit.setAttribute("aria-expanded", "true");
+ editMenuSetActive(0);
document.addEventListener("click", onDocClick);
- document.addEventListener("keydown", onKeydown);
menuOpened(closeMenu);
}
function closeMenu() {
@@ -345,7 +357,6 @@
editMenu.classList.remove("open");
btnEdit.setAttribute("aria-expanded", "false");
document.removeEventListener("click", onDocClick);
- document.removeEventListener("keydown", onKeydown);
menuClosed(closeMenu);
}
function toggleMenu() {
@@ -2989,6 +3000,7 @@
let firstPositionPending = false;
state.ws.onopen = () => {
wsDot.className = "connected";
+ wsDot.dataset.tooltip = "Connected";
const assert = authoritative && sends();
firstPositionPending = assert;
if (assert) sendNav();
@@ -3030,6 +3042,7 @@
};
state.ws.onclose = () => {
wsDot.className = "";
+ wsDot.dataset.tooltip = "Disconnected";
state.ws = null;
setTimeout(() => connectWS(wsPort, true), 2e3);
};
@@ -3070,7 +3083,7 @@
const t = e.target;
if (!btnSync.contains(t) && !syncMenu.contains(t)) closeMenu2();
}
- function onKeydown2(e) {
+ function onKeydown(e) {
if (e.key === "Escape") {
closeMenu2();
btnSync.focus();
@@ -3080,7 +3093,7 @@
syncMenu.classList.add("open");
btnSync.setAttribute("aria-expanded", "true");
document.addEventListener("click", onDocClick2);
- document.addEventListener("keydown", onKeydown2);
+ document.addEventListener("keydown", onKeydown);
menuOpened(closeMenu2);
}
function closeMenu2() {
@@ -3088,7 +3101,7 @@
syncMenu.classList.remove("open");
btnSync.setAttribute("aria-expanded", "false");
document.removeEventListener("click", onDocClick2);
- document.removeEventListener("keydown", onKeydown2);
+ document.removeEventListener("keydown", onKeydown);
menuClosed(closeMenu2);
}
function toggleMenu2() {
@@ -3150,16 +3163,19 @@
clearInterval(linkPoll);
linkPoll = void 0;
}
+ var _wsPort = null;
+ function openSyncedWindow() {
+ if (_wsPort === null && state.windowLink) return;
+ const child = window.open(location.href);
+ if (!child) {
+ showNotify(POPUP_BLOCKED_MESSAGE, "yellow");
+ return;
+ }
+ if (_wsPort === null) attachLink(child);
+ }
function initWindowSync(wsPort) {
- btnPresenterView.addEventListener("click", () => {
- if (wsPort === null && state.windowLink) return;
- const child = window.open(location.href);
- if (!child) {
- showNotify(POPUP_BLOCKED_MESSAGE, "yellow");
- return;
- }
- if (wsPort === null) attachLink(child);
- });
+ _wsPort = wsPort;
+ btnPresenterView.addEventListener("click", openSyncedWindow);
if (wsPort !== null) return;
if (window.opener) attachLink(window.opener, true);
}
@@ -3740,6 +3756,7 @@
$: { action: gotoLast },
g: { action: openPicker, preventDefault: true },
o: { action: toggleOverview, preventDefault: true },
+ e: { action: toggleMenu },
f: { action: toggleFullscreen },
b: { action: () => toggleCurtain("black") },
".": { action: toggleLaser },
@@ -3756,6 +3773,7 @@
p: { action: togglePv },
d: { action: toggleLogs },
m: { action: toggleNotifyHistory },
+ n: { action: openSyncedWindow },
s: { action: cycleSyncMode }
};
var helpEl = document.getElementById("help");
@@ -3764,6 +3782,7 @@
var curtainEl = document.getElementById("curtain");
var logBannerEl = document.getElementById("log-banner");
var notifyHistoryEl2 = document.getElementById("notify-history");
+ var editMenuEl = document.getElementById("edit-menu");
document.addEventListener("keydown", (e) => {
if (helpEl.classList.contains("visible")) {
if (e.key === "?" || e.key === "Escape" || e.key === "q") {
@@ -3778,6 +3797,28 @@
}
return;
}
+ if (editMenuEl.classList.contains("open")) {
+ if (e.key === "Escape" || e.key === "q" || e.key === "e") {
+ closeMenu();
+ return;
+ }
+ if (e.key === "ArrowDown" || e.key === "j") {
+ e.preventDefault();
+ editMenuSetActive(state._editActive + 1);
+ return;
+ }
+ if (e.key === "ArrowUp" || e.key === "k") {
+ e.preventDefault();
+ editMenuSetActive(state._editActive - 1);
+ return;
+ }
+ if (e.key === "Enter") {
+ e.preventDefault();
+ editMenuCommit();
+ return;
+ }
+ return;
+ }
if (overviewEl2.classList.contains("visible")) {
if (e.key === "Escape" || e.key === "q") {
closeOverview();
diff --git a/src/inkflow/presenter.html b/src/inkflow/presenter.html
index b70999ee..cbf71d69 100644
--- a/src/inkflow/presenter.html
+++ b/src/inkflow/presenter.html
@@ -57,6 +57,7 @@
KEYBINDINGS
| $ End | Last slide |
| g | Slide picker |
| o | Slide overview |
+ | e | Edit source |
| Present |
| . | Laser pointer |
| Ctrl + scroll | Zoom · Ctrl + drag to pan |
@@ -69,6 +70,7 @@ KEYBINDINGS
| p | Presenter panel |
| d | Diagnostics |
| m | Messages |
+ | n | New window |
| s | Cycle sync mode |
| ? | This help |
@@ -183,7 +185,7 @@ MESSAGES
-