oficina / calendar Public

quick add: Enter adds it

Enter submits, Shift+Enter is a newline, Cmd/Ctrl+Enter submits too —
the Slack convention plus the chord people arrive from GitHub and Linear
expecting. Guarded against an IME, so a mid-composition Enter commits
the candidate word instead of submitting the form under it.

Enter can fire before the debounced parse has landed, so the pane may
never have shown that sentence. Accepted deliberately: the pane is a
PREVIEW, never a gate. The server parses again on submit, so racing it
can only mean "you did not get to look", never "the wrong event was
created". Anyone who wants to look pauses, which is the same pause the
debounce is already waiting for.

The hint is revealed by quickadd.js rather than rendered by the
template. With no script Enter is an ordinary newline, and a page
advertising a shortcut it cannot honour is worse than a silent one. The
browser gate asserts the hint is absent with JavaScript off.

The Add button is untouched — this sits on a floor that already worked.

The browser gate grew two passes and now covers Enter creating the
event, Shift+Enter creating nothing, and the hint's presence in both
directions. One of them caught a test bug of my own on the way: /today
redirects to /day/<date>, so asserting "still on /today" was checking a
URL the page had never been on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev f18898b50f973741578ae1739b70d829c441d33e parent 77f45fa
5 files changed, +132 −3
  • docs/superpowers/specs/2026-09-02-quick-add-design.md +21 −0
  • internal/calendar/static/calendar.css +8 −0
  • internal/calendar/static/quickadd.js +39 −0
  • internal/calendar/templates/_quickadd.html +4 −0
  • test/browser/quickadd.mjs +60 −3
diff --git a/docs/superpowers/specs/2026-09-02-quick-add-design.md b/docs/superpowers/specs/2026-09-02-quick-add-design.md
index 3b5ce39..4d80921 100644
--- a/docs/superpowers/specs/2026-09-02-quick-add-design.md
+++ b/docs/superpowers/specs/2026-09-02-quick-add-design.md
@@ -52,6 +52,27 @@ and it makes the fast path two steps for everybody in order to protect
the case where the parse was wrong — which the pane already covers for
anyone who has script, and which Edit covers for anyone who does not.
+## 2.1 Enter adds it
+
+**Enter submits. Shift+Enter is a newline. Cmd/Ctrl+Enter also submits.**
+The Slack convention, plus the chord people arrive from GitHub and
+Linear expecting.
+
+Enter can fire before the debounced parse has landed, so the pane may
+never have shown that sentence. Accepted, because **the pane is a
+preview and never a gate**: the server parses again on submit, so
+racing it can only mean "you did not get to look", never "the wrong
+event was created". Anyone who wants to look pauses — the same pause
+the debounce is waiting for.
+
+The hint that says so is revealed by `quickadd.js`, never rendered by
+the template. With no script Enter is an ordinary newline, and a page
+advertising a shortcut it cannot honour is worse than a silent one. The
+browser gate asserts the hint is absent with JavaScript off.
+
+The Add button is untouched. This is an enhancement on a floor that
+already worked.
+
## 3. Nothing is guessed silently
Every defaulted field is **named in the pane in words**, not implied:
diff --git a/internal/calendar/static/calendar.css b/internal/calendar/static/calendar.css
index 268a794..3480b78 100644
--- a/internal/calendar/static/calendar.css
+++ b/internal/calendar/static/calendar.css
@@ -453,3 +453,11 @@
}
[cal-quick-row] { grid-template-columns: 1fr; gap: 0; }
}
+
+/* The keyboard hint. Revealed by quickadd.js, never by the template:
+ * with no script Enter is just a newline, and advertising a shortcut
+ * the page cannot honour is worse than staying quiet. */
+[cal-quick-key] {
+ color: var(--rst-text-faint);
+ font-size: var(--rst-fs-xs);
+}
diff --git a/internal/calendar/static/quickadd.js b/internal/calendar/static/quickadd.js
index 7b046a1..2d38281 100644
--- a/internal/calendar/static/quickadd.js
+++ b/internal/calendar/static/quickadd.js
@@ -90,6 +90,39 @@
}
});
+ // Enter adds it. Shift+Enter is a newline, and Cmd/Ctrl+Enter adds
+ // it too — the chord people arrive from GitHub and Linear expecting.
+ //
+ // Enter can fire before the debounced parse has landed, so the pane
+ // may never have shown this sentence. That is ACCEPTABLE, and the
+ // reason is worth stating: the pane is a preview, never a gate. The
+ // server parses again on submit, so racing it can only mean "you did
+ // not get to look" — never "the wrong event was created". Anyone who
+ // wants to look pauses, which is the same pause the debounce needs.
+ text.addEventListener("keydown", function (e) {
+ if (e.key !== "Enter") return;
+ if (e.shiftKey) return; // a deliberate newline
+ // isComposing guards an IME: mid-composition Enter commits the
+ // candidate word and must not submit the form under it.
+ if (e.isComposing || e.keyCode === 229) return;
+ if (text.value.trim() === "") return;
+ e.preventDefault();
+ submit();
+ });
+
+ function submit() {
+ var form = root.querySelector("[cal-quick-form]");
+ if (!form) return;
+ // requestSubmit fires submit handlers and honours validation the
+ // way a real button press does; form.submit() skips both.
+ if (typeof form.requestSubmit === "function") {
+ form.requestSubmit();
+ return;
+ }
+ var button = form.querySelector("button[type=submit]");
+ if (button) button.click();
+ }
+
// Escape closes, which is what every other popover on the web does.
root.addEventListener("keydown", function (e) {
if (e.key !== "Escape" || !root.open) return;
@@ -97,6 +130,12 @@
var summary = root.querySelector("summary");
if (summary) summary.focus();
});
+
+ // The keyboard hint is revealed HERE rather than rendered in the
+ // template, because without this file Enter does nothing and a
+ // promise the page cannot keep is worse than no hint at all.
+ var hint = root.querySelector("[cal-quick-key]");
+ if (hint) hint.hidden = false;
}
function init() {
diff --git a/internal/calendar/templates/_quickadd.html b/internal/calendar/templates/_quickadd.html
index b29e924..2679f7c 100644
--- a/internal/calendar/templates/_quickadd.html
+++ b/internal/calendar/templates/_quickadd.html
@@ -39,6 +39,10 @@
<div cal-quick-actions>
<button rst-btn="primary" type="submit">Add</button>
+ {{/* hidden until quickadd.js reveals it: with no script, Enter
+ inserts a newline like any other textarea, and a page that
+ advertised a shortcut it does not have would be lying. */}}
+ <span cal-quick-key hidden>or press Enter</span>
<span cal-quick-hint>Times, dates, durations, places and email addresses are read from the sentence.</span>
</div>
</form>
diff --git a/test/browser/quickadd.mjs b/test/browser/quickadd.mjs
index 3bcdcc3..9cf9e73 100644
--- a/test/browser/quickadd.mjs
+++ b/test/browser/quickadd.mjs
@@ -6,14 +6,17 @@
// all (it starts Chromium and parks at about:blank until the Go test
// times out), and a gate that cannot run is not a gate.
//
-// Two passes, and the second is the one that matters most:
+// Four passes, and the scriptless ones matter most:
//
// 1. WITH script — type, and the pane fills in from the server.
// 2. WITHOUT script — the same box still opens, still submits, and
// still creates the event.
+// 3. WITH script — Enter adds it, Shift+Enter does not.
+// 4. WITHOUT script — the Enter hint is absent, because the shortcut
+// does not exist there and a page must not promise one.
//
-// The scriptless pass is the only way to prove the floor is real. Every
-// other test in this repo asks Go what Go produced; this one asks a
+// The scriptless passes are the only way to prove the floor is real.
+// Every other test in this repo asks Go what Go produced; these ask a
// browser with JavaScript switched off whether the feature still works.
import { chromium } from 'playwright';
@@ -118,6 +121,60 @@ async function contextWith(javaScriptEnabled) {
await ctx.close();
}
+// ── 3. Enter adds it; Shift+Enter does not ──────────────────────────
+{
+ const ctx = await contextWith(true);
+ const page = await ctx.newPage();
+ await page.goto(origin + '/today', { waitUntil: 'networkidle' });
+ await page.locator('[cal-quick] > summary').click();
+
+ const box = page.locator('[cal-quick-text]');
+
+ // Shift+Enter is a newline, so it must NOT create anything. Asserted
+ // as "did not land on an event" rather than "still on /today": /today
+ // redirects to /day/<date>, so the page was never on /today to begin
+ // with and the tighter-looking assertion was just wrong.
+ //
+ // Checked BEFORE the submitting case — once Enter navigates there is no
+ // page left to test this on.
+ await box.fill('Draft lunch');
+ await box.press('Shift+Enter');
+ await page.waitForTimeout(300);
+ check(!/\/events\/\d+$/.test(page.url()),
+ 'Shift+Enter created nothing (got ' + page.url() + ')');
+ check((await box.inputValue()).includes('\n'),
+ 'Shift+Enter inserts a newline rather than submitting');
+
+ // The hint only appears when this file is running, because without it
+ // Enter does nothing.
+ check(await page.locator('[cal-quick-key]').isVisible(),
+ 'the Enter hint is shown when script is on');
+
+ await box.fill('Keyboard standup tomorrow at 8am in the Loft');
+ await Promise.all([
+ page.waitForNavigation(),
+ box.press('Enter'),
+ ]);
+
+ check(/\/events\/\d+$/.test(page.url()),
+ 'Enter creates the event (got ' + page.url() + ')');
+ const body = await page.locator('body').innerText();
+ check(body.includes('Keyboard standup'), 'the event Enter created kept its title');
+ check(body.includes('Loft'), 'the event Enter created kept its place');
+ await ctx.close();
+}
+
+// ── 4. The Enter hint is absent with no script ──────────────────────
+{
+ const ctx = await contextWith(false);
+ const page = await ctx.newPage();
+ await page.goto(origin + '/today');
+ await page.locator('[cal-quick] > summary').click();
+ check(!(await page.locator('[cal-quick-key]').isVisible()),
+ 'the Enter hint stays hidden with JavaScript off — the shortcut does not work there');
+ await ctx.close();
+}
+
await browser.close();
if (failures.length) {