annotepage-client 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/HASHES.txt +1 -0
- package/dist/annotepage.js +138 -3
- package/labels/fr.json +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Load the client once, on a page of the site, with `data-setup` and **without**
|
|
|
34
34
|
|
|
35
35
|
```html
|
|
36
36
|
<script src="https://<your-cdn>/annotepage-client@2.5.0/dist/annotepage.js"
|
|
37
|
-
integrity="sha384-
|
|
37
|
+
integrity="sha384-i7lpLUYLSVSOFMi+9W+EMhMbYDe7Nff/TF2vUuyQvVQ+XHph8rGJDXn2s4aNPSOT"
|
|
38
38
|
crossorigin="anonymous"
|
|
39
39
|
data-server="https://<your-server>/annotepage/api.php"
|
|
40
40
|
data-setup
|
|
@@ -55,7 +55,7 @@ on the server. No network request is made at that point.
|
|
|
55
55
|
|
|
56
56
|
```html
|
|
57
57
|
<script src="https://<your-cdn>/annotepage-client@2.5.0/dist/annotepage.js"
|
|
58
|
-
integrity="sha384-
|
|
58
|
+
integrity="sha384-i7lpLUYLSVSOFMi+9W+EMhMbYDe7Nff/TF2vUuyQvVQ+XHph8rGJDXn2s4aNPSOT"
|
|
59
59
|
crossorigin="anonymous"
|
|
60
60
|
data-server="https://<your-server>/annotepage/api.php"
|
|
61
61
|
data-project="7Qb1kZ3xNvA9dLpEqKf2Zt"
|
package/dist/HASHES.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
2.7.0 sha384-i7lpLUYLSVSOFMi+9W+EMhMbYDe7Nff/TF2vUuyQvVQ+XHph8rGJDXn2s4aNPSOT 175916 bytes
|
|
1
2
|
2.6.0 sha384-PVE6tjzC72/VHyRLdxHLpwMLIp9ji+VUBapD+j3vmoi69rpkXn0h+ZicxwORUdQt 167714 bytes
|
|
2
3
|
2.5.0 sha384-8bHU9VMghgfLL4PSwE3XJZXcDYqhd5SlUE31yMgaI/2ZNhH3Z18he6X7BsF7kvx1 167714 bytes
|
|
3
4
|
2.4.0 sha384-RbO8lASKNckSOum5Gm462DTZ8y6/+yR6ZFKyphrAe9Hf3x95tJ57eY/6GX4XfBJr 160669 bytes
|
package/dist/annotepage.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
2
|
annotepage -- the annotation layer, browser side.
|
|
3
3
|
|
|
4
|
-
Package version : 2.
|
|
4
|
+
Package version : 2.7.0
|
|
5
5
|
Format version : 2 (see FORMAT.md)
|
|
6
6
|
Licence : MIT
|
|
7
7
|
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
/* Injected by the build: they come from package.json and from
|
|
17
17
|
src/styles.css, so that no value is written in two places and can
|
|
18
18
|
therefore diverge. */
|
|
19
|
-
const TOOL_VERSION = "2.
|
|
19
|
+
const TOOL_VERSION = "2.7.0";
|
|
20
20
|
const FORMAT = 2;
|
|
21
|
-
const STYLES = "/* ============================================================================\n styles.css -- THE STYLES OF THE TOOL, AND OF NO OTHER ELEMENT.\n\n This sheet is INLINED into the served file by the build, then put into the\n tool's shadow root -- as a constructed sheet when the browser can do it, in\n a <style> otherwise. It was loaded by a <link> in the original tool; the\n move to a CDN under SRI brought it inside the file, so that there is only\n one digest to keep up to date. The containment itself has not changed, and\n is still twofold:\n\n - from the tool towards the site: no rule from here can reach an element\n of the host site, the browser sees to that. That is what makes the\n claim \"the layer touches nothing\" checkable rather than promised;\n - from the site towards the tool: no rule of the site can reach an\n element here. A redesign of the site's stylesheet therefore cannot\n distort the tool, nor the other way round.\n\n The \"ap-\" prefix on every class is the third safeguard: the day somebody\n loads these styles WITHOUT a shadow root -- by mistake, or to debug --\n nothing would answer a selector of the site.\n\n NO RULE TARGETS html, body, * OR ANY SELECTOR OF THE SITE. That is the one\n absolute prohibition of this file.\n\n COLOURS: the tool has its OWN palette, defined on the shadow root. It\n reads neither the site's variables nor its theme attribute: it has no\n reason to know how the site names its colours, and it must stay readable\n on a light site as on a dark one. The switch follows the system\n preference, the only information the tool has without asking anyone.\n ============================================================================ */\n\n\n:host {\n --ap-bg: #ffffff;\n --ap-bg-soft: #f4f6f8;\n --ap-bg-raised: #e9edf2;\n --ap-text: #1a1d21;\n --ap-text-soft: #5b6570;\n --ap-border: #d5dbe2;\n --ap-accent: #2f6fed;\n --ap-accent-dark: #1d55c8;\n --ap-accent-text: #ffffff;\n --ap-accent-veil: rgba(47, 111, 237, 0.14);\n --ap-alert-bg: #fdeceb;\n --ap-alert-border: #e3a9a4;\n --ap-alert-text: #8a1f16;\n --ap-shadow: 0 6px 24px rgba(16, 24, 40, 0.18);\n --ap-radius: 10px;\n --ap-font: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\",\n Arial, sans-serif;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n --ap-bg: #1d2126;\n --ap-bg-soft: #262b32;\n --ap-bg-raised: #323942;\n --ap-text: #e9ecf0;\n --ap-text-soft: #a4adb8;\n --ap-border: #3a424c;\n --ap-accent: #6d9bff;\n --ap-accent-dark: #8fb4ff;\n --ap-accent-text: #10151c;\n --ap-accent-veil: rgba(109, 155, 255, 0.18);\n --ap-alert-bg: #3a1f1c;\n --ap-alert-border: #7c3a33;\n --ap-alert-text: #ffb9b1;\n --ap-shadow: 0 6px 24px rgba(0, 0, 0, 0.55);\n }\n}\n\n/* ----------------------------------------------------------------------------\n The layer.\n\n It covers the viewport and receives NO click: that is what lets the page\n behave exactly as usual as long as the tool is not in annotation mode.\n Each widget re-enables clicks for itself alone.\n ---------------------------------------------------------------------------- */\n\n.ap-layer {\n position: absolute;\n inset: 0;\n pointer-events: none;\n font-family: var(--ap-font);\n font-size: 14px;\n line-height: 1.45;\n color: var(--ap-text);\n text-align: left;\n -webkit-font-smoothing: antialiased;\n}\n\n.ap-layer button,\n.ap-layer input,\n.ap-layer textarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n color: inherit;\n margin: 0;\n box-sizing: border-box;\n}\n\n/* ----------------------------------------------------------------------------\n The button: the only thing visible when the tool is at rest.\n ---------------------------------------------------------------------------- */\n\n.ap-button {\n position: fixed;\n right: 16px;\n bottom: 16px;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 9px 14px;\n border: 1px solid var(--ap-border);\n border-radius: 999px;\n background: var(--ap-bg);\n color: var(--ap-text);\n box-shadow: var(--ap-shadow);\n cursor: pointer;\n pointer-events: auto;\n opacity: 0.92;\n transition: opacity 0.15s ease, transform 0.15s ease;\n}\n\n.ap-button:hover,\n.ap-button:focus-visible {\n opacity: 1;\n transform: translateY(-1px);\n}\n\n.ap-button:focus-visible {\n outline: 2px solid var(--ap-accent);\n outline-offset: 2px;\n}\n\n.ap-button[aria-pressed=\"true\"] {\n background: var(--ap-accent);\n border-color: var(--ap-accent);\n color: var(--ap-accent-text);\n opacity: 1;\n}\n\n.ap-button-dot {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--ap-accent);\n flex: none;\n}\n\n.ap-button[aria-pressed=\"true\"] .ap-button-dot {\n background: var(--ap-accent-text);\n}\n\n.ap-button-count {\n padding: 1px 7px;\n border-radius: 999px;\n background: var(--ap-bg-raised);\n color: var(--ap-text-soft);\n font-size: 12px;\n}\n\n.ap-button[aria-pressed=\"true\"] .ap-button-count {\n background: rgba(255, 255, 255, 0.22);\n color: var(--ap-accent-text);\n}\n\n/* ----------------------------------------------------------------------------\n The pointing highlight.\n\n It is DRAWN HERE, from the coordinates of the element being pointed at.\n Nothing is put on the element itself: no class, no attribute, no style. So\n the site cannot move by a single pixel because of the pointing.\n ---------------------------------------------------------------------------- */\n\n.ap-highlight {\n position: fixed;\n border: 2px solid var(--ap-accent);\n border-radius: 3px;\n background: var(--ap-accent-veil);\n pointer-events: none;\n display: none;\n}\n\n.ap-highlight-label {\n position: fixed;\n max-width: 320px;\n padding: 4px 8px;\n border-radius: 6px;\n background: var(--ap-accent);\n color: var(--ap-accent-text);\n font-size: 12px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n pointer-events: none;\n display: none;\n box-shadow: var(--ap-shadow);\n}\n\n/* ----------------------------------------------------------------------------\n The markers: \"there are already notes here\".\n ---------------------------------------------------------------------------- */\n\n.ap-marker {\n position: fixed;\n min-width: 22px;\n height: 22px;\n padding: 0 6px;\n border: 2px solid var(--ap-bg);\n border-radius: 999px;\n background: var(--ap-accent);\n color: var(--ap-accent-text);\n font-size: 12px;\n font-weight: 700;\n line-height: 18px;\n text-align: center;\n cursor: pointer;\n pointer-events: auto;\n box-shadow: var(--ap-shadow);\n}\n\n.ap-marker:focus-visible {\n outline: 2px solid var(--ap-accent-dark);\n outline-offset: 2px;\n}\n\n/* ----------------------------------------------------------------------------\n The panel.\n ---------------------------------------------------------------------------- */\n\n.ap-panel {\n position: fixed;\n top: 12px;\n right: 12px;\n bottom: 72px;\n width: 360px;\n max-width: calc(100vw - 24px);\n display: none;\n flex-direction: column;\n border: 1px solid var(--ap-border);\n border-radius: var(--ap-radius);\n background: var(--ap-bg);\n box-shadow: var(--ap-shadow);\n pointer-events: auto;\n overflow: hidden;\n}\n\n.ap-panel.ap-open {\n display: flex;\n}\n\n.ap-panel-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--ap-border);\n background: var(--ap-bg-soft);\n}\n\n.ap-panel-title {\n font-size: 15px;\n font-weight: 600;\n flex: 1 1 auto;\n}\n\n.ap-panel-instructions {\n padding: 10px 14px;\n border-bottom: 1px solid var(--ap-border);\n color: var(--ap-text-soft);\n font-size: 13px;\n}\n\n.ap-panel-body {\n flex: 1 1 auto;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 4px 14px 14px;\n}\n\n.ap-panel-footer {\n padding: 8px 14px;\n border-top: 1px solid var(--ap-border);\n background: var(--ap-bg-soft);\n color: var(--ap-text-soft);\n font-size: 12px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.ap-section-title {\n margin: 14px 0 6px;\n color: var(--ap-text-soft);\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.ap-section-help {\n margin: 0 0 8px;\n color: var(--ap-text-soft);\n font-size: 12px;\n}\n\n.ap-empty {\n margin: 16px 0;\n color: var(--ap-text-soft);\n}\n\n/* The standing mention of a public key. Deliberately NOT the alert colours:\n this is not a failure and it is on screen for ever -- an alarm that never\n goes away stops being read. It is a fact about the project, stated in the\n panel's own tone, and it stays at the top of every draw. */\n/* The public-key notice and the \"a newer client exists\" line are the same\n object on screen: a standing statement about what one is looking at, above\n the notes and above the failures. One rule, so they cannot drift apart. */\n.ap-public,\n.ap-upgrade {\n margin: 0 0 10px;\n padding: 8px 10px;\n border: 1px solid var(--ap-border);\n border-radius: var(--ap-radius);\n background: var(--ap-bg-soft);\n color: var(--ap-text-soft);\n font-size: 12px;\n line-height: 1.45;\n}\n\n/* ----------------------------------------------------------------------------\n A note, and its replies.\n ---------------------------------------------------------------------------- */\n\n.ap-note {\n margin: 8px 0;\n padding: 10px 12px;\n border: 1px solid var(--ap-border);\n border-radius: var(--ap-radius);\n background: var(--ap-bg);\n}\n\n.ap-note.ap-orphan {\n background: var(--ap-bg-soft);\n}\n\n.ap-note.ap-focused {\n border-color: var(--ap-accent);\n box-shadow: 0 0 0 3px var(--ap-accent-veil);\n}\n\n.ap-note-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ap-note-author {\n font-weight: 600;\n}\n\n.ap-note-date {\n color: var(--ap-text-soft);\n font-size: 12px;\n}\n\n.ap-note-target {\n margin: 4px 0 0;\n color: var(--ap-text-soft);\n font-size: 12px;\n font-style: italic;\n overflow-wrap: anywhere;\n}\n\n.ap-note-text {\n margin: 6px 0 0;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n.ap-note-actions {\n margin-top: 8px;\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ap-replies {\n margin: 8px 0 0;\n padding-left: 10px;\n border-left: 2px solid var(--ap-border);\n}\n\n.ap-reply {\n margin: 8px 0 0;\n}\n\n/* ----------------------------------------------------------------------------\n The form, anchored near the element pointed at.\n ---------------------------------------------------------------------------- */\n\n.ap-form {\n position: fixed;\n width: 340px;\n max-width: calc(100vw - 24px);\n display: none;\n flex-direction: column;\n gap: 8px;\n padding: 14px;\n border: 1px solid var(--ap-border);\n border-radius: var(--ap-radius);\n background: var(--ap-bg);\n box-shadow: var(--ap-shadow);\n pointer-events: auto;\n}\n\n.ap-form.ap-open {\n display: flex;\n}\n\n.ap-form-title {\n font-size: 15px;\n font-weight: 600;\n}\n\n.ap-form-target {\n color: var(--ap-text-soft);\n font-size: 12px;\n font-style: italic;\n overflow-wrap: anywhere;\n}\n\n.ap-label {\n display: block;\n margin-bottom: 3px;\n font-size: 12px;\n font-weight: 600;\n color: var(--ap-text-soft);\n}\n\n.ap-help {\n margin: 3px 0 0;\n font-size: 12px;\n color: var(--ap-text-soft);\n}\n\n.ap-field,\n.ap-area {\n width: 100%;\n padding: 8px 10px;\n border: 1px solid var(--ap-border);\n border-radius: 8px;\n background: var(--ap-bg-soft);\n color: var(--ap-text);\n}\n\n.ap-field:focus,\n.ap-area:focus {\n outline: 2px solid var(--ap-accent);\n outline-offset: 1px;\n}\n\n.ap-area {\n min-height: 92px;\n resize: vertical;\n}\n\n.ap-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ap-counter {\n margin-left: auto;\n font-size: 12px;\n color: var(--ap-text-soft);\n}\n\n/* ----------------------------------------------------------------------------\n Buttons.\n ---------------------------------------------------------------------------- */\n\n.ap-primary,\n.ap-secondary,\n.ap-link {\n border-radius: 8px;\n cursor: pointer;\n pointer-events: auto;\n}\n\n.ap-primary {\n padding: 8px 14px;\n border: 1px solid var(--ap-accent);\n background: var(--ap-accent);\n color: var(--ap-accent-text);\n font-weight: 600;\n}\n\n.ap-primary:hover {\n background: var(--ap-accent-dark);\n border-color: var(--ap-accent-dark);\n}\n\n.ap-secondary {\n padding: 8px 14px;\n border: 1px solid var(--ap-border);\n background: var(--ap-bg);\n color: var(--ap-text);\n}\n\n.ap-secondary:hover {\n background: var(--ap-bg-raised);\n}\n\n.ap-link {\n padding: 2px 4px;\n border: 0;\n background: none;\n color: var(--ap-accent);\n text-decoration: underline;\n font-size: 13px;\n}\n\n.ap-primary:disabled,\n.ap-secondary:disabled,\n.ap-link:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.ap-primary:focus-visible,\n.ap-secondary:focus-visible,\n.ap-link:focus-visible {\n outline: 2px solid var(--ap-accent);\n outline-offset: 2px;\n}\n\n/* ----------------------------------------------------------------------------\n The failures.\n\n They are RED, at the top of the block concerned, and carry the message the\n server returned as it stands: that is how a non-technical team learns that\n its remark is not saved, instead of believing it is.\n ---------------------------------------------------------------------------- */\n\n.ap-error {\n margin: 8px 0;\n padding: 10px 12px;\n border: 1px solid var(--ap-alert-border);\n border-radius: var(--ap-radius);\n background: var(--ap-alert-bg);\n color: var(--ap-alert-text);\n}\n\n.ap-error-title {\n font-weight: 700;\n margin-bottom: 4px;\n}\n\n.ap-error-detail {\n margin: 6px 0 0;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font-size: 13px;\n}\n\n.ap-error .ap-link {\n color: var(--ap-alert-text);\n}\n\n/* ----------------------------------------------------------------------------\n Narrow: the panel takes the full width, and so does the form.\n ---------------------------------------------------------------------------- */\n\n/* ----------------------------------------------------------------------------\n Narrow.\n\n DEFECT OBSERVED at 375 px wide: a panel taking the full height covers the\n whole page, and no element can be pointed at any more -- every click lands\n on the panel. So it becomes a bottom band, which leaves the top half of\n the viewport free; one scrolls the page there to bring the wanted element\n into view. The form, for its part, hides the panel while typing (see\n notes.js): on a screen that size, writing and reading the list at the same\n time does not hold.\n ---------------------------------------------------------------------------- */\n\n/* On a narrow screen the panel becomes a bottom band and the form takes the\n full width.\n\n THE WIDTH CEILING IS KEPT, and it comes from a measured defect: \"left: 8;\n right: 8\" sizes the element against its CONTAINING BLOCK, which the host\n site's horizontal overflow can make wider than the visible window.\n Measured, in mobile emulation at 390 px: the site overflows to 407 px\n (with the tool and without it), and the panel came out 391 px wide\n starting at 8, that is 9 px off screen. \"100vw\" is the window, not the\n containing block: the ceiling therefore does nothing when the site does\n not overflow, and pulls the width back when it does. */\n@media (max-width: 560px) {\n .ap-panel {\n top: auto;\n right: 8px;\n left: 8px;\n bottom: 66px;\n height: 52vh;\n width: auto;\n max-width: calc(100vw - 16px);\n }\n\n .ap-form {\n left: 8px;\n right: 8px;\n width: auto;\n max-width: calc(100vw - 16px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ap-button {\n transition: none;\n }\n}\n\n/* The failure shows without opening the panel: the button's dot changes\n colour. A team that does not click must be able to see that something is\n wrong. */\n.ap-button.ap-failed .ap-button-dot {\n background: var(--ap-alert-text);\n}\n\n.ap-button.ap-failed {\n border-color: var(--ap-alert-border);\n}\n\n/* Signature reminder, in the note form.\n The name was shown at the foot of the panel only: invisible at the moment\n one writes. A user reported not knowing which name they were writing\n under. */\n.ap-form-signature {\n display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;\n margin: 0 0 .6rem; font-size: .85rem; opacity: .8;\n}\n\n/* Resolution state, said on the card.\n Two cases NOT to be confused: resolved and online, resolved but not\n deployed yet. The second keeps the defect on the reviewer's screen; hiding\n it or announcing it as fixed would cost them their trust in the tool. */\n.ap-state-mark {\n display: inline-block; margin: 0 0 .5rem;\n padding: .15rem .55rem; border-radius: 4px;\n font-size: .75rem; font-weight: 600; letter-spacing: .02em;\n}\n.ap-note.ap-resolved { opacity: .72; }\n.ap-note.ap-resolved .ap-state-mark {\n color: #0f7a52; background: rgba(16, 185, 129, .14);\n}\n.ap-note.ap-resolved-pending .ap-state-mark {\n color: #8a5a00; background: rgba(245, 158, 11, .16);\n}\n/* The \"it is fixed\" / \"reopen\" block, opened under the card. Same shape as\n the reply block: it is the same gesture, one answers a remark. */\n.ap-resolve,\n.ap-reply-form {\n margin-top: .6rem;\n padding-top: .6rem;\n border-top: 1px solid var(--ap-border);\n}\n\n/* The question asked before the key is dropped, at the foot of the list.\n Framed like the resolution block -- it is the same shape of gesture, one\n answers before something changes -- and set apart from the notes above it,\n because it is not about a note. */\n.ap-forget {\n margin-top: 1rem;\n padding-top: .6rem;\n border-top: 1px solid var(--ap-border);\n}\n\n.ap-forget .ap-actions {\n margin-top: .5rem;\n}\n\n.ap-history-toggle {\n display: block; width: 100%; margin: 1rem 0 .25rem;\n padding: .5rem .75rem; border: 1px dashed currentColor; border-radius: 6px;\n background: none; color: inherit; font: inherit; opacity: .7; cursor: pointer;\n}\n.ap-history-toggle:hover { opacity: 1; }\n\n\n/* ----------------------------------------------------------------------------\n Setup and pasting the salt.\n\n These are the only screens where something is copied by hand. Everything\n there is SELECTABLE and monospaced: a 43-character salt copied wrong\n cannot be recovered, and nothing helps less than a font that confuses I, l\n and 1.\n ---------------------------------------------------------------------------- */\n\n.ap-panel-wide {\n width: 560px;\n}\n\n.ap-copy {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n margin: 0 0 12px;\n}\n\n.ap-code {\n flex: 1 1 auto;\n width: 100%;\n padding: 8px 10px;\n border: 1px solid var(--ap-border);\n border-radius: 8px;\n background: var(--ap-bg-soft);\n color: var(--ap-text);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\",\n monospace;\n font-size: 12.5px;\n line-height: 1.5;\n resize: vertical;\n white-space: pre;\n overflow-x: auto;\n}\n\n.ap-code:focus-visible {\n outline: 2px solid var(--ap-accent);\n outline-offset: 1px;\n}\n\n@media (max-width: 560px) {\n .ap-panel-wide {\n width: auto;\n }\n\n .ap-copy {\n flex-direction: column;\n }\n}\n";
|
|
21
|
+
const STYLES = "/* ============================================================================\n styles.css -- THE STYLES OF THE TOOL, AND OF NO OTHER ELEMENT.\n\n This sheet is INLINED into the served file by the build, then put into the\n tool's shadow root -- as a constructed sheet when the browser can do it, in\n a <style> otherwise. It was loaded by a <link> in the original tool; the\n move to a CDN under SRI brought it inside the file, so that there is only\n one digest to keep up to date. The containment itself has not changed, and\n is still twofold:\n\n - from the tool towards the site: no rule from here can reach an element\n of the host site, the browser sees to that. That is what makes the\n claim \"the layer touches nothing\" checkable rather than promised;\n - from the site towards the tool: no rule of the site can reach an\n element here. A redesign of the site's stylesheet therefore cannot\n distort the tool, nor the other way round.\n\n The \"ap-\" prefix on every class is the third safeguard: the day somebody\n loads these styles WITHOUT a shadow root -- by mistake, or to debug --\n nothing would answer a selector of the site.\n\n NO RULE TARGETS html, body, * OR ANY SELECTOR OF THE SITE. That is the one\n absolute prohibition of this file.\n\n COLOURS: the tool has its OWN palette, defined on the shadow root. It\n reads neither the site's variables nor its theme attribute: it has no\n reason to know how the site names its colours, and it must stay readable\n on a light site as on a dark one. The switch follows the system\n preference, the only information the tool has without asking anyone.\n ============================================================================ */\n\n\n:host {\n --ap-bg: #ffffff;\n --ap-bg-soft: #f4f6f8;\n --ap-bg-raised: #e9edf2;\n --ap-text: #1a1d21;\n --ap-text-soft: #5b6570;\n --ap-border: #d5dbe2;\n --ap-accent: #2f6fed;\n --ap-accent-dark: #1d55c8;\n --ap-accent-text: #ffffff;\n --ap-accent-veil: rgba(47, 111, 237, 0.14);\n --ap-alert-bg: #fdeceb;\n --ap-alert-border: #e3a9a4;\n --ap-alert-text: #8a1f16;\n --ap-shadow: 0 6px 24px rgba(16, 24, 40, 0.18);\n --ap-radius: 10px;\n --ap-font: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\",\n Arial, sans-serif;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n --ap-bg: #1d2126;\n --ap-bg-soft: #262b32;\n --ap-bg-raised: #323942;\n --ap-text: #e9ecf0;\n --ap-text-soft: #a4adb8;\n --ap-border: #3a424c;\n --ap-accent: #6d9bff;\n --ap-accent-dark: #8fb4ff;\n --ap-accent-text: #10151c;\n --ap-accent-veil: rgba(109, 155, 255, 0.18);\n --ap-alert-bg: #3a1f1c;\n --ap-alert-border: #7c3a33;\n --ap-alert-text: #ffb9b1;\n --ap-shadow: 0 6px 24px rgba(0, 0, 0, 0.55);\n }\n}\n\n/* ----------------------------------------------------------------------------\n The layer.\n\n It covers the viewport and receives NO click: that is what lets the page\n behave exactly as usual as long as the tool is not in annotation mode.\n Each widget re-enables clicks for itself alone.\n ---------------------------------------------------------------------------- */\n\n.ap-layer {\n position: absolute;\n inset: 0;\n pointer-events: none;\n font-family: var(--ap-font);\n font-size: 14px;\n line-height: 1.45;\n color: var(--ap-text);\n text-align: left;\n -webkit-font-smoothing: antialiased;\n}\n\n.ap-layer button,\n.ap-layer input,\n.ap-layer textarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n color: inherit;\n margin: 0;\n box-sizing: border-box;\n}\n\n/* ----------------------------------------------------------------------------\n The button: the only thing visible when the tool is at rest.\n ---------------------------------------------------------------------------- */\n\n.ap-button {\n position: fixed;\n right: 16px;\n bottom: 16px;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 9px 14px;\n border: 1px solid var(--ap-border);\n border-radius: 999px;\n background: var(--ap-bg);\n color: var(--ap-text);\n box-shadow: var(--ap-shadow);\n cursor: pointer;\n pointer-events: auto;\n opacity: 0.92;\n transition: opacity 0.15s ease, transform 0.15s ease;\n}\n\n.ap-button:hover,\n.ap-button:focus-visible {\n opacity: 1;\n transform: translateY(-1px);\n}\n\n.ap-button:focus-visible {\n outline: 2px solid var(--ap-accent);\n outline-offset: 2px;\n}\n\n.ap-button[aria-pressed=\"true\"] {\n background: var(--ap-accent);\n border-color: var(--ap-accent);\n color: var(--ap-accent-text);\n opacity: 1;\n}\n\n.ap-button-dot {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--ap-accent);\n flex: none;\n}\n\n.ap-button[aria-pressed=\"true\"] .ap-button-dot {\n background: var(--ap-accent-text);\n}\n\n.ap-button-count {\n padding: 1px 7px;\n border-radius: 999px;\n background: var(--ap-bg-raised);\n color: var(--ap-text-soft);\n font-size: 12px;\n}\n\n.ap-button[aria-pressed=\"true\"] .ap-button-count {\n background: rgba(255, 255, 255, 0.22);\n color: var(--ap-accent-text);\n}\n\n/* ----------------------------------------------------------------------------\n The pointing highlight.\n\n It is DRAWN HERE, from the coordinates of the element being pointed at.\n Nothing is put on the element itself: no class, no attribute, no style. So\n the site cannot move by a single pixel because of the pointing.\n ---------------------------------------------------------------------------- */\n\n.ap-highlight {\n position: fixed;\n border: 2px solid var(--ap-accent);\n border-radius: 3px;\n background: var(--ap-accent-veil);\n pointer-events: none;\n display: none;\n}\n\n.ap-highlight-label {\n position: fixed;\n max-width: 320px;\n padding: 4px 8px;\n border-radius: 6px;\n background: var(--ap-accent);\n color: var(--ap-accent-text);\n font-size: 12px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n pointer-events: none;\n display: none;\n box-shadow: var(--ap-shadow);\n}\n\n/* ----------------------------------------------------------------------------\n The markers: \"there are already notes here\".\n ---------------------------------------------------------------------------- */\n\n.ap-marker {\n position: fixed;\n min-width: 22px;\n height: 22px;\n padding: 0 6px;\n border: 2px solid var(--ap-bg);\n border-radius: 999px;\n background: var(--ap-accent);\n color: var(--ap-accent-text);\n font-size: 12px;\n font-weight: 700;\n line-height: 18px;\n text-align: center;\n cursor: pointer;\n pointer-events: auto;\n box-shadow: var(--ap-shadow);\n}\n\n.ap-marker:focus-visible {\n outline: 2px solid var(--ap-accent-dark);\n outline-offset: 2px;\n}\n\n/* ----------------------------------------------------------------------------\n The panel.\n ---------------------------------------------------------------------------- */\n\n.ap-panel {\n position: fixed;\n top: 12px;\n right: 12px;\n bottom: 72px;\n width: 360px;\n max-width: calc(100vw - 24px);\n display: none;\n flex-direction: column;\n border: 1px solid var(--ap-border);\n border-radius: var(--ap-radius);\n background: var(--ap-bg);\n box-shadow: var(--ap-shadow);\n pointer-events: auto;\n overflow: hidden;\n}\n\n.ap-panel.ap-open {\n display: flex;\n}\n\n.ap-panel-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--ap-border);\n background: var(--ap-bg-soft);\n}\n\n.ap-panel-title {\n font-size: 15px;\n font-weight: 600;\n flex: 1 1 auto;\n}\n\n.ap-panel-instructions {\n padding: 10px 14px;\n border-bottom: 1px solid var(--ap-border);\n color: var(--ap-text-soft);\n font-size: 13px;\n}\n\n.ap-panel-body {\n flex: 1 1 auto;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 4px 14px 14px;\n}\n\n.ap-panel-footer {\n padding: 8px 14px;\n border-top: 1px solid var(--ap-border);\n background: var(--ap-bg-soft);\n color: var(--ap-text-soft);\n font-size: 12px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.ap-section-title {\n margin: 14px 0 6px;\n color: var(--ap-text-soft);\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.ap-section-help {\n margin: 0 0 8px;\n color: var(--ap-text-soft);\n font-size: 12px;\n}\n\n.ap-empty {\n margin: 16px 0;\n color: var(--ap-text-soft);\n}\n\n/* The standing mention of a public key. Deliberately NOT the alert colours:\n this is not a failure and it is on screen for ever -- an alarm that never\n goes away stops being read. It is a fact about the project, stated in the\n panel's own tone, and it stays at the top of every draw. */\n/* The public-key notice, the \"a newer client exists\" line and the \"this\n server speaks another format\" line are the same object on screen: a\n standing statement about what one is looking at, above the notes and above\n the failures. One rule, so they cannot drift apart -- and the third one\n joins it rather than inventing a colour, precisely because a protocol\n disagreement is a fact about the project, not an incident. */\n.ap-public,\n.ap-upgrade,\n.ap-format {\n margin: 0 0 10px;\n padding: 8px 10px;\n border: 1px solid var(--ap-border);\n border-radius: var(--ap-radius);\n background: var(--ap-bg-soft);\n color: var(--ap-text-soft);\n font-size: 12px;\n line-height: 1.45;\n}\n\n/* ----------------------------------------------------------------------------\n A note, and its replies.\n ---------------------------------------------------------------------------- */\n\n.ap-note {\n margin: 8px 0;\n padding: 10px 12px;\n border: 1px solid var(--ap-border);\n border-radius: var(--ap-radius);\n background: var(--ap-bg);\n}\n\n.ap-note.ap-orphan {\n background: var(--ap-bg-soft);\n}\n\n.ap-note.ap-focused {\n border-color: var(--ap-accent);\n box-shadow: 0 0 0 3px var(--ap-accent-veil);\n}\n\n.ap-note-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ap-note-author {\n font-weight: 600;\n}\n\n.ap-note-date {\n color: var(--ap-text-soft);\n font-size: 12px;\n}\n\n.ap-note-target {\n margin: 4px 0 0;\n color: var(--ap-text-soft);\n font-size: 12px;\n font-style: italic;\n overflow-wrap: anywhere;\n}\n\n.ap-note-text {\n margin: 6px 0 0;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n.ap-note-actions {\n margin-top: 8px;\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ap-replies {\n margin: 8px 0 0;\n padding-left: 10px;\n border-left: 2px solid var(--ap-border);\n}\n\n.ap-reply {\n margin: 8px 0 0;\n}\n\n/* ----------------------------------------------------------------------------\n The form, anchored near the element pointed at.\n ---------------------------------------------------------------------------- */\n\n.ap-form {\n position: fixed;\n width: 340px;\n max-width: calc(100vw - 24px);\n display: none;\n flex-direction: column;\n gap: 8px;\n padding: 14px;\n border: 1px solid var(--ap-border);\n border-radius: var(--ap-radius);\n background: var(--ap-bg);\n box-shadow: var(--ap-shadow);\n pointer-events: auto;\n}\n\n.ap-form.ap-open {\n display: flex;\n}\n\n.ap-form-title {\n font-size: 15px;\n font-weight: 600;\n}\n\n.ap-form-target {\n color: var(--ap-text-soft);\n font-size: 12px;\n font-style: italic;\n overflow-wrap: anywhere;\n}\n\n.ap-label {\n display: block;\n margin-bottom: 3px;\n font-size: 12px;\n font-weight: 600;\n color: var(--ap-text-soft);\n}\n\n.ap-help {\n margin: 3px 0 0;\n font-size: 12px;\n color: var(--ap-text-soft);\n}\n\n.ap-field,\n.ap-area {\n width: 100%;\n padding: 8px 10px;\n border: 1px solid var(--ap-border);\n border-radius: 8px;\n background: var(--ap-bg-soft);\n color: var(--ap-text);\n}\n\n.ap-field:focus,\n.ap-area:focus {\n outline: 2px solid var(--ap-accent);\n outline-offset: 1px;\n}\n\n.ap-area {\n min-height: 92px;\n resize: vertical;\n}\n\n.ap-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ap-counter {\n margin-left: auto;\n font-size: 12px;\n color: var(--ap-text-soft);\n}\n\n/* ----------------------------------------------------------------------------\n Buttons.\n ---------------------------------------------------------------------------- */\n\n.ap-primary,\n.ap-secondary,\n.ap-link {\n border-radius: 8px;\n cursor: pointer;\n pointer-events: auto;\n}\n\n.ap-primary {\n padding: 8px 14px;\n border: 1px solid var(--ap-accent);\n background: var(--ap-accent);\n color: var(--ap-accent-text);\n font-weight: 600;\n}\n\n.ap-primary:hover {\n background: var(--ap-accent-dark);\n border-color: var(--ap-accent-dark);\n}\n\n.ap-secondary {\n padding: 8px 14px;\n border: 1px solid var(--ap-border);\n background: var(--ap-bg);\n color: var(--ap-text);\n}\n\n.ap-secondary:hover {\n background: var(--ap-bg-raised);\n}\n\n.ap-link {\n padding: 2px 4px;\n border: 0;\n background: none;\n color: var(--ap-accent);\n text-decoration: underline;\n font-size: 13px;\n}\n\n.ap-primary:disabled,\n.ap-secondary:disabled,\n.ap-link:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.ap-primary:focus-visible,\n.ap-secondary:focus-visible,\n.ap-link:focus-visible {\n outline: 2px solid var(--ap-accent);\n outline-offset: 2px;\n}\n\n/* ----------------------------------------------------------------------------\n The failures.\n\n They are RED, at the top of the block concerned, and carry the message the\n server returned as it stands: that is how a non-technical team learns that\n its remark is not saved, instead of believing it is.\n ---------------------------------------------------------------------------- */\n\n.ap-error {\n margin: 8px 0;\n padding: 10px 12px;\n border: 1px solid var(--ap-alert-border);\n border-radius: var(--ap-radius);\n background: var(--ap-alert-bg);\n color: var(--ap-alert-text);\n}\n\n.ap-error-title {\n font-weight: 700;\n margin-bottom: 4px;\n}\n\n.ap-error-detail {\n margin: 6px 0 0;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font-size: 13px;\n}\n\n.ap-error .ap-link {\n color: var(--ap-alert-text);\n}\n\n/* ----------------------------------------------------------------------------\n Narrow: the panel takes the full width, and so does the form.\n ---------------------------------------------------------------------------- */\n\n/* ----------------------------------------------------------------------------\n Narrow.\n\n DEFECT OBSERVED at 375 px wide: a panel taking the full height covers the\n whole page, and no element can be pointed at any more -- every click lands\n on the panel. So it becomes a bottom band, which leaves the top half of\n the viewport free; one scrolls the page there to bring the wanted element\n into view. The form, for its part, hides the panel while typing (see\n notes.js): on a screen that size, writing and reading the list at the same\n time does not hold.\n ---------------------------------------------------------------------------- */\n\n/* On a narrow screen the panel becomes a bottom band and the form takes the\n full width.\n\n THE WIDTH CEILING IS KEPT, and it comes from a measured defect: \"left: 8;\n right: 8\" sizes the element against its CONTAINING BLOCK, which the host\n site's horizontal overflow can make wider than the visible window.\n Measured, in mobile emulation at 390 px: the site overflows to 407 px\n (with the tool and without it), and the panel came out 391 px wide\n starting at 8, that is 9 px off screen. \"100vw\" is the window, not the\n containing block: the ceiling therefore does nothing when the site does\n not overflow, and pulls the width back when it does. */\n@media (max-width: 560px) {\n .ap-panel {\n top: auto;\n right: 8px;\n left: 8px;\n bottom: 66px;\n height: 52vh;\n width: auto;\n max-width: calc(100vw - 16px);\n }\n\n .ap-form {\n left: 8px;\n right: 8px;\n width: auto;\n max-width: calc(100vw - 16px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ap-button {\n transition: none;\n }\n}\n\n/* The failure shows without opening the panel: the button's dot changes\n colour. A team that does not click must be able to see that something is\n wrong. */\n.ap-button.ap-failed .ap-button-dot {\n background: var(--ap-alert-text);\n}\n\n.ap-button.ap-failed {\n border-color: var(--ap-alert-border);\n}\n\n/* Signature reminder, in the note form.\n The name was shown at the foot of the panel only: invisible at the moment\n one writes. A user reported not knowing which name they were writing\n under. */\n.ap-form-signature {\n display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;\n margin: 0 0 .6rem; font-size: .85rem; opacity: .8;\n}\n\n/* Resolution state, said on the card.\n Two cases NOT to be confused: resolved and online, resolved but not\n deployed yet. The second keeps the defect on the reviewer's screen; hiding\n it or announcing it as fixed would cost them their trust in the tool. */\n.ap-state-mark {\n display: inline-block; margin: 0 0 .5rem;\n padding: .15rem .55rem; border-radius: 4px;\n font-size: .75rem; font-weight: 600; letter-spacing: .02em;\n}\n.ap-note.ap-resolved { opacity: .72; }\n.ap-note.ap-resolved .ap-state-mark {\n color: #0f7a52; background: rgba(16, 185, 129, .14);\n}\n.ap-note.ap-resolved-pending .ap-state-mark {\n color: #8a5a00; background: rgba(245, 158, 11, .16);\n}\n/* The \"it is fixed\" / \"reopen\" block, opened under the card. Same shape as\n the reply block: it is the same gesture, one answers a remark. */\n.ap-resolve,\n.ap-reply-form {\n margin-top: .6rem;\n padding-top: .6rem;\n border-top: 1px solid var(--ap-border);\n}\n\n/* The question asked before the key is dropped, at the foot of the list.\n Framed like the resolution block -- it is the same shape of gesture, one\n answers before something changes -- and set apart from the notes above it,\n because it is not about a note. */\n.ap-forget {\n margin-top: 1rem;\n padding-top: .6rem;\n border-top: 1px solid var(--ap-border);\n}\n\n.ap-forget .ap-actions {\n margin-top: .5rem;\n}\n\n.ap-history-toggle {\n display: block; width: 100%; margin: 1rem 0 .25rem;\n padding: .5rem .75rem; border: 1px dashed currentColor; border-radius: 6px;\n background: none; color: inherit; font: inherit; opacity: .7; cursor: pointer;\n}\n.ap-history-toggle:hover { opacity: 1; }\n\n\n/* ----------------------------------------------------------------------------\n Setup and pasting the salt.\n\n These are the only screens where something is copied by hand. Everything\n there is SELECTABLE and monospaced: a 43-character salt copied wrong\n cannot be recovered, and nothing helps less than a font that confuses I, l\n and 1.\n ---------------------------------------------------------------------------- */\n\n.ap-panel-wide {\n width: 560px;\n}\n\n.ap-copy {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n margin: 0 0 12px;\n}\n\n.ap-code {\n flex: 1 1 auto;\n width: 100%;\n padding: 8px 10px;\n border: 1px solid var(--ap-border);\n border-radius: 8px;\n background: var(--ap-bg-soft);\n color: var(--ap-text);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\",\n monospace;\n font-size: 12.5px;\n line-height: 1.5;\n resize: vertical;\n white-space: pre;\n overflow-x: auto;\n}\n\n.ap-code:focus-visible {\n outline: 2px solid var(--ap-accent);\n outline-offset: 1px;\n}\n\n@media (max-width: 560px) {\n .ap-panel-wide {\n width: auto;\n }\n\n .ap-copy {\n flex-direction: column;\n }\n}\n";
|
|
22
22
|
|
|
23
23
|
/* ==== 00-preamble.js ==== */
|
|
24
24
|
|
|
@@ -574,6 +574,37 @@
|
|
|
574
574
|
+ 'running {current}, served by the site itself -- nothing was fetched '
|
|
575
575
|
+ 'to replace it, and when to update the file is the owner\'s call.',
|
|
576
576
|
|
|
577
|
+
/* -- The server and this client do not speak the same protocol -----
|
|
578
|
+
Two numbers in every sentence, never the word "incompatible" on its
|
|
579
|
+
own: "incompatible" sends somebody hunting through three components,
|
|
580
|
+
two numbers say in one line which end is behind and therefore what to
|
|
581
|
+
update. The two directions do not say the same thing because they do
|
|
582
|
+
not cost the same thing -- ahead of us, writing is refused; behind us,
|
|
583
|
+
everything still works. */
|
|
584
|
+
'format.server_newer':
|
|
585
|
+
'This server speaks annotepage format {server}; this client speaks '
|
|
586
|
+
+ 'format {ours}. Writing is refused from this page while that is '
|
|
587
|
+
+ 'true: a remark sealed at format {ours} would be stored in an '
|
|
588
|
+
+ 'envelope this server cannot read back, and nothing is ever deleted '
|
|
589
|
+
+ 'in this tool. The notes are still shown, and what cannot be read is '
|
|
590
|
+
+ 'counted below. To fix it, update the annotepage client file this '
|
|
591
|
+
+ 'page loads to a version that speaks format {server} -- that is the '
|
|
592
|
+
+ 'call of whoever looks after the site.',
|
|
593
|
+
'format.server_older':
|
|
594
|
+
'This server speaks annotepage format {server}; this client speaks '
|
|
595
|
+
+ 'format {ours}. Nothing is blocked: notes of format {server} are '
|
|
596
|
+
+ 'read here, and remarks can still be written. It is the SERVER that '
|
|
597
|
+
+ 'is behind -- tell whoever looks after it to update it to format '
|
|
598
|
+
+ '{ours}.',
|
|
599
|
+
'format.write_refused':
|
|
600
|
+
'Nothing was sent, and nothing was lost: your text is kept above. '
|
|
601
|
+
+ 'This server speaks annotepage format {server}, this client speaks '
|
|
602
|
+
+ 'format {ours}, so a remark written from here would be stored in an '
|
|
603
|
+
+ 'envelope the server cannot read back -- and nothing is ever deleted '
|
|
604
|
+
+ 'in this tool. The annotepage client file loaded by this page has to '
|
|
605
|
+
+ 'be updated to format {server} first. Until then, write the remark '
|
|
606
|
+
+ 'somewhere else: it will not arrive from this page.',
|
|
607
|
+
|
|
577
608
|
/* -- A tag that cannot be used as it stands ------------------------
|
|
578
609
|
Somebody put that tag there on purpose, so we speak instead of staying
|
|
579
610
|
silent -- and we refuse exactly as a wrong pasted key is refused
|
|
@@ -984,9 +1015,82 @@
|
|
|
984
1015
|
This function never rejects and never writes to the console: it returns a
|
|
985
1016
|
cause, and the caller decides whether we keep quiet or speak. */
|
|
986
1017
|
|
|
1018
|
+
/* -- 8bis. THE FORMAT THE SERVER ANNOUNCES, AND WHAT WE DO ABOUT IT ------
|
|
1019
|
+
The three components -- this client, the MCP package, the server -- speak
|
|
1020
|
+
ONE protocol number, and nothing at build time can hold them together once
|
|
1021
|
+
they are deployed: this file comes from a CDN, the server is updated by
|
|
1022
|
+
whoever runs it, on their own day. tools/check-versions.mjs makes the three
|
|
1023
|
+
agree IN THE REPOSITORY; it has never seen a deployment.
|
|
1024
|
+
|
|
1025
|
+
So they are compared HERE, at runtime, on the only thing that crosses the
|
|
1026
|
+
wire: the `format` the server writes into the envelope of every answer
|
|
1027
|
+
(api.php, AP_FORMAT). It has been sent since format 2 and nobody read it.
|
|
1028
|
+
|
|
1029
|
+
WHAT A DISAGREEMENT COSTS, AND WHY IT IS SILENT: the server accepts the
|
|
1030
|
+
write, stores an envelope it does not know how to read back, and the only
|
|
1031
|
+
symptom is a reader that quietly skips rows. Nothing fails, nothing is
|
|
1032
|
+
logged, and the remark is gone by the time anybody goes looking.
|
|
1033
|
+
|
|
1034
|
+
THE TWO DIRECTIONS ARE NOT SYMMETRICAL, and that asymmetry IS the decision:
|
|
1035
|
+
|
|
1036
|
+
SERVER NEWER THAN US -- we can guarantee nothing: neither that we read
|
|
1037
|
+
what is there, nor that what we write can be read back. We say it, and we
|
|
1038
|
+
REFUSE TO WRITE. A wrong envelope is the damage that cannot be taken
|
|
1039
|
+
back, because nothing is ever deleted in this tool; showing a page we may
|
|
1040
|
+
be reading incompletely is taken back by reloading.
|
|
1041
|
+
|
|
1042
|
+
SERVER OLDER THAN US -- we still know how to read it. Format 1 rows are
|
|
1043
|
+
read by this client today (openNote, below), and that is exactly what the
|
|
1044
|
+
per-row rule buys (FORMAT.md section 7). Mention only, no refusal.
|
|
1045
|
+
|
|
1046
|
+
IT IS A STATE, NOT AN ALARM. The mention is drawn in the panel at every
|
|
1047
|
+
draw, in the register of the public-key notice: it describes what one is
|
|
1048
|
+
looking AT, it is not an event that has just happened.
|
|
1049
|
+
|
|
1050
|
+
EVERY DOUBT IS A SILENCE, exactly as for the announced client version
|
|
1051
|
+
(80-upgrade). The number comes off the network from a server we do not own:
|
|
1052
|
+
absent, not a number, out of shape, equal to ours -- carry on, say nothing.
|
|
1053
|
+
It is matched against a shape before it is compared, and it is only ever
|
|
1054
|
+
COMPARED: nothing here is concatenated anywhere. */
|
|
1055
|
+
|
|
1056
|
+
const FORMAT_SHAPE = /^(0|[1-9][0-9]{0,3})$/;
|
|
1057
|
+
|
|
1058
|
+
/* 0 means "nothing readable was announced", which is also the state of a
|
|
1059
|
+
server too old to send the field at all. Nothing is said then: a number we
|
|
1060
|
+
do not have is not a disagreement. */
|
|
1061
|
+
let serverFormat = 0;
|
|
1062
|
+
|
|
1063
|
+
const readAnnouncedFormat = (data) => {
|
|
1064
|
+
if (!data || typeof data !== 'object') return;
|
|
1065
|
+
const announced = data.format;
|
|
1066
|
+
// A JSON number, as api.php sends it. A string is accepted too: an
|
|
1067
|
+
// intermediary that re-encodes the envelope is not a reason to go blind.
|
|
1068
|
+
if (typeof announced !== 'number' && typeof announced !== 'string') return;
|
|
1069
|
+
const text = String(announced);
|
|
1070
|
+
if (!FORMAT_SHAPE.test(text)) return;
|
|
1071
|
+
serverFormat = parseInt(text, 10);
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
/** The server speaks a format we do not: this copy must not write. */
|
|
1075
|
+
const serverIsNewer = () => serverFormat > FORMAT;
|
|
1076
|
+
|
|
1077
|
+
/** The server speaks an older one: we read it, and we say so. */
|
|
1078
|
+
const serverIsOlder = () => serverFormat > 0 && serverFormat < FORMAT;
|
|
1079
|
+
|
|
987
1080
|
const call = (action, body) => {
|
|
988
1081
|
if (!API) return Promise.resolve({ ok: false, cause: 'inactive' });
|
|
989
1082
|
|
|
1083
|
+
/* THE SINGLE CHOKE POINT OF THE REFUSAL. Every write this client makes
|
|
1084
|
+
goes through here -- a note, a reply, a resolution, and whatever is
|
|
1085
|
+
added tomorrow -- and a check written at the three call sites instead
|
|
1086
|
+
of this one is the check the fourth call site forgets. A READ is never
|
|
1087
|
+
refused: what we cannot read is already counted and said (readFailure
|
|
1088
|
+
below), and refusing to read would hide notes rather than protect
|
|
1089
|
+
them. */
|
|
1090
|
+
if (body && serverIsNewer()) {
|
|
1091
|
+
return Promise.resolve({ ok: false, cause: 'format-newer' });
|
|
1092
|
+
}
|
|
1093
|
+
|
|
990
1094
|
const options = {
|
|
991
1095
|
method: body ? 'POST' : 'GET',
|
|
992
1096
|
cache: 'no-store',
|
|
@@ -1023,6 +1127,10 @@
|
|
|
1023
1127
|
} catch (e) {
|
|
1024
1128
|
return { ok: false, cause: 'nonjson' };
|
|
1025
1129
|
}
|
|
1130
|
+
/* Read from EVERY answer, not just the first: a server
|
|
1131
|
+
updated while a page stayed open all afternoon says so on
|
|
1132
|
+
its next answer, and the panel follows it. */
|
|
1133
|
+
readAnnouncedFormat(data);
|
|
1026
1134
|
// The tool is dropped in here but not configured: it SAYS so
|
|
1027
1135
|
// with a 200, so as not to leave the browser an error to log.
|
|
1028
1136
|
// We stand down, as on a 404.
|
|
@@ -1068,6 +1176,14 @@
|
|
|
1068
1176
|
const failureFrom = (result, title) => {
|
|
1069
1177
|
if (result.ok) return null;
|
|
1070
1178
|
const say = (key) => ({ title: T(title), detail: T(key, { code: result.code }) });
|
|
1179
|
+
/* Nothing left this browser. The message names BOTH numbers, because
|
|
1180
|
+
"incompatible" sends somebody hunting, and two numbers say in one line
|
|
1181
|
+
which of the two ends is behind. */
|
|
1182
|
+
if (result.cause === 'format-newer') {
|
|
1183
|
+
return { title: T(title),
|
|
1184
|
+
detail: T('format.write_refused',
|
|
1185
|
+
{ server: serverFormat, ours: FORMAT }) };
|
|
1186
|
+
}
|
|
1071
1187
|
if (result.cause === 'server') return { title: T(title), detail: result.message };
|
|
1072
1188
|
if (result.cause === 'network') return say('error.network');
|
|
1073
1189
|
if (result.cause === 'refused') return say('error.refused');
|
|
@@ -2032,6 +2148,25 @@
|
|
|
2032
2148
|
ui.body.appendChild(notice);
|
|
2033
2149
|
}
|
|
2034
2150
|
|
|
2151
|
+
/* THE SERVER AND THIS CLIENT DO NOT SPEAK THE SAME PROTOCOL NUMBER, and
|
|
2152
|
+
that is a standing property of what is on screen, not an event: same
|
|
2153
|
+
register as the two notices above, same place, at every draw
|
|
2154
|
+
(40-api, section 8bis).
|
|
2155
|
+
|
|
2156
|
+
Which of the two sentences is drawn is the whole asymmetry. A server
|
|
2157
|
+
AHEAD of us also refuses our writes, and the sentence says so, here,
|
|
2158
|
+
before anybody types four hundred words into the form -- the refusal at
|
|
2159
|
+
send time (failureFrom) is the guarantee, not the announcement. A
|
|
2160
|
+
server BEHIND us is read normally and written to normally, so its
|
|
2161
|
+
sentence names what to update and stops there. */
|
|
2162
|
+
if (serverIsNewer() || serverIsOlder()) {
|
|
2163
|
+
const notice = create('div', 'ap-format',
|
|
2164
|
+
T(serverIsNewer() ? 'format.server_newer' : 'format.server_older',
|
|
2165
|
+
{ server: serverFormat, ours: FORMAT }));
|
|
2166
|
+
notice.setAttribute('role', 'note');
|
|
2167
|
+
ui.body.appendChild(notice);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2035
2170
|
if (currentFailure) {
|
|
2036
2171
|
ui.body.appendChild(failureBlock(currentFailure, () => {
|
|
2037
2172
|
currentFailure = null;
|
package/labels/fr.json
CHANGED
|
@@ -88,6 +88,9 @@
|
|
|
88
88
|
"key.forget_confirm": "Ce navigateur cessera de retenir la clé de ce projet : l’outil la redemandera ici, et sur toutes les pages de ce site. Les notes ne sont pas touchées — elles restent sur le serveur, chiffrées, et reviennent dès que la clé est recollée. Assurez-vous de pouvoir encore vous procurer la clé avant de valider : personne, pas même le serveur, ne peut la redonner.",
|
|
89
89
|
"public.notice": "Chiffré de bout en bout, et la clé de ce projet est publique : elle est écrite dans cette page. Quiconque peut ouvrir la page peut lire ces notes ET en écrire — la clé donne les deux, et ce format n’a pas de rôle lecteur seul.",
|
|
90
90
|
"upgrade.available": "Un client annotepage plus récent existe : {version}. Cette page exécute la version {current}, servie par le site lui-même — rien n’a été chargé pour la remplacer, et le moment de mettre à jour le fichier appartient au propriétaire.",
|
|
91
|
+
"format.server_newer": "Ce serveur parle le format annotepage {server} ; ce client parle le format {ours}. L’écriture est refusée depuis cette page tant que c’est le cas : une remarque scellée au format {ours} serait rangée dans une enveloppe que ce serveur ne sait pas relire, et rien n’est jamais supprimé dans cet outil. Les notes restent affichées, et ce qui n’a pas pu être lu est compté plus bas. Pour corriger : mettre à jour le fichier client annotepage chargé par cette page vers une version qui parle le format {server} — c’est la décision de la personne qui s’occupe du site.",
|
|
92
|
+
"format.server_older": "Ce serveur parle le format annotepage {server} ; ce client parle le format {ours}. Rien n’est bloqué : les notes de format {server} sont lues ici, et les remarques peuvent toujours être écrites. C’est le SERVEUR qui est en retard — demandez sa mise à jour vers le format {ours} à la personne qui s’en occupe.",
|
|
93
|
+
"format.write_refused": "Rien n’a été envoyé, et rien n’est perdu : votre texte est conservé ci-dessus. Ce serveur parle le format annotepage {server}, ce client parle le format {ours} : une remarque écrite d’ici serait rangée dans une enveloppe que le serveur ne sait pas relire — et rien n’est jamais supprimé dans cet outil. Le fichier client annotepage chargé par cette page doit d’abord être mis à jour vers le format {server}. En attendant, notez la remarque ailleurs : elle n’arrivera pas depuis cette page.",
|
|
91
94
|
"tag.title": "Cette balise annotepage est inutilisable telle quelle",
|
|
92
95
|
"tag.key_shape": "L’attribut data-key de la balise de cette page n’est pas une clé : 43 caractères sont attendus, parmi A-Z a-z 0-9 - _, sans espace et sans tiret décoratif. Rien n’a été envoyé, rien n’a été déchiffré. La personne qui a installé l’outil doit recopier la clé, d’un seul bloc.",
|
|
93
96
|
"tag.key_mismatch": "La balise de cette page porte une clé et un identifiant de projet qui ne vont pas ensemble : la clé ne dérive pas cet identifiant. Rien n’a été envoyé, rien n’a été déchiffré, et l’outil ne devine pas lequel des deux a raison. Une balise publique se suffit de la clé — l’identifiant en est dérivé — donc retirez data-project, ou corrigez celui des deux qui est faux.",
|
package/package.json
CHANGED