Unify new and edit series sidebar layout
The new-series form used a stacked label-over-input layout while the edit
sidebar used a compact dl/dd grid. Both now share the same structure so
the app feels consistent. The Create button moves to the header, matching
the Edit/Done position, and stays disabled until the note has content.
Fixes a KeyError when viewing an overdue series—urgency now falls back
to the series association when interval columns aren't joined.
Assisted-by: Claude Opus 4.6 via Claude Code
diff --git a/BACKLOG.md b/BACKLOG.md
index b3b589d..c7820fe 100644
--- a/BACKLOG.md
+++ b/BACKLOG.md
@@ -51,3 +51,4 @@
- Fixed-schedule recurrence
- Personal/shared toggle
- CLI interface
+- Sentry
diff --git a/lib/ketchup/models.rb b/lib/ketchup/models.rb
index ee36254..8b75bda 100644
--- a/lib/ketchup/models.rb
+++ b/lib/ketchup/models.rb
@@ -49,7 +49,9 @@ class Task < Sequel::Model
days_overdue = Date.today - self[:due_date]
return 0 if days_overdue <= 0
- interval = self[:interval_count] * INTERVAL_DAYS.fetch(self[:interval_unit])
+ count = self[:interval_count] || series.interval_count
+ unit = self[:interval_unit] || series.interval_unit
+ interval = count * INTERVAL_DAYS.fetch(unit)
days_overdue.to_f / interval
end
diff --git a/lib/ketchup/views/home.rb b/lib/ketchup/views/home.rb
index 3bb60d3..9b3bb17 100644
--- a/lib/ketchup/views/home.rb
+++ b/lib/ketchup/views/home.rb
@@ -66,18 +66,18 @@ module Views
div(class: "column column-aside", "x-data": "{ editing: false }") do
div(class: "column-header") do
h2(class: "aside-heading") do
- a(href: "/", class: "aside-heading-action") { "+ New" }
- end
- nav(class: "sort-toggle") do
- button(
- "x-show": "!editing",
- "x-on:click": "editing = true; $dispatch('start-editing')"
- ) { "Edit" }
- button(
- "x-show": "editing",
- "x-on:click": "editing = false; $dispatch('stop-editing')"
- ) { "Done" }
+ a(href: "/", class: "aside-heading-action") { "New" }
end
+ button(
+ class: "aside-heading-action",
+ "x-show": "!editing",
+ "x-on:click": "editing = true; $dispatch('start-editing')"
+ ) { "Edit" }
+ button(
+ class: "aside-heading-action",
+ "x-show": "editing",
+ "x-on:click": "editing = false; $dispatch('stop-editing')"
+ ) { "Done" }
end
div(class: "task-detail") do
@@ -88,7 +88,7 @@ module Views
"data-series-id": @selected_series.id.to_s
)
dl(class: "task-detail-fields") do
- dt { "Interval" }
+ dt { "Repeat every" }
dd("x-show": "!editing") do
plain interval_text(@selected_series.interval_count, @selected_series.interval_unit)
end
@@ -135,8 +135,8 @@ module Views
end
if @selected_task.urgency > 0
- dt("x-show": "!editing") { "Urgency" }
- dd("x-show": "!editing") { "#{format("%.1f", @selected_task.urgency)}x" }
+ dt { "Urgency" }
+ dd { "#{format("%.1f", @selected_task.urgency)}x" }
end
end
end
@@ -186,43 +186,39 @@ module Views
def new_series_sidebar
div(class: "column column-aside") do
div(class: "column-header") do
- h2(class: "aside-heading") do
- span { "New Series" }
- end
+ h2 { "New Series" }
+ button(type: "submit", form: "new-series-form", id: "create-series-btn", class: "aside-heading-action", disabled: true) { "Create" }
end
- form(method: "post", action: "/series") do
- div(class: "field") do
- label(for: "note") { "Note" }
- div(id: "series-note-editor")
- end
+ form(method: "post", action: "/series", id: "new-series-form", class: "task-detail") do
+ div(id: "series-note-editor", class: "task-detail-note")
- div(class: "field") do
- label(for: "interval_count") { "Repeat every" }
- div(class: "interval") do
+ dl(class: "task-detail-fields") do
+ dt { "Repeat every" }
+ dd(class: "detail-edit-interval") do
input(
- type: "number", id: "interval_count", name: "interval_count",
+ type: "number", name: "interval_count",
+ class: "detail-input detail-input-count",
min: 1, value: 1, required: true
)
- select(id: "interval_unit", name: "interval_unit", required: true) do
- option(value: "day") { "day(s)" }
+ select(name: "interval_unit", class: "detail-input detail-input-unit", required: true) do
+ option(value: "day", selected: true) { "day(s)" }
option(value: "week") { "week(s)" }
option(value: "month") { "month(s)" }
option(value: "quarter") { "quarter(s)" }
option(value: "year") { "year(s)" }
end
end
- end
- div(class: "field") do
- label(for: "first_due_date") { "First due date" }
- input(
- type: "date", id: "first_due_date", name: "first_due_date",
- value: Date.today.to_s, required: true
- )
+ dt { "First due date" }
+ dd do
+ input(
+ type: "date", name: "first_due_date",
+ class: "detail-input detail-input-date",
+ value: Date.today.to_s, required: true
+ )
+ end
end
-
- button(type: "submit") { "Create" }
end
end
end
diff --git a/lib/ketchup/views/layout.rb b/lib/ketchup/views/layout.rb
index 9fc3d20..557bd60 100644
--- a/lib/ketchup/views/layout.rb
+++ b/lib/ketchup/views/layout.rb
@@ -25,7 +25,7 @@ module Views
end
body do
header(class: "site-header") do
- span(class: "site-name") { "Ketchup" }
+ a(href: "/", class: "site-name") { "Ketchup" }
span(class: "user") { @current_user[:name] || @current_user[:login] }
end
yield
diff --git a/public/css/app.css b/public/css/app.css
index 15c0c10..a670771 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -83,6 +83,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
display: flex;
justify-content: space-between;
align-items: baseline;
+ gap: var(--space-2xs);
margin-block-end: var(--space-xs);
padding-block-end: var(--space-3xs);
border-block-end: 1px dotted #ccc;
@@ -105,6 +106,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
}
.aside-heading {
+ flex: 1;
color: #999;
}
@@ -113,21 +115,20 @@ h2 { font-size: var(--step-0); font-weight: 600; }
}
.aside-heading-action {
+ all: unset;
font-size: var(--step--1);
font-weight: 400;
+ color: #999;
cursor: default;
}
-.column-aside form {
- font-size: var(--step--1);
-}
-
-.column-aside label {
- font-size: var(--step--2);
+.aside-heading-action:hover {
+ color: #1a1a1a;
}
-.column-aside button[type="submit"] {
- font-size: var(--step--1);
+.aside-heading-action:disabled {
+ color: #ccc;
+ cursor: default;
}
#series-note-editor {
@@ -139,6 +140,14 @@ h2 { font-size: var(--step-0); font-weight: 600; }
border-color: #999;
}
+.column-aside form {
+ font-size: var(--step--1);
+}
+
+.column-aside button[type="submit"] {
+ font-size: var(--step--1);
+}
+
/* -------------------- */
/* Task list */
/* -------------------- */
@@ -230,6 +239,15 @@ h2 { font-size: var(--step-0); font-weight: 600; }
border-radius: 3px;
}
+#series-note-editor {
+ margin-block-end: 0;
+ border-color: #ccc;
+}
+
+#series-note-editor:focus-within {
+ border-color: #999;
+}
+
.task-detail-note:hover {
border-color: transparent;
}
@@ -258,6 +276,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
.detail-input {
font-size: var(--step--2);
padding: 1px 4px;
+ margin: -2px -5px;
border: 1px solid transparent;
border-radius: 3px;
background: transparent;
@@ -479,7 +498,7 @@ textarea {
gap: var(--space-3xs);
}
-button[type="submit"]:not(.complete-btn) {
+button[type="submit"]:not(.complete-btn):not(.aside-heading-action) {
padding: var(--space-3xs) var(--space-s);
border: 1px solid #1a1a1a;
border-radius: 3px;
@@ -490,7 +509,7 @@ button[type="submit"]:not(.complete-btn) {
cursor: pointer;
}
-button[type="submit"]:not(.complete-btn):hover {
+button[type="submit"]:not(.complete-btn):not(.aside-heading-action):hover {
background: #333;
}
diff --git a/public/js/app.js b/public/js/app.js
index 79bdc9f..ae5b693 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -198,11 +198,17 @@ document.addEventListener("alpine:init", () => {
// New series form editor
const newNoteEl = document.getElementById("series-note-editor")
if (newNoteEl) {
- new OverType("#series-note-editor", {
+ const editor = new OverType("#series-note-editor", {
placeholder: "What needs doing...",
textareaProps: { name: "note", required: true },
autoResize: true,
})
+ const createBtn = document.getElementById("create-series-btn")
+ if (createBtn) {
+ newNoteEl.addEventListener("input", () => {
+ createBtn.disabled = !editor.value.trim()
+ })
+ }
}
Alpine.data("upcoming", () => ({
diff --git a/test/test_web.rb b/test/test_web.rb
index 0703079..815c458 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -236,7 +236,7 @@ class TestWeb < Minitest::Test
series = DB[:series].first
get "/series/#{series[:id]}", {}, tailscale_headers
- assert_includes last_response.body, "+ New"
+ assert_includes last_response.body, "New"
assert_includes last_response.body, 'href="/"'
end