serve: dark board chrome with light task cards, fix keyboard nav
Focus target moved from <details> to <summary> so Enter/Space
uses native browser toggle rather than manual JS open/close.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/src/bin/ranger/commands/serve.rs b/src/bin/ranger/commands/serve.rs
index bbc3b1f..3ebd03f 100644
--- a/src/bin/ranger/commands/serve.rs
+++ b/src/bin/ranger/commands/serve.rs
@@ -184,8 +184,8 @@ fn render_task(task: &TaskView) -> Markup {
let has_details = task.description.is_some() || task.has_subtasks;
html! {
@if has_details {
- details.task tabindex="0" {
- summary.task-header {
+ details.task {
+ summary.task-header tabindex="0" {
span.key {
span.key-prefix { (task.key_prefix) }
span.key-rest { (task.key_rest) }
@@ -237,29 +237,30 @@ fn keyboard_nav_script() -> Markup {
script {
(PreEscaped(r#"
(function() {
- function getTasks() {
- return Array.from(document.querySelectorAll('.task'));
+ function getFocusables() {
+ return Array.from(document.querySelectorAll(
+ 'details.task > summary, div.task'
+ ));
}
- function focusTask(tasks, idx) {
- if (idx >= 0 && idx < tasks.length) {
- tasks[idx].focus();
- tasks[idx].scrollIntoView({ block: 'nearest' });
+ function focusEl(els, idx) {
+ if (idx >= 0 && idx < els.length) {
+ els[idx].focus();
+ els[idx].scrollIntoView({ block: 'nearest' });
}
}
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
- var tasks = getTasks();
- var current = tasks.indexOf(document.activeElement);
+ var els = getFocusables();
+ var current = els.indexOf(document.activeElement);
if (e.key === 'j' || e.key === 'ArrowDown') {
e.preventDefault();
- focusTask(tasks, current < 0 ? 0 : current + 1);
+ focusEl(els, current < 0 ? 0 : current + 1);
} else if (e.key === 'k' || e.key === 'ArrowUp') {
e.preventDefault();
- focusTask(tasks, current < 0 ? 0 : current - 1);
- } else if ((e.key === 'Enter' || e.key === ' ') && document.activeElement.tagName === 'DETAILS') {
+ focusEl(els, current < 0 ? 0 : current - 1);
+ } else if ((e.key === 'Enter' || e.key === ' ') && document.activeElement.tagName === 'SUMMARY') {
e.preventDefault();
- var details = document.activeElement;
- details.open = !details.open;
+ document.activeElement.click();
}
});
})();
diff --git a/static/style.css b/static/style.css
index 3eb5b2b..274c0ea 100644
--- a/static/style.css
+++ b/static/style.css
@@ -10,17 +10,17 @@
:root {
/* ── Color tokens: surfaces ── */
- --color-bg-header: #3c3d3f;
- --color-bg-panel: #eeeddf;
- --color-border: #d5d3c5;
+ --color-bg-header: #1a1b1e;
+ --color-bg-panel: #24262a;
+ --color-border: #3a3c42;
/* ── Color tokens: text ── */
- --color-text: #333;
- --color-text-muted: #7a7868;
- --color-text-header: #fff;
+ --color-text: #d8d8d8;
+ --color-text-muted: #8a8a8a;
+ --color-text-header: #e0e0e0;
/* ── Color tokens: accent ── */
- --color-accent: #3d6098;
+ --color-accent: #6a9fd8;
/* ── Color tokens: state ── */
--color-in-progress: #629a3e;
@@ -29,10 +29,10 @@
--color-icebox: #8a8878;
/* ── State row tints (12 % opacity) ── */
- --tint-in-progress: rgba(98, 154, 62, 0.12);
- --tint-queued: rgba(61, 96, 152, 0.12);
- --tint-done: rgba(139, 168, 59, 0.12);
- --tint-icebox: rgba(138, 136, 120, 0.12);
+ --tint-in-progress: rgba(210, 170, 30, 0.12);
+ --tint-queued: rgba(160, 160, 160, 0.12);
+ --tint-done: rgba(98, 154, 62, 0.24);
+ --tint-icebox: rgba(61, 96, 152, 0.20);
/* ── Typography ── */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
@@ -46,12 +46,12 @@
/* ── Spacing ── */
--row-padding-y: 7px;
- --row-gap: 3px;
+ --row-gap: 0px;
--panel-padding: 20px;
--radius: 4px;
/* ── Focus ── */
- --color-focus: rgba(61, 96, 152, 0.5);
+ --color-focus: rgba(106, 159, 216, 0.5);
}
@@ -95,7 +95,7 @@ header h1 .sep {
header .counts {
font-size: var(--step-mono);
- color: rgba(255, 255, 255, 0.6);
+ color: rgba(224, 224, 224, 0.5);
}
@@ -103,23 +103,24 @@ header .counts {
.board {
display: grid;
grid-template-columns: repeat(3, 1fr);
- gap: var(--panel-padding);
+ gap: 16px;
height: calc(100vh - 41px); /* header height */
- padding: var(--panel-padding);
+ padding: 16px;
}
/* === Panels === */
.panel {
overflow-y: auto;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: var(--radius);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
- margin-bottom: 12px;
- padding-bottom: 8px;
+ padding: 8px 12px;
border-bottom: 1px solid var(--color-border);
}
@@ -165,28 +166,35 @@ header .counts {
/* === Task rows === */
.task {
- background: #fff;
- border: 1px solid var(--color-border);
- border-radius: var(--radius);
+ background: none;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ margin-top: -1px;
+ border-radius: 0;
padding: var(--row-padding-y) 10px;
margin-bottom: var(--row-gap);
outline: none;
transition: box-shadow 0.1s ease;
}
-.task:focus {
+.task:focus-visible,
+details.task > summary:focus-visible {
box-shadow: 0 0 0 2px var(--color-focus);
z-index: 1;
position: relative;
}
-/* details element reset */
-details.task {
- cursor: pointer;
+.task:focus,
+details.task > summary:focus {
+ outline: none;
}
+/* details element reset */
details.task > summary {
list-style: none;
+ cursor: pointer;
+ outline: none;
+ -webkit-user-select: none;
+ user-select: none;
}
details.task > summary::-webkit-details-marker {
@@ -212,19 +220,19 @@ details.task > summary::marker {
}
.task .key-prefix {
- color: var(--color-accent);
+ color: #3d6098;
font-weight: 700;
}
.task .key-rest {
- color: var(--color-text-muted);
+ color: #7a7868;
font-weight: 400;
}
.task .title {
font-size: var(--step-0);
font-weight: 500;
- color: var(--color-text);
+ color: #333;
flex: 1;
min-width: 0;
}
@@ -232,7 +240,7 @@ details.task > summary::marker {
/* === Expand icon === */
.task .expand-icon {
font-size: var(--step-mono);
- color: var(--color-text-muted);
+ color: #7a7868;
margin-left: auto;
flex-shrink: 0;
transition: transform 0.15s ease;
@@ -247,12 +255,12 @@ details.task[open] > summary .expand-icon {
.task-body {
padding: 6px 0 2px 0;
margin-top: 4px;
- border-top: 1px solid var(--color-border);
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.task .desc {
font-size: var(--step-0);
- color: var(--color-text-muted);
+ color: #666;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
@@ -260,7 +268,7 @@ details.task[open] > summary .expand-icon {
.task .subtask-indicator {
font-size: var(--step-mono);
- color: var(--color-text-muted);
+ color: #7a7868;
margin-top: 4px;
}
@@ -275,7 +283,7 @@ details.task[open] > summary .expand-icon {
.task .tag {
font-size: 0.625rem;
font-family: var(--font-mono);
- color: var(--color-accent);
+ color: #3d6098;
background: rgba(61, 96, 152, 0.1);
padding: 0 5px;
border-radius: 3px;
@@ -284,11 +292,29 @@ details.task[open] > summary .expand-icon {
}
+/* === First/last task rounding === */
+.panel > :last-child .task:last-child,
+.panel > .task:last-child {
+ border-radius: 0 0 var(--radius) var(--radius);
+}
+
+.panel > :nth-child(2) .task:first-child,
+.panel > .task:first-child {
+ border-radius: var(--radius) var(--radius) 0 0;
+ margin-top: 0;
+}
+
+/* When a panel has only one task group with one task */
+.panel > :nth-child(2):last-child .task:only-child,
+.panel > .task:only-child {
+ border-radius: var(--radius);
+}
+
/* === State-specific row tints === */
-.state-in-progress .task { background: var(--tint-in-progress); }
-.state-queued .task { background: var(--tint-queued); }
-.state-done .task { background: var(--tint-done); }
-.state-icebox .task { background: var(--tint-icebox); }
+.state-in-progress .task { background: #fdf6e3; }
+.state-queued .task { background: #f5f5f3; }
+.state-done .task { background: #eef5e6; }
+.state-icebox .task { background: #e8eef6; }
/* Done tasks are slightly desaturated */
.state-done .task {