annotepage-client 2.1.0 → 2.2.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 +4 -4
- package/dist/HASHES.txt +1 -0
- package/dist/annotepage.js +259 -20
- package/labels/fr.json +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,8 +33,8 @@ Load the client once, on a page of the site, with `data-setup` and **without**
|
|
|
33
33
|
`data-project`:
|
|
34
34
|
|
|
35
35
|
```html
|
|
36
|
-
<script src="https://<your-cdn>/annotepage-client@2.
|
|
37
|
-
integrity="sha384-
|
|
36
|
+
<script src="https://<your-cdn>/annotepage-client@2.2.0/dist/annotepage.js"
|
|
37
|
+
integrity="sha384-5wrAEkCKCLyEM3YJsVd6H7gFCOHt9f63XTDIM0Eu4fVYEAr3X4wgDWEDXkx/WVb/"
|
|
38
38
|
crossorigin="anonymous"
|
|
39
39
|
data-server="https://<your-server>/annotepage/api.php"
|
|
40
40
|
data-setup
|
|
@@ -54,8 +54,8 @@ on the server. No network request is made at that point.
|
|
|
54
54
|
### 2. Paste the final tag, at the end of `<body>`
|
|
55
55
|
|
|
56
56
|
```html
|
|
57
|
-
<script src="https://<your-cdn>/annotepage-client@2.
|
|
58
|
-
integrity="sha384-
|
|
57
|
+
<script src="https://<your-cdn>/annotepage-client@2.2.0/dist/annotepage.js"
|
|
58
|
+
integrity="sha384-5wrAEkCKCLyEM3YJsVd6H7gFCOHt9f63XTDIM0Eu4fVYEAr3X4wgDWEDXkx/WVb/"
|
|
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.2.0 sha384-5wrAEkCKCLyEM3YJsVd6H7gFCOHt9f63XTDIM0Eu4fVYEAr3X4wgDWEDXkx/WVb/ 154893 bytes
|
|
1
2
|
2.1.0 sha384-tMQlD7gryDE13ShW/GCVLMhJU8tqODcREl9RksXuf67eectM4YPTUZXLfkmakFoO 142816 bytes
|
|
2
3
|
2.0.2 sha384-IeG681tUVluUYNg632FP8SMWIHZp1pEEP5/s6XT+PIJpbp7JJDkGThQPVx1xf31p 142816 bytes
|
|
3
4
|
2.0.1 sha384-aSkx1XQLhcYpDQrpoD8yUn0Wu00BHbi1KktWiXqMQufioNtReQw+tm804/ZzNfo8 134184 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.2.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.2.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.ap-public {\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.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 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.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
|
|
|
@@ -545,6 +545,15 @@
|
|
|
545
545
|
+ 'notes AND write them -- the key gives both, and this format has no '
|
|
546
546
|
+ 'reader-only role.',
|
|
547
547
|
|
|
548
|
+
/* -- A newer client exists, and this copy is not going to fetch it -
|
|
549
|
+
Shown ONLY when the file is served by the site itself: a copy served
|
|
550
|
+
by a CDN replaces itself instead of talking about it (80-upgrade).
|
|
551
|
+
So the sentence has to say what was NOT done, and whose call it is. */
|
|
552
|
+
'upgrade.available':
|
|
553
|
+
'A more recent annotepage client exists: {version}. This page is '
|
|
554
|
+
+ 'running {current}, served by the site itself -- nothing was fetched '
|
|
555
|
+
+ 'to replace it, and when to update the file is the owner\'s call.',
|
|
556
|
+
|
|
548
557
|
/* -- A tag that cannot be used as it stands ------------------------
|
|
549
558
|
Somebody put that tag there on purpose, so we speak instead of staying
|
|
550
559
|
silent -- and we refuse exactly as a wrong pasted key is refused
|
|
@@ -1871,6 +1880,21 @@
|
|
|
1871
1880
|
ui.body.appendChild(notice);
|
|
1872
1881
|
}
|
|
1873
1882
|
|
|
1883
|
+
/* A NEWER CLIENT EXISTS, AND WE ARE NOT GOING TO FETCH IT. This copy is
|
|
1884
|
+
served by the site itself -- somebody took the file off a CDN on
|
|
1885
|
+
purpose -- so it says so and stops there (80-upgrade). A copy served
|
|
1886
|
+
BY a CDN never gets here: it replaced itself before the panel existed.
|
|
1887
|
+
|
|
1888
|
+
Said at every draw and not once at load, for the same reason as the
|
|
1889
|
+
notice above: a message shown at load time is read by whoever happened
|
|
1890
|
+
to be looking, and by nobody who opens this panel a week later. */
|
|
1891
|
+
if (upgradeAvailable) {
|
|
1892
|
+
const notice = create('div', 'ap-upgrade',
|
|
1893
|
+
T('upgrade.available', { version: upgradeAvailable, current: TOOL_VERSION }));
|
|
1894
|
+
notice.setAttribute('role', 'note');
|
|
1895
|
+
ui.body.appendChild(notice);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1874
1898
|
if (currentFailure) {
|
|
1875
1899
|
ui.body.appendChild(failureBlock(currentFailure, () => {
|
|
1876
1900
|
currentFailure = null;
|
|
@@ -2529,9 +2553,182 @@
|
|
|
2529
2553
|
screen.body.appendChild(create('p', 'ap-help', T('context.help')));
|
|
2530
2554
|
};
|
|
2531
2555
|
|
|
2556
|
+
/* ==== 80-upgrade.js ==== */
|
|
2557
|
+
|
|
2558
|
+
/* -- 19. A stale copy replaces itself ------------------------------------
|
|
2559
|
+
The problem, in one line: the distributed tag points at a RANGE on a CDN
|
|
2560
|
+
(annotepage-client@2), and jsDelivr serves that range with
|
|
2561
|
+
max-age=604800. A fix published today does not reach a visitor who came
|
|
2562
|
+
back within seven days. The lifetime is the CDN's to set and no attribute
|
|
2563
|
+
of <script> touches it -- integrity, crossorigin, defer, async, none of
|
|
2564
|
+
them. Cache-Control is a response header; the requester cannot overrule
|
|
2565
|
+
it.
|
|
2566
|
+
|
|
2567
|
+
So we do not fight the cache, we WALK AROUND IT. The stale copy does not
|
|
2568
|
+
refresh its own URL: it loads a DIFFERENT one, the pinned
|
|
2569
|
+
annotepage-client@X.Y.Z/dist/annotepage.js, which this browser has never
|
|
2570
|
+
fetched and which therefore no cache entry can answer.
|
|
2571
|
+
|
|
2572
|
+
WHERE THE CURRENT VERSION COMES FROM: the answer the client already asks
|
|
2573
|
+
for. `list` runs before the DOM is touched, and the server names the
|
|
2574
|
+
current client version in it. No second request, no extra file to host,
|
|
2575
|
+
and the answer lands exactly at the seam where the check belongs.
|
|
2576
|
+
|
|
2577
|
+
IT ANNOUNCES, IT NEVER GATES. The announced version says "there is
|
|
2578
|
+
something newer" and nothing else. It must never decide whether a request
|
|
2579
|
+
is allowed, and nothing here compares it to authorise anything:
|
|
2580
|
+
compatibility belongs to the FORMAT number and only to it (FORMAT.md
|
|
2581
|
+
section 7). 2.1.0 and 2.2.0 speak the same format by construction, and a
|
|
2582
|
+
client that refused to talk to a server one release ahead would break a
|
|
2583
|
+
pair that works.
|
|
2584
|
+
|
|
2585
|
+
WHY EVERY DOUBT IS A SILENCE. The announcement rides on an answer from a
|
|
2586
|
+
server that may be anybody's. A self-hosted server announces whatever it
|
|
2587
|
+
was installed with, which can be older than what is on this page. So:
|
|
2588
|
+
absent, unreadable, malformed, EQUAL or OLDER than ours -- carry on, say
|
|
2589
|
+
nothing. We only ever move forward, never back. A server cannot push a
|
|
2590
|
+
client downhill. */
|
|
2591
|
+
|
|
2592
|
+
/* THE SHAPE OF A VERSION, and it is the only gate that matters here.
|
|
2593
|
+
|
|
2594
|
+
This string arrives over the network from a server we do not own. It is
|
|
2595
|
+
never concatenated into a URL as it stands: it is matched against this
|
|
2596
|
+
expression, and the URL is then REBUILT from a base written here, in this
|
|
2597
|
+
file. A compromised or hostile server can therefore make us load a version
|
|
2598
|
+
of the official package that does not exist -- which fails, and is handled
|
|
2599
|
+
-- and nothing else. No host, no path, no scheme, no protocol-relative
|
|
2600
|
+
"//evil", no "../", no query, no "@" of its own.
|
|
2601
|
+
|
|
2602
|
+
Strict on purpose: three numbers, nothing around them. No pre-release
|
|
2603
|
+
suffix (a pre-release is not what one pushes to every visitor of every
|
|
2604
|
+
site), no leading zero, at most four digits per part. */
|
|
2605
|
+
const ANNOUNCED_SHAPE = /^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$/;
|
|
2606
|
+
|
|
2607
|
+
/* The CDNs whose exact-version address we know how to write ourselves, and
|
|
2608
|
+
only those. `base` is the whole beginning of the URL: nothing of what the
|
|
2609
|
+
server said ever appears before the version number.
|
|
2610
|
+
|
|
2611
|
+
`prefix` is matched against the PATHNAME of the tag's own src, and the host
|
|
2612
|
+
is compared whole -- indexOf on the full URL would accept
|
|
2613
|
+
"cdn.jsdelivr.net.example.com". Both entries are the npm package, which is
|
|
2614
|
+
the only package this file will ever point at. */
|
|
2615
|
+
const CDNS = [
|
|
2616
|
+
{
|
|
2617
|
+
host: 'cdn.jsdelivr.net',
|
|
2618
|
+
prefix: '/npm/annotepage-client@',
|
|
2619
|
+
base: 'https://cdn.jsdelivr.net/npm/annotepage-client@'
|
|
2620
|
+
},
|
|
2621
|
+
{
|
|
2622
|
+
host: 'unpkg.com',
|
|
2623
|
+
prefix: '/annotepage-client@',
|
|
2624
|
+
base: 'https://unpkg.com/annotepage-client@'
|
|
2625
|
+
}
|
|
2626
|
+
];
|
|
2627
|
+
|
|
2628
|
+
/**
|
|
2629
|
+
* Which CDN is serving THIS copy, or null -- and null is the interesting
|
|
2630
|
+
* case, because it means the site serves the file itself.
|
|
2631
|
+
*/
|
|
2632
|
+
const cdnServing = (src) => {
|
|
2633
|
+
let url;
|
|
2634
|
+
try {
|
|
2635
|
+
url = new URL(String(src));
|
|
2636
|
+
} catch (e) {
|
|
2637
|
+
return null;
|
|
2638
|
+
}
|
|
2639
|
+
if (url.protocol !== 'https:') return null;
|
|
2640
|
+
for (let i = 0; i < CDNS.length; i += 1) {
|
|
2641
|
+
const cdn = CDNS[i];
|
|
2642
|
+
if (url.host === cdn.host && url.pathname.indexOf(cdn.prefix) === 0) return cdn;
|
|
2643
|
+
}
|
|
2644
|
+
return null;
|
|
2645
|
+
};
|
|
2646
|
+
|
|
2647
|
+
/** The address of one exact version, BUILT HERE. Null if anything is off. */
|
|
2648
|
+
const officialUrl = (cdn, version) => {
|
|
2649
|
+
// Checked again, on the very line that builds the string: the caller
|
|
2650
|
+
// already checked, and a second reader of this file should not have to
|
|
2651
|
+
// go and verify that it did.
|
|
2652
|
+
if (!cdn || !ANNOUNCED_SHAPE.test(String(version))) return null;
|
|
2653
|
+
return cdn.base + version + '/dist/annotepage.js';
|
|
2654
|
+
};
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* The version the server announces, IF it is newer than ours. Null in every
|
|
2658
|
+
* other case, and that includes every case of doubt: no field, not a string,
|
|
2659
|
+
* not three numbers, equal to ours, older than ours.
|
|
2660
|
+
*/
|
|
2661
|
+
const announcedVersion = (data) => {
|
|
2662
|
+
if (!data || typeof data !== 'object') return null;
|
|
2663
|
+
const announced = data.client_version;
|
|
2664
|
+
if (typeof announced !== 'string' || !ANNOUNCED_SHAPE.test(announced)) return null;
|
|
2665
|
+
|
|
2666
|
+
const theirs = versionNumbers(announced);
|
|
2667
|
+
const mine = versionNumbers(TOOL_VERSION);
|
|
2668
|
+
if (!theirs || !mine) return null;
|
|
2669
|
+
for (let i = 0; i < 3; i += 1) {
|
|
2670
|
+
if (theirs[i] !== mine[i]) return theirs[i] > mine[i] ? announced : null;
|
|
2671
|
+
}
|
|
2672
|
+
// Equal: nothing to say, and nothing to load.
|
|
2673
|
+
return null;
|
|
2674
|
+
};
|
|
2675
|
+
|
|
2676
|
+
/* Set when we are behind and we are NOT going to do anything about it --
|
|
2677
|
+
the file is served by the site itself. The panel says so, at every draw,
|
|
2678
|
+
the way the public-key notice does: a message shown once at load is read
|
|
2679
|
+
by whoever happened to be looking. */
|
|
2680
|
+
let upgradeAvailable = '';
|
|
2681
|
+
|
|
2682
|
+
/* True from the moment the replacement tag is in the document. It stops the
|
|
2683
|
+
old copy building anything, and it stops a second injection. */
|
|
2684
|
+
let handingOver = false;
|
|
2685
|
+
|
|
2686
|
+
/**
|
|
2687
|
+
* Injects the pinned version and stands down.
|
|
2688
|
+
*
|
|
2689
|
+
* The new tag CARRIES THE data- ATTRIBUTES OF THE OLD ONE, and it has to:
|
|
2690
|
+
* the client reads everything it knows from its own tag
|
|
2691
|
+
* (document.currentScript, 00-preamble), so a bare tag would produce a copy
|
|
2692
|
+
* with no server, no project and no key -- which is a copy that does
|
|
2693
|
+
* strictly nothing. The integrity attribute is deliberately NOT carried
|
|
2694
|
+
* over: it is the digest of the version we are leaving, and it would refuse
|
|
2695
|
+
* the version we are fetching.
|
|
2696
|
+
*
|
|
2697
|
+
* `onFailure` is called if that tag never loads -- a version announced but
|
|
2698
|
+
* never published, a CDN that is down. We are then back to being merely old,
|
|
2699
|
+
* which is the state we started in, and the tool boots normally. Losing the
|
|
2700
|
+
* tool entirely because a number was wrong somewhere would be a worse
|
|
2701
|
+
* outcome than being one release behind.
|
|
2702
|
+
*/
|
|
2703
|
+
const handOverTo = (cdn, version, onFailure) => {
|
|
2704
|
+
const url = officialUrl(cdn, version);
|
|
2705
|
+
if (!url || handingOver) return false;
|
|
2706
|
+
handingOver = true;
|
|
2707
|
+
|
|
2708
|
+
// BEFORE the new copy builds anything: its element and its listeners go
|
|
2709
|
+
// first, or the page ends up carrying two pills. At the seam there is
|
|
2710
|
+
// usually nothing to remove -- which is the whole point of checking
|
|
2711
|
+
// before the work rather than after it.
|
|
2712
|
+
withdraw();
|
|
2713
|
+
|
|
2714
|
+
const fresh = document.createElement('script');
|
|
2715
|
+
const attributes = script.attributes;
|
|
2716
|
+
for (let i = 0; i < attributes.length; i += 1) {
|
|
2717
|
+
const name = attributes[i].name;
|
|
2718
|
+
if (name.indexOf('data-') === 0) fresh.setAttribute(name, attributes[i].value);
|
|
2719
|
+
}
|
|
2720
|
+
fresh.src = url;
|
|
2721
|
+
fresh.addEventListener('error', () => {
|
|
2722
|
+
handingOver = false;
|
|
2723
|
+
onFailure();
|
|
2724
|
+
});
|
|
2725
|
+
(document.head || document.documentElement).appendChild(fresh);
|
|
2726
|
+
return true;
|
|
2727
|
+
};
|
|
2728
|
+
|
|
2532
2729
|
/* ==== 90-boot.js ==== */
|
|
2533
2730
|
|
|
2534
|
-
/* --
|
|
2731
|
+
/* -- 20. Reading the notes ----------------------------------------------- */
|
|
2535
2732
|
|
|
2536
2733
|
const redraw = () => {
|
|
2537
2734
|
if (!ui) return;
|
|
@@ -2560,7 +2757,7 @@
|
|
|
2560
2757
|
});
|
|
2561
2758
|
});
|
|
2562
2759
|
|
|
2563
|
-
/* --
|
|
2760
|
+
/* -- 21. Startup ----------------------------------------------------------
|
|
2564
2761
|
The order matters: we ask the API BEFORE touching the DOM. If it does not
|
|
2565
2762
|
answer what it should, the site never saw anything go by.
|
|
2566
2763
|
|
|
@@ -2589,7 +2786,22 @@
|
|
|
2589
2786
|
});
|
|
2590
2787
|
};
|
|
2591
2788
|
|
|
2789
|
+
/**
|
|
2790
|
+
* The tool leaves the page, and leaves NOTHING behind.
|
|
2791
|
+
*
|
|
2792
|
+
* Removing the host element is not enough on its own and never was: in
|
|
2793
|
+
* annotation mode the listeners sit on `document` and on `window`, not on the
|
|
2794
|
+
* host, and a repeating timer is running. Dropping the element would leave
|
|
2795
|
+
* them hovering, clicking and measuring a layer that no longer exists. That
|
|
2796
|
+
* did not show while withdrawal only ever happened before anything was built;
|
|
2797
|
+
* it does the moment a copy withdraws in favour of a newer one (80-upgrade).
|
|
2798
|
+
*
|
|
2799
|
+
* leaveMode() is the one place that knows the whole list, and it is called
|
|
2800
|
+
* rather than copied: two lists drift, and the one that drifts is the one
|
|
2801
|
+
* nobody runs.
|
|
2802
|
+
*/
|
|
2592
2803
|
const withdraw = () => {
|
|
2804
|
+
if (ui && mode) leaveMode();
|
|
2593
2805
|
if (host) host.remove();
|
|
2594
2806
|
host = null;
|
|
2595
2807
|
root = null;
|
|
@@ -2639,24 +2851,51 @@
|
|
|
2639
2851
|
return null;
|
|
2640
2852
|
}
|
|
2641
2853
|
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2854
|
+
/* THE SEAM. This is the one moment where a copy can discover it
|
|
2855
|
+
is out of date having drawn nothing, listened to nothing and
|
|
2856
|
+
decrypted nothing -- so it withdraws instead of undoing. The
|
|
2857
|
+
answer that carries the announcement is the one we were
|
|
2858
|
+
waiting for anyway: nothing was added in front of the boot,
|
|
2859
|
+
and a page that is up to date pays exactly nothing.
|
|
2860
|
+
|
|
2861
|
+
WHAT HAPPENS NEXT DEPENDS ON WHERE THIS FILE CAME FROM, and
|
|
2862
|
+
that distinction is the whole design:
|
|
2863
|
+
|
|
2864
|
+
from a CDN -- the seven-day cache is what put us here, and
|
|
2865
|
+
pulling the pinned version walks around it. We hand over.
|
|
2866
|
+
|
|
2867
|
+
from anywhere else -- the site serves its own copy, which
|
|
2868
|
+
somebody CHOSE to do, and going to a CDN behind their back
|
|
2869
|
+
would add the dependency they deliberately removed. We say
|
|
2870
|
+
it in the panel and we load nothing. */
|
|
2871
|
+
const newer = first.ok ? announcedVersion(first.data) : null;
|
|
2872
|
+
const cdn = newer ? cdnServing(script.src) : null;
|
|
2873
|
+
if (cdn && handOverTo(cdn, newer, () => { proceed(first); })) return null;
|
|
2874
|
+
if (newer) upgradeAvailable = newer;
|
|
2875
|
+
|
|
2876
|
+
return proceed(first);
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
/** Everything the tool does once it has decided to stay. */
|
|
2881
|
+
function proceed(first) {
|
|
2882
|
+
// From here on the tool EXISTS, and will no longer keep quiet
|
|
2883
|
+
// about its failures.
|
|
2884
|
+
buildHost();
|
|
2885
|
+
return loadLocalLabels().then(() => {
|
|
2886
|
+
clearLayer();
|
|
2887
|
+
buildUi();
|
|
2888
|
+
if (first.ok) {
|
|
2889
|
+
return readList(first.data).then((read) => {
|
|
2890
|
+
notes = read;
|
|
2656
2891
|
redraw();
|
|
2657
2892
|
return null;
|
|
2658
2893
|
});
|
|
2659
|
-
}
|
|
2894
|
+
}
|
|
2895
|
+
currentFailure = failureFrom(first, 'error.title_read');
|
|
2896
|
+
redraw();
|
|
2897
|
+
return null;
|
|
2898
|
+
});
|
|
2660
2899
|
}
|
|
2661
2900
|
|
|
2662
2901
|
const start = () => {
|
package/labels/fr.json
CHANGED
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"salt.replace": "Coller une autre clé",
|
|
85
85
|
"salt.forget": "Oublier la clé sur ce navigateur",
|
|
86
86
|
"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.",
|
|
87
|
+
"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.",
|
|
87
88
|
"tag.title": "Cette balise annotepage est inutilisable telle quelle",
|
|
88
89
|
"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.",
|
|
89
90
|
"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