|
@@ -10,436 +10,7 @@
|
|
|
<link rel="icon" type="image/png" pstm-res-href="clanker-saddle.png">
|
|
<link rel="icon" type="image/png" pstm-res-href="clanker-saddle.png">
|
|
|
<link rel="apple-touch-icon" pstm-res-href="clanker-saddle.png">
|
|
<link rel="apple-touch-icon" pstm-res-href="clanker-saddle.png">
|
|
|
<link rel="stm-guard" require="auth != null || location.pathname == '/login'" href="/login">
|
|
<link rel="stm-guard" require="auth != null || location.pathname == '/login'" href="/login">
|
|
|
- <script>
|
|
|
|
|
- // Shared page plumbing. showToast: slide-in notifications (used in
|
|
|
|
|
- // place of alert). initQuoteComments: registers a container matcher
|
|
|
|
|
- // for the text-highlight -> comment popover - selecting text inside
|
|
|
|
|
- // a matched container shows a floating button, then a popover that
|
|
|
|
|
- // posts the comment with the quoted text via the action the page
|
|
|
|
|
- // supplies.
|
|
|
|
|
- // 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);
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- //
|
|
|
|
|
- // initLongFormEditors (plan QoL task 12): in-place editors for the
|
|
|
|
|
- // long-form card fields (task mandate, goal brief). The rendered
|
|
|
|
|
- // markdown view and the editor <form> BOTH live in the page markup;
|
|
|
|
|
- // a per-card `data-lf-open` flag (kept OUTSIDE statum state, the
|
|
|
|
|
- // plan-remarks pattern) toggles which side shows. Why JS-applied
|
|
|
|
|
- // values and not stm-attribute.value: statum's attribute binding
|
|
|
|
|
- // sets the CONTENT attribute, which the browser ignores on
|
|
|
|
|
- // <textarea> (its value lives in the text node) - so the textarea
|
|
|
|
|
- // is re-prefilled from the card's data-value payload after every
|
|
|
|
|
- // render cycle, while the page state carries the source of truth.
|
|
|
|
|
- // The card contract:
|
|
|
|
|
- // [data-long-form] the card root; data-field = slot key
|
|
|
|
|
- // whose object carries the payload
|
|
|
|
|
- // [data-lf-view] the rendered markdown half
|
|
|
|
|
- // [data-lf-edit] the editor half (form)
|
|
|
|
|
- // textarea/input[data-lf-src] a state path ("task.description")
|
|
|
|
|
- // whose current value prefills the
|
|
|
|
|
- // control on every render
|
|
|
|
|
- // [data-lf-toggle] the Edit / Cancel button
|
|
|
|
|
- // Buttons INSIDE the form (Save) submit through the form's
|
|
|
|
|
- // stm-action as usual; toggling is purely presentational, so no
|
|
|
|
|
- // state round-trip and no flicker.
|
|
|
|
|
- //
|
|
|
|
|
- window.initLongFormEditors = function () {
|
|
|
|
|
- if (!window.statum || !statum.afterEvaluate) return;
|
|
|
|
|
-
|
|
|
|
|
- // Resolve a "type.field" state path: statum.read returns the
|
|
|
|
|
- // SLOT's public object, so the first segment is the slot and
|
|
|
|
|
- // the rest walks into it.
|
|
|
|
|
- function readPath(path) {
|
|
|
|
|
- if (!path) return '';
|
|
|
|
|
- var parts = path.split('.');
|
|
|
|
|
- var value = statum.read(parts[0]);
|
|
|
|
|
- for (var i = 1; i < parts.length && value != null; i++) {
|
|
|
|
|
- value = value[parts[i]];
|
|
|
|
|
- }
|
|
|
|
|
- return value === undefined || value === null ? '' : String(value);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function syncCard(card) {
|
|
|
|
|
- var open = card.getAttribute('data-lf-open') === '1';
|
|
|
|
|
- var view = card.querySelector('[data-lf-view]');
|
|
|
|
|
- var editor = card.querySelector('[data-lf-edit]');
|
|
|
|
|
- if (view) {
|
|
|
|
|
- if (open) view.setAttribute('hidden', '');
|
|
|
|
|
- else view.removeAttribute('hidden');
|
|
|
|
|
- }
|
|
|
|
|
- if (editor) {
|
|
|
|
|
- if (open) editor.removeAttribute('hidden');
|
|
|
|
|
- else editor.setAttribute('hidden', '');
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Prefill bookkeeping: applied ONCE per open - a statum
|
|
|
|
|
- // re-render (pulse refresh, action response) rebinds
|
|
|
|
|
- // attributes but never textarea VALUES, so re-applying on
|
|
|
|
|
- // every cycle would clobber the user's in-progress typing.
|
|
|
|
|
- // Closing the card clears the flag; the next open re-prefills
|
|
|
|
|
- // from the (possibly updated) state.
|
|
|
|
|
- var prefilled = {};
|
|
|
|
|
- function applyPrefills() {
|
|
|
|
|
- var cards = document.querySelectorAll('[data-long-form]');
|
|
|
|
|
- Array.prototype.forEach.call(cards, function (card) {
|
|
|
|
|
- syncCard(card);
|
|
|
|
|
- var key = card.getAttribute('data-field') || '';
|
|
|
|
|
- var open = card.getAttribute('data-lf-open') === '1';
|
|
|
|
|
- if (open && !prefilled[key]) {
|
|
|
|
|
- var controls = card.querySelectorAll('[data-lf-src]');
|
|
|
|
|
- Array.prototype.forEach.call(controls, function (control) {
|
|
|
|
|
- var value = readPath(control.getAttribute('data-lf-src'));
|
|
|
|
|
- if (document.activeElement !== control) {
|
|
|
|
|
- control.value = value;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- prefilled[key] = true;
|
|
|
|
|
- }
|
|
|
|
|
- if (!open) delete prefilled[key];
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- document.addEventListener('click', function (event) {
|
|
|
|
|
- if (!event.target.closest) return;
|
|
|
|
|
- var button = event.target.closest('[data-lf-toggle]');
|
|
|
|
|
- if (!button) return;
|
|
|
|
|
- var card = button.closest('[data-long-form]');
|
|
|
|
|
- if (!card) return;
|
|
|
|
|
- event.preventDefault();
|
|
|
|
|
- var open = card.getAttribute('data-lf-open') === '1';
|
|
|
|
|
- if (open) {
|
|
|
|
|
- card.setAttribute('data-lf-open', '0');
|
|
|
|
|
- delete prefilled[card.getAttribute('data-field') || ''];
|
|
|
|
|
- } else {
|
|
|
|
|
- card.setAttribute('data-lf-open', '1');
|
|
|
|
|
- }
|
|
|
|
|
- syncCard(card);
|
|
|
|
|
- // Prefill immediately (the render cycle applies the state
|
|
|
|
|
- // bindings; the textarea's value needs this direct pass).
|
|
|
|
|
- applyPrefills();
|
|
|
|
|
- if (!open) {
|
|
|
|
|
- var area = card.querySelector('textarea');
|
|
|
|
|
- if (area && area.focus) area.focus();
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- statum.afterEvaluate(applyPrefills);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- if (window.statum) {
|
|
|
|
|
- (function () {
|
|
|
|
|
- 'use strict';
|
|
|
|
|
-
|
|
|
|
|
- window.showToast = function (message, kind) {
|
|
|
|
|
- var stack = document.querySelector('.toast-stack');
|
|
|
|
|
- if (!stack) {
|
|
|
|
|
- stack = document.createElement('div');
|
|
|
|
|
- stack.className = 'toast-stack';
|
|
|
|
|
- document.body.appendChild(stack);
|
|
|
|
|
- }
|
|
|
|
|
- var toast = document.createElement('div');
|
|
|
|
|
- toast.className = 'toast ' + (kind || '');
|
|
|
|
|
- toast.textContent = message;
|
|
|
|
|
- stack.appendChild(toast);
|
|
|
|
|
- window.setTimeout(function () {
|
|
|
|
|
- toast.classList.add('leaving');
|
|
|
|
|
- window.setTimeout(function () { toast.remove(); }, 300);
|
|
|
|
|
- }, 5000);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- statum.onNotifyHandler = function (kind, message) {
|
|
|
|
|
- showToast(message || kind, kind);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- var quoteHandlers = [];
|
|
|
|
|
- var quoteButton = null;
|
|
|
|
|
- var quotePopover = null;
|
|
|
|
|
- var quotedText = '';
|
|
|
|
|
- var activeHandler = null;
|
|
|
|
|
-
|
|
|
|
|
- window.initQuoteComments = function (handler) {
|
|
|
|
|
- quoteHandlers.push(handler);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- document.addEventListener('mouseup', function (event) {
|
|
|
|
|
- if (quoteButton && quoteButton.contains(event.target)) return;
|
|
|
|
|
- var selection = window.getSelection();
|
|
|
|
|
- var text = selection ? String(selection.toString()).trim() : '';
|
|
|
|
|
- activeHandler = matchHandler(event.target);
|
|
|
|
|
- if (text.length === 0 || !activeHandler) {
|
|
|
|
|
- hideQuoteUi();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- quotedText = text;
|
|
|
|
|
- showQuoteButton(event.pageX, event.pageY);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- document.addEventListener('mousedown', function (event) {
|
|
|
|
|
- if (quoteButton && quoteButton.contains(event.target)) return;
|
|
|
|
|
- if (quotePopover && quotePopover.contains(event.target)) return;
|
|
|
|
|
- hideQuoteUi();
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- function matchHandler(node) {
|
|
|
|
|
- for (var i = 0; i < quoteHandlers.length; i++) {
|
|
|
|
|
- if (quoteHandlers[i].matches(node)) return quoteHandlers[i];
|
|
|
|
|
- }
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function showQuoteButton(x, y) {
|
|
|
|
|
- hideQuoteUi();
|
|
|
|
|
- quoteButton = document.createElement('button');
|
|
|
|
|
- quoteButton.type = 'button';
|
|
|
|
|
- quoteButton.className = 'quote-button';
|
|
|
|
|
- quoteButton.textContent = 'Comment on this';
|
|
|
|
|
- quoteButton.style.left = Math.max(x - 60, 8) + 'px';
|
|
|
|
|
- quoteButton.style.top = (y - 36) + 'px';
|
|
|
|
|
- quoteButton.addEventListener('click', function (event) {
|
|
|
|
|
- event.preventDefault();
|
|
|
|
|
- openQuoteDialog();
|
|
|
|
|
- });
|
|
|
|
|
- document.body.appendChild(quoteButton);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function openQuoteDialog() {
|
|
|
|
|
- if (quoteButton) { quoteButton.remove(); quoteButton = null; }
|
|
|
|
|
- var dialog = document.getElementById('quote-comment-dialog');
|
|
|
|
|
- if (!dialog) return;
|
|
|
|
|
- var quote = dialog.querySelector('#quote-text');
|
|
|
|
|
- var field = dialog.querySelector('input[name="quote"]');
|
|
|
|
|
- if (quote) quote.textContent = quotedText;
|
|
|
|
|
- if (field) field.value = quotedText;
|
|
|
|
|
- var returnField = dialog.querySelector('input[name="return_to"]');
|
|
|
|
|
- if (returnField) returnField.value = location.pathname + location.search;
|
|
|
|
|
- if (typeof dialog.showModal === 'function') {
|
|
|
|
|
- dialog.showModal();
|
|
|
|
|
- } else {
|
|
|
|
|
- dialog.setAttribute('open', '');
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function hideQuoteUi() {
|
|
|
|
|
- if (quoteButton) { quoteButton.remove(); quoteButton = null; }
|
|
|
|
|
- if (quotePopover) { quotePopover.remove(); quotePopover = null; }
|
|
|
|
|
- activeHandler = null;
|
|
|
|
|
- }
|
|
|
|
|
- })();
|
|
|
|
|
- }
|
|
|
|
|
- </script>
|
|
|
|
|
|
|
+ <script pstm-res-src="app.js"></script>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
|
<header>
|
|
<header>
|
|
@@ -447,18 +18,33 @@
|
|
|
<nav>
|
|
<nav>
|
|
|
<a href="/">Dashboard</a>
|
|
<a href="/">Dashboard</a>
|
|
|
<!-- spry:nav-begin -->
|
|
<!-- spry:nav-begin -->
|
|
|
- <a href="/projects">Projects</a>
|
|
|
|
|
- <a href="/tasks">Tasks</a>
|
|
|
|
|
- <a href="/goals">Goals</a>
|
|
|
|
|
- <a href="/saddles">Saddles</a>
|
|
|
|
|
- <a href="/rules">Rules</a>
|
|
|
|
|
- <a href="/stacks">Stacks</a>
|
|
|
|
|
- <!-- The users link is display-guarded (the page's entrypoint
|
|
|
|
|
- guard is authoritative); auth.permissions rides the
|
|
|
|
|
- signed auth slot, so an admin sees it everywhere. -->
|
|
|
|
|
- <a stm-if="auth != null && auth.permissions.indexOf('admin') != -1" href="/users">Users</a>
|
|
|
|
|
|
|
+ <details class="nav-menu" name="nav-menu">
|
|
|
|
|
+ <summary>Work</summary>
|
|
|
|
|
+ <div class="nav-dropdown">
|
|
|
|
|
+ <a href="/tasks">Tasks</a>
|
|
|
|
|
+ <a class="nav-goal" href="/goals"><em class="goal-icon">🎯</em> Goals</a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </details>
|
|
|
|
|
+ <details class="nav-menu" name="nav-menu">
|
|
|
|
|
+ <summary>Library</summary>
|
|
|
|
|
+ <div class="nav-dropdown">
|
|
|
|
|
+ <a href="/projects">Projects</a>
|
|
|
|
|
+ <a href="/rules">Rules</a>
|
|
|
|
|
+ <a href="/stacks">Stacks</a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </details>
|
|
|
|
|
+ <details class="nav-menu" name="nav-menu">
|
|
|
|
|
+ <summary>Admin</summary>
|
|
|
|
|
+ <div class="nav-dropdown">
|
|
|
|
|
+ <a href="/saddles">Saddles</a>
|
|
|
|
|
+ <!-- The users link is display-guarded (the page's entrypoint
|
|
|
|
|
+ guard is authoritative); auth.permissions rides the
|
|
|
|
|
+ signed auth slot, so an admin sees it everywhere. -->
|
|
|
|
|
+ <a stm-if="auth != null && auth.permissions.indexOf('admin') != -1" href="/users">Users</a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </details>
|
|
|
<!-- spry:nav-end -->
|
|
<!-- spry:nav-end -->
|
|
|
- <span stm-if="auth != null"><strong stm-text="auth.username">user</strong>
|
|
|
|
|
|
|
+ <span stm-if="auth != null" class="nav-auth"><strong stm-text="auth.username">user</strong>
|
|
|
<a href="/profile" title="Your profile">Profile</a>
|
|
<a href="/profile" title="Your profile">Profile</a>
|
|
|
<button type="button" class="inline" stm-action="auth.logout">Log out</button>
|
|
<button type="button" class="inline" stm-action="auth.logout">Log out</button>
|
|
|
<!-- spry:nav-auth-begin -->
|
|
<!-- spry:nav-auth-begin -->
|