Add issues and comment threads (Phase 3)
Issues: per-project numbers, statuses (open, accepted, rejected,
withdrawn, duplicate), feature links, edits, and a timeline of comments
and events. From an issue, collaborators create or update a feature
with the revision tied to the issue and an option to accept it.
Comments: general and line-anchored threads on features, one
discussion thread per issue, edit history, tombstone deletes, resolve,
and promoting a feature thread to an issue. Line threads attach to
top-level Markdown blocks and go outdated only when the text changes.
HTMX swaps threads in place with focus management; every action also
works as a plain form post. Markdown headings are re-levelled so user
text never skips heading levels on the page.
The example flow from concept.md (steps 1-7) is covered end to end in
tests/test_example_flow.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:18:45 -04:00
|
|
|
// Keyboard and screen-reader focus after HTMX swaps a thread in place.
|
|
|
|
|
// The server marks the element to land on (a new or edited comment, or the thread itself)
|
|
|
|
|
// with data-autofocus; without this, focus would fall back to the top of the page.
|
|
|
|
|
document.addEventListener("htmx:afterSettle", function () {
|
|
|
|
|
var target = document.querySelector("[data-autofocus]");
|
|
|
|
|
if (!target) return;
|
|
|
|
|
target.removeAttribute("data-autofocus");
|
|
|
|
|
target.focus();
|
|
|
|
|
});
|
Add feature files, image pins and implementations (Phase 4)
Files: prototype and documentation files on features, with versions.
Adding, replacing or removing a file is a feature revision with a
change note. Types come from the extension; uploads are capped at
25 MB and stored under opaque paths. Every file URL checks project
access, then redirects to a short-lived signed URL on the bucket, or
streams with nosniff and a CSP sandbox when no bucket is configured.
HTML prototypes run in a sandboxed iframe without same-origin access.
Threads: whole-file threads and pins at a point on an image, drawn as
an SVG overlay in percentages. Pins are placed with number fields, or
by clicking the image with JS.
Implementations: name, link and status, plus a per-feature status grid
stamped with the feature revision, marked behind when the feature
changes, and confirmable without changing the status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:15:30 -04:00
|
|
|
|
|
|
|
|
// Image pins. The across/down number fields are the real controls and work without this;
|
|
|
|
|
// clicking the image just fills them in and shows where the pin will go.
|
|
|
|
|
(function () {
|
|
|
|
|
var SVG = "http://www.w3.org/2000/svg";
|
|
|
|
|
|
|
|
|
|
function fields() {
|
|
|
|
|
return {
|
|
|
|
|
across: document.getElementById("pin-across"),
|
|
|
|
|
down: document.getElementById("pin-down"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showPending(board, across, down) {
|
|
|
|
|
var svg = board.querySelector("[data-pins]");
|
|
|
|
|
var pin = svg.querySelector(".pending");
|
|
|
|
|
if (across === "" || down === "") {
|
|
|
|
|
if (pin) pin.remove();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!pin) {
|
|
|
|
|
pin = document.createElementNS(SVG, "circle");
|
|
|
|
|
pin.setAttribute("class", "pending");
|
|
|
|
|
pin.setAttribute("r", "12");
|
|
|
|
|
svg.appendChild(pin);
|
|
|
|
|
}
|
|
|
|
|
pin.setAttribute("cx", across + "%");
|
|
|
|
|
pin.setAttribute("cy", down + "%");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener("click", function (event) {
|
|
|
|
|
var board = event.target.closest(".pinboard.placing");
|
|
|
|
|
if (!board || event.target.closest("a")) return;
|
|
|
|
|
var f = fields();
|
|
|
|
|
if (!f.across || !f.down) return;
|
|
|
|
|
var box = board.querySelector("img").getBoundingClientRect();
|
|
|
|
|
var clamp = function (v) { return Math.min(100, Math.max(0, v)); };
|
|
|
|
|
f.across.value = clamp(((event.clientX - box.left) / box.width) * 100).toFixed(1);
|
|
|
|
|
f.down.value = clamp(((event.clientY - box.top) / box.height) * 100).toFixed(1);
|
|
|
|
|
showPending(board, f.across.value, f.down.value);
|
|
|
|
|
var comment = document.getElementById("file-comment");
|
|
|
|
|
if (comment) comment.focus();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener("input", function (event) {
|
|
|
|
|
if (event.target.id !== "pin-across" && event.target.id !== "pin-down") return;
|
|
|
|
|
var board = document.querySelector(".pinboard.placing");
|
|
|
|
|
var f = fields();
|
|
|
|
|
if (board) showPending(board, f.across.value, f.down.value);
|
|
|
|
|
});
|
|
|
|
|
})();
|