Explorar el Código

r3: shared plan-review JS (arch 15) — one parameterised initPlanReviews in main.html. The ~180-line collect-remarks pattern (the per-block remark queue, the plan-popover, the numbered rejection body with quote-prefixed remarks, accept-clears-queue, remark-less rejection via the feedback textarea, syncPlanRemarks on afterEvaluate) moves verbatim into main.html as the shared engine; the page passes only resolveBlock(id). goal_detail.html keeps the resolution (resolveReviewBlock over goal.open_blocks + plan_pending_block — the S2 completion-review deep-link semantics) and registers. The task-page twin of the pattern died with the poll era in the arch-19 commit (the plain-task plan review was unreachable), so the duplication the review flagged is gone at the source: the engine lives once, asserted identifiers (remarksByBlock, rejectOpenByBlock, syncPlanRemarks, plan-popover, the numbered (index + 1) + '. ' body) all preserved on the composed goal page — the goalui S2/S3 pins pass unchanged. 851 checks.

clanker hace 1 mes
padre
commit
e578a698f5
Se han modificado 2 ficheros con 229 adiciones y 198 borrados
  1. 14 197
      src/pages/goal_detail.html
  2. 215 1
      src/pages/main.html

+ 14 - 197
src/pages/goal_detail.html

@@ -17,10 +17,11 @@
     // selection-quoting, and the goal agent's runs. Every long-form
     // body (brief, plans, reports, blocks, comments, events) renders
     // as markdown via stm-markdown. The plan review collects REMARKS
-    // (plan 14 B2): "Add remark" appends to a per-block queue that
-    // renders at the bottom of the plan, persists across pulse
-    // refreshes (the maps live outside statum re-renders), rides the
-    // rejection numbered, and clears on accept. The status strip's
+    // through the shared initPlanReviews engine (main.html, plan 16
+    // R3/arch 15): a per-block queue that renders at the bottom of
+    // the plan, persists across pulse refreshes (the maps live
+    // outside statum re-renders), rides the rejection numbered, and
+    // clears on accept. The status strip's
     // filter lives client-side in window.goalTaskFilter; clicking a
     // strip button re-runs the sealed refresh action, and the filter
     // rides the re-render.
@@ -78,167 +79,11 @@
                 });
             }
 
-            // Plan 14 B2: the plan-review remarks queue — the
-            // task-page collect-remarks pattern (the feedbackByBlock
-            // maps) ported to the goal's plan review. The queue lives
-            // in these maps (keyed by goal block id) so statum
-            // re-renders never lose it; the DOM list is rebuilt after
-            // every render cycle.
-            var remarksByBlock = {};
-            var rejectOpenByBlock = {};
-            var remarkPopover = null;
-
-            statum.afterEvaluate(syncPlanRemarks);
-
-            document.addEventListener('click', function (event) {
-                if (!event.target.closest) return;
-                var accept = event.target.closest('[data-plan-accept]');
-                if (accept) {
-                    // Accept ignores/clears the queued remarks.
-                    var accepted = accept.closest('[data-plan-review]');
-                    if (accepted) {
-                        var acceptId = accepted.getAttribute('data-block-id');
-                        remarksByBlock[acceptId] = [];
-                        rejectOpenByBlock[acceptId] = false;
-                    }
-                    return;
-                }
-                var toggle = event.target.closest('[data-plan-toggle]');
-                if (toggle) {
-                    hideRemarkPopover();
-                    var review = toggle.closest('[data-plan-review]');
-                    var id = review && review.getAttribute('data-block-id');
-                    if (id) rejectOpenByBlock[id] = !rejectOpenByBlock[id];
-                    syncPlanRemarks();
-                    return;
-                }
-                var submit = event.target.closest('[data-plan-submit]');
-                if (submit) {
-                    submitPlanRemarks(submit.closest('[data-plan-review]'));
-                    return;
-                }
-                var remove = event.target.closest('[data-plan-remove]');
-                if (remove) {
-                    var entry = remove.closest('[data-plan-remark-id]');
-                    var blockId = remove.closest('[data-plan-review]').getAttribute('data-block-id');
-                    var list = remarksByBlock[blockId] || [];
-                    var index = parseInt(entry.getAttribute('data-plan-remark-id'), 10);
-                    if (!isNaN(index) && list[index]) list.splice(index, 1);
-                    remarksByBlock[blockId] = list;
-                    syncPlanRemarks();
-                }
-            });
-
-            document.addEventListener('mouseup', function (event) {
-                if (!event.target.closest) return;
-                if (remarkPopover && remarkPopover.contains(event.target)) return;
-                var body = event.target.closest('[data-plan-body]');
-                if (!body) {
-                    hideRemarkPopover();
-                    return;
-                }
-                var review = body.closest('[data-plan-review]');
-                if (!review || review.getAttribute('data-plan-open') !== '1') return;
-                var selection = window.getSelection();
-                var text = selection ? String(selection.toString()).trim() : '';
-                if (text.length === 0) {
-                    hideRemarkPopover();
-                    return;
-                }
-                showRemarkPopover(event.pageX, event.pageY, text, review);
-            });
-
-            document.addEventListener('mousedown', function (event) {
-                if (remarkPopover && event.target.closest
-                        && event.target.closest('.plan-popover')) return;
-                hideRemarkPopover();
-            });
-
-            function showRemarkPopover(x, y, quote, review) {
-                hideRemarkPopover();
-                remarkPopover = document.createElement('div');
-                remarkPopover.className = 'plan-popover';
-                remarkPopover.style.left = Math.min(x, window.innerWidth - 140) + 'px';
-                remarkPopover.style.top = y + 'px';
-
-                var quoted = document.createElement('blockquote');
-                quoted.textContent = quote;
-                remarkPopover.appendChild(quoted);
-
-                var body = document.createElement('textarea');
-                body.placeholder = 'Your remark on the selected plan text';
-                remarkPopover.appendChild(body);
-
-                var actions = document.createElement('div');
-                actions.className = 'actions';
-                var add = document.createElement('button');
-                add.type = 'button';
-                add.className = 'primary';
-                add.textContent = 'Add remark';
-                add.addEventListener('click', function () {
-                    if (body.value.trim().length === 0) return;
-                    var id = review.getAttribute('data-block-id');
-                    var list = remarksByBlock[id] || [];
-                    list.push({ quote: quote, body: body.value.trim() });
-                    remarksByBlock[id] = list;
-                    hideRemarkPopover();
-                    syncPlanRemarks();
-                });
-                var cancel = document.createElement('button');
-                cancel.type = 'button';
-                cancel.textContent = 'Cancel';
-                cancel.addEventListener('click', hideRemarkPopover);
-                actions.appendChild(add);
-                actions.appendChild(cancel);
-                remarkPopover.appendChild(actions);
-
-                document.body.appendChild(remarkPopover);
-                body.focus();
-            }
-
-            function hideRemarkPopover() {
-                if (remarkPopover) {
-                    remarkPopover.remove();
-                    remarkPopover = null;
-                }
-            }
-
-            function submitPlanRemarks(review) {
-                if (!review) return;
-                var id = review.getAttribute('data-block-id');
-                var list = remarksByBlock[id] || [];
-                var extra = review.querySelector('[data-plan-feedback] textarea[name="feedback"]');
-                var extraText = extra && extra.value.trim().length > 0 ? extra.value.trim() : '';
-                // Remark-less rejection is allowed when the feedback
-                // textarea carries the review's substance — one of the
-                // two must be non-empty (plan 16 S2).
-                if (list.length === 0 && extraText.length === 0) {
-                    if (window.showToast) window.showToast('Highlight some text and add a remark, or write feedback first', 'error');
-                    return;
-                }
-                var block = resolveReviewBlock(id);
-                if (!block || !block.answer_action) return;
-                // The rejection body rides `feedback` and the SERVER
-                // wraps it with the per-kind stable prefix (plan 16
-                // S2/R4 — the template carries no verdict constants,
-                // any [data-plan-review] container resolves its own
-                // block). The body keeps the historical shape: every
-                // queued remark NUMBERED in queue order, each with its
-                // selection quote prefixed like a comment, then the
-                // free-text feedback.
-                var feedback = list.map(function (item, index) {
-                    return (item.quote
-                        ? '> ' + item.quote.replace(/\n/g, '\n> ') + '\n\n'
-                        : '') + (index + 1) + '. ' + item.body;
-                }).join('\n\n');
-                if (extraText.length > 0) {
-                    feedback += (feedback.length > 0 ? '\n\n' : '') + extraText;
-                }
-                remarksByBlock[id] = [];
-                rejectOpenByBlock[id] = false;
-                statum.act(block.answer_action, { feedback: feedback }).catch(function () { });
-            }
-
+            // The goal's plan-review remarks ride the SHARED
+            // engine (main.html's initPlanReviews, plan 16 R3/arch
+            // 15 — the duplicated ~180-line collect-remarks pattern
+            // now lives once); only the block resolution is page
+            // state.
             // Plan 16 S2: the block for a review container resolves
             // from the ACTUAL [data-plan-review] element — the pending
             // completion review renders as an attention block (NOT the
@@ -259,40 +104,12 @@
                 return block;
             }
 
-            function syncPlanRemarks() {
-                document.querySelectorAll('[data-plan-review]').forEach(function (review) {
-                    var id = review.getAttribute('data-block-id');
-                    var open = rejectOpenByBlock[id] ? '1' : '0';
-                    review.setAttribute('data-plan-open', open);
-                    var feedback = review.querySelector('[data-plan-feedback]');
-                    if (feedback) {
-                        if (rejectOpenByBlock[id]) feedback.removeAttribute('hidden');
-                        else feedback.setAttribute('hidden', '');
-                    }
-                    var list = review.querySelector('[data-plan-remarks]');
-                    if (!list) return;
-                    while (list.firstChild) list.removeChild(list.firstChild);
-                    (remarksByBlock[id] || []).forEach(function (item, index) {
-                        var entry = document.createElement('li');
-                        entry.setAttribute('data-plan-remark-id', String(index));
-                        if (item.quote) {
-                            var quoted = document.createElement('blockquote');
-                            quoted.textContent = item.quote;
-                            entry.appendChild(quoted);
-                        }
-                        var body = document.createElement('div');
-                        body.textContent = item.body;
-                        entry.appendChild(body);
-                        var remove = document.createElement('button');
-                        remove.type = 'button';
-                        remove.className = 'inline';
-                        remove.setAttribute('data-plan-remove', '');
-                        remove.textContent = 'Remove';
-                        entry.appendChild(remove);
-                        list.appendChild(entry);
-                    });
+            if (window.initPlanReviews) {
+                window.initPlanReviews({
+                    resolveBlock: function (id) { return resolveReviewBlock(id); }
                 });
             }
+
         })();
     }
 </script>

+ 215 - 1
src/pages/main.html

@@ -15,7 +15,221 @@
         // a matched container shows a floating button, then a popover that
         // posts the comment with the quoted text via the action the page
         // supplies.
-        if (window.statum) {
+        // Shared UI engines (parameterised per page):
+    //
+    // showToast — every page's toast corner.
+    //
+    // initQuoteComments — the text-selection quote popover + dialog
+    // posting a comment with the quoted text; pages pass a `matches`
+    // node predicate and the action accessor.
+    //
+    // initPlanReviews (plan 16 R3, arch 15 — the ~180-line pattern
+    // that lived duplicated between the task and goal pages, now
+    // goal-only after the task-page plan review died with the poll
+    // era): the plan/completion REVIEW remark queue. "Add remark"
+    // appends to a per-block queue rendered at the bottom of the
+    // review body, persists across statum re-renders (the maps live
+    // outside the render cycle), rides the rejection NUMBERED (each
+    // remark with its selection quote prefixed like a comment), and
+    // clears on accept. Remark-less rejection is allowed when the
+    // feedback textarea carries the substance (plan 16 S2). The
+    // rejection body rides `feedback`; the SERVER wraps the per-kind
+    // stable prefix (R4 — templates carry no verdict constants). The
+    // page supplies `resolveBlock(id)` — how a [data-plan-review]
+    // container's block row resolves from page state.
+    window.initPlanReviews = function (config) {
+        if (!window.statum || !statum.afterEvaluate) return;
+        var resolveBlock = (config && config.resolveBlock)
+            || function () { return null; };
+
+        var remarksByBlock = {};
+        var rejectOpenByBlock = {};
+        var remarkPopover = null;
+
+        statum.afterEvaluate(syncPlanRemarks);
+
+        document.addEventListener('click', function (event) {
+            if (!event.target.closest) return;
+            var accept = event.target.closest('[data-plan-accept]');
+            if (accept) {
+                // Accept ignores/clears the queued remarks.
+                var accepted = accept.closest('[data-plan-review]');
+                if (accepted) {
+                    var acceptId = accepted.getAttribute('data-block-id');
+                    remarksByBlock[acceptId] = [];
+                    rejectOpenByBlock[acceptId] = false;
+                }
+                return;
+            }
+            var toggle = event.target.closest('[data-plan-toggle]');
+            if (toggle) {
+                hideRemarkPopover();
+                var review = toggle.closest('[data-plan-review]');
+                var id = review && review.getAttribute('data-block-id');
+                if (id) rejectOpenByBlock[id] = !rejectOpenByBlock[id];
+                syncPlanRemarks();
+                return;
+            }
+            var submit = event.target.closest('[data-plan-submit]');
+            if (submit) {
+                submitPlanRemarks(submit.closest('[data-plan-review]'));
+                return;
+            }
+            var remove = event.target.closest('[data-plan-remove]');
+            if (remove) {
+                var entry = remove.closest('[data-plan-remark-id]');
+                var blockId = remove.closest('[data-plan-review]').getAttribute('data-block-id');
+                var list = remarksByBlock[blockId] || [];
+                var index = parseInt(entry.getAttribute('data-plan-remark-id'), 10);
+                if (!isNaN(index) && list[index]) list.splice(index, 1);
+                remarksByBlock[blockId] = list;
+                syncPlanRemarks();
+            }
+        });
+
+        document.addEventListener('mouseup', function (event) {
+            if (!event.target.closest) return;
+            if (remarkPopover && remarkPopover.contains(event.target)) return;
+            var body = event.target.closest('[data-plan-body]');
+            if (!body) {
+                hideRemarkPopover();
+                return;
+            }
+            var review = body.closest('[data-plan-review]');
+            if (!review || review.getAttribute('data-plan-open') !== '1') return;
+            var selection = window.getSelection();
+            var text = selection ? String(selection.toString()).trim() : '';
+            if (text.length === 0) {
+                hideRemarkPopover();
+                return;
+            }
+            showRemarkPopover(event.pageX, event.pageY, text, review);
+        });
+
+        document.addEventListener('mousedown', function (event) {
+            if (remarkPopover && event.target.closest
+                    && event.target.closest('.plan-popover')) return;
+            hideRemarkPopover();
+        });
+
+        function showRemarkPopover(x, y, quote, review) {
+            hideRemarkPopover();
+            remarkPopover = document.createElement('div');
+            remarkPopover.className = 'plan-popover';
+            remarkPopover.style.left = Math.min(x, window.innerWidth - 140) + 'px';
+            remarkPopover.style.top = y + 'px';
+
+            var quoted = document.createElement('blockquote');
+            quoted.textContent = quote;
+            remarkPopover.appendChild(quoted);
+
+            var body = document.createElement('textarea');
+            body.placeholder = 'Your remark on the selected plan text';
+            remarkPopover.appendChild(body);
+
+            var actions = document.createElement('div');
+            actions.className = 'actions';
+            var add = document.createElement('button');
+            add.type = 'button';
+            add.className = 'primary';
+            add.textContent = 'Add remark';
+            add.addEventListener('click', function () {
+                if (body.value.trim().length === 0) return;
+                var id = review.getAttribute('data-block-id');
+                var list = remarksByBlock[id] || [];
+                list.push({ quote: quote, body: body.value.trim() });
+                remarksByBlock[id] = list;
+                hideRemarkPopover();
+                syncPlanRemarks();
+            });
+            var cancel = document.createElement('button');
+            cancel.type = 'button';
+            cancel.textContent = 'Cancel';
+            cancel.addEventListener('click', hideRemarkPopover);
+            actions.appendChild(add);
+            actions.appendChild(cancel);
+            remarkPopover.appendChild(actions);
+
+            document.body.appendChild(remarkPopover);
+            body.focus();
+        }
+
+        function hideRemarkPopover() {
+            if (remarkPopover) {
+                remarkPopover.remove();
+                remarkPopover = null;
+            }
+        }
+
+        function submitPlanRemarks(review) {
+            if (!review) return;
+            var id = review.getAttribute('data-block-id');
+            var list = remarksByBlock[id] || [];
+            var extra = review.querySelector('[data-plan-feedback] textarea[name="feedback"]');
+            var extraText = extra && extra.value.trim().length > 0 ? extra.value.trim() : '';
+            // Remark-less rejection is allowed when the feedback
+            // textarea carries the review's substance — one of the
+            // two must be non-empty (plan 16 S2).
+            if (list.length === 0 && extraText.length === 0) {
+                if (window.showToast) window.showToast('Highlight some text and add a remark, or write feedback first', 'error');
+                return;
+            }
+            var block = resolveBlock(id);
+            if (!block || !block.answer_action) return;
+            // The rejection body keeps the historical shape: every
+            // queued remark NUMBERED in queue order, each with its
+            // selection quote prefixed like a comment, then the
+            // free-text feedback.
+            var feedback = list.map(function (item, index) {
+                return (item.quote
+                    ? '> ' + item.quote.replace(/\n/g, '\n> ') + '\n\n'
+                    : '') + (index + 1) + '. ' + item.body;
+            }).join('\n\n');
+            if (extraText.length > 0) {
+                feedback += (feedback.length > 0 ? '\n\n' : '') + extraText;
+            }
+            remarksByBlock[id] = [];
+            rejectOpenByBlock[id] = false;
+            statum.act(block.answer_action, { feedback: feedback }).catch(function () { });
+        }
+
+        function syncPlanRemarks() {
+            document.querySelectorAll('[data-plan-review]').forEach(function (review) {
+                var id = review.getAttribute('data-block-id');
+                var open = rejectOpenByBlock[id] ? '1' : '0';
+                review.setAttribute('data-plan-open', open);
+                var feedback = review.querySelector('[data-plan-feedback]');
+                if (feedback) {
+                    if (rejectOpenByBlock[id]) feedback.removeAttribute('hidden');
+                    else feedback.setAttribute('hidden', '');
+                }
+                var list = review.querySelector('[data-plan-remarks]');
+                if (!list) return;
+                while (list.firstChild) list.removeChild(list.firstChild);
+                (remarksByBlock[id] || []).forEach(function (item, index) {
+                    var entry = document.createElement('li');
+                    entry.setAttribute('data-plan-remark-id', String(index));
+                    if (item.quote) {
+                        var quoted = document.createElement('blockquote');
+                        quoted.textContent = item.quote;
+                        entry.appendChild(quoted);
+                    }
+                    var body = document.createElement('div');
+                    body.textContent = item.body;
+                    entry.appendChild(body);
+                    var remove = document.createElement('button');
+                    remove.type = 'button';
+                    remove.className = 'inline';
+                    remove.setAttribute('data-plan-remove', '');
+                    remove.textContent = 'Remove';
+                    entry.appendChild(remove);
+                    list.appendChild(entry);
+                });
+            });
+        }
+    };
+
+    if (window.statum) {
             (function () {
                 'use strict';