annotepage-client 2.0.1 → 2.1.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 +84 -5
- package/dist/HASHES.txt +3 -1
- package/dist/annotepage.js +170 -27
- package/labels/fr.json +23 -19
- 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.0
|
|
37
|
-
integrity="sha384-
|
|
36
|
+
<script src="https://<your-cdn>/annotepage-client@2.1.0/dist/annotepage.js"
|
|
37
|
+
integrity="sha384-tMQlD7gryDE13ShW/GCVLMhJU8tqODcREl9RksXuf67eectM4YPTUZXLfkmakFoO"
|
|
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.0
|
|
58
|
-
integrity="sha384-
|
|
57
|
+
<script src="https://<your-cdn>/annotepage-client@2.1.0/dist/annotepage.js"
|
|
58
|
+
integrity="sha384-tMQlD7gryDE13ShW/GCVLMhJU8tqODcREl9RksXuf67eectM4YPTUZXLfkmakFoO"
|
|
59
59
|
crossorigin="anonymous"
|
|
60
60
|
data-server="https://<your-server>/annotepage/api.php"
|
|
61
61
|
data-project="7Qb1kZ3xNvA9dLpEqKf2Zt"
|
|
@@ -93,7 +93,8 @@ browser.
|
|
|
93
93
|
| Attribute | What it declares |
|
|
94
94
|
|---|---|
|
|
95
95
|
| `data-server` | the address of `api.php`. Required as soon as the client comes from a CDN. Without it, and only if the client is served by the site, the tool deduces `../api.php` from its own address -- as in version 1.2.0 |
|
|
96
|
-
| `data-
|
|
96
|
+
| `data-key` | **the key itself**, 43 characters. The project is then **public**: the tool derives the project id from it, asks for nothing, stores nothing, and starts. See below |
|
|
97
|
+
| `data-project` | the project id, 22 characters. The project is then **confidential**: the key is asked for once per browser. Without either attribute the tool does **nothing** (except with `data-setup`) |
|
|
97
98
|
| `data-setup` | opens the setup screen. To be removed once the project is created |
|
|
98
99
|
| `data-mode` | `encrypted` (default) or `plain`. See below |
|
|
99
100
|
| `data-path` | path prefix: the pages of the project. `/fr/` does not annotate `/en/` |
|
|
@@ -112,6 +113,63 @@ NOT deployed, the note stays visible.
|
|
|
112
113
|
A standalone tool does not guess how a site names its version: without these
|
|
113
114
|
attributes the fields stay empty, and that is intended.
|
|
114
115
|
|
|
116
|
+
## The key in the tag, or the id in the tag
|
|
117
|
+
|
|
118
|
+
The tag carries **one of the two**, and which one it carries **is** the mode.
|
|
119
|
+
There is no setting anywhere else, and nothing in a stored note records it:
|
|
120
|
+
|
|
121
|
+
```html
|
|
122
|
+
<!-- public: whoever can open the page reads AND writes -->
|
|
123
|
+
<script src="..." data-server="..." data-key="<43 characters>" defer></script>
|
|
124
|
+
|
|
125
|
+
<!-- confidential: each reviewer pastes the key once, per browser -->
|
|
126
|
+
<script src="..." data-server="..." data-project="<22 characters>" defer></script>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The id is **not** written next to the key: the client derives it (HKDF label
|
|
130
|
+
`id`), so writing both is writing the same fact twice in a tag people copy by
|
|
131
|
+
hand. A tag carrying both is not a third mode -- the client checks that they
|
|
132
|
+
agree and **refuses** the whole tag if they do not, with nothing sent and
|
|
133
|
+
nothing decrypted, rather than guessing which of the two is the typo. A
|
|
134
|
+
`data-key` that is not 43 base64url characters is refused the same way, and
|
|
135
|
+
said on screen: somebody put that attribute there on purpose.
|
|
136
|
+
|
|
137
|
+
When the key is public, the tool says so **in the panel, at every draw** -- not
|
|
138
|
+
once at load time:
|
|
139
|
+
|
|
140
|
+
> End-to-end encrypted, and the key of this project is public: it is written
|
|
141
|
+
> into this page. Anyone who can open the page can read these notes AND write
|
|
142
|
+
> them -- the key gives both, and this format has no reader-only role.
|
|
143
|
+
|
|
144
|
+
### What a public key actually opens, and it is not mainly reading
|
|
145
|
+
|
|
146
|
+
Reading is the obvious half. **The key gives read AND write, and there is no
|
|
147
|
+
reader role in this format**: a page anybody can open is a page anybody can
|
|
148
|
+
post to. Two consequences that have to be named before choosing it:
|
|
149
|
+
|
|
150
|
+
- the project's note cap is the only thing between that and a full database.
|
|
151
|
+
The relay's rate limit is per IP, which is not an answer to more than one of
|
|
152
|
+
them;
|
|
153
|
+
- **a copied tag writes into your project.** The domain lock is what normally
|
|
154
|
+
stops that -- and on a relay with open registration a project has no declared
|
|
155
|
+
origins to match, so it does not apply. Someone who lifts your public tag out
|
|
156
|
+
of your page source writes into your notes, from their own site.
|
|
157
|
+
|
|
158
|
+
On a staging site behind a login, a VPN or an IP allowlist, that audience is
|
|
159
|
+
your team: the notes are protected by the same thing that protects the site,
|
|
160
|
+
being able to reach it. On a public production page, that audience is the
|
|
161
|
+
internet.
|
|
162
|
+
|
|
163
|
+
And it is **irreversible**: the key is served to whoever the page is served to,
|
|
164
|
+
search engines and archive sites included. A project made public cannot be made
|
|
165
|
+
private again -- that would mean a new key, therefore a new project id,
|
|
166
|
+
therefore a new tag, and the old notes stay behind.
|
|
167
|
+
|
|
168
|
+
What does **not** change: the notes are encrypted exactly as before, the server
|
|
169
|
+
still cannot read a single one, and a note written on a public key is an
|
|
170
|
+
ordinary `encrypted` note. The public/confidential choice is not the
|
|
171
|
+
`data-mode` below -- they are two different words for two different things.
|
|
172
|
+
|
|
115
173
|
## Encrypted, or plain
|
|
116
174
|
|
|
117
175
|
Encryption is **on by default**. In encrypted mode, everything typed or
|
|
@@ -185,6 +243,12 @@ A refusal on the very first call is shown too, unlike 1.2.0: a firewall
|
|
|
185
243
|
blocking everything made the tool entirely invisible, and one looked for the
|
|
186
244
|
failure in the wrong file.
|
|
187
245
|
|
|
246
|
+
**And a tag that cannot be used is named too**, for the same reason: a
|
|
247
|
+
`data-key` that is not a key, or a `data-key` and a `data-project` that do not
|
|
248
|
+
derive one another, open a screen saying which of the two is wrong. Standing
|
|
249
|
+
down silently would be indistinguishable from a page with no tag at all, and
|
|
250
|
+
that is the difference nobody finds.
|
|
251
|
+
|
|
188
252
|
## What it does not touch
|
|
189
253
|
|
|
190
254
|
The client adds **one single element** to the site, at the end of `<body>`, and
|
|
@@ -200,6 +264,21 @@ variables nor the theme of the host site.
|
|
|
200
264
|
`textContent` everywhere, `innerHTML` nowhere: the text of a note is typed by a
|
|
201
265
|
human and is never interpreted as markup.
|
|
202
266
|
|
|
267
|
+
## The interface says "key", the format says "salt"
|
|
268
|
+
|
|
269
|
+
Since 2.0.2 every string a person reads says **key** -- "The key of this
|
|
270
|
+
project is needed", "The project key (43 characters)". Cryptographically that
|
|
271
|
+
is what it is: 32 bytes of key material, from which HKDF derives the project
|
|
272
|
+
id, the AES-256-GCM key and the blind-index key. A salt, by definition, is
|
|
273
|
+
public; this is the only secret there is, and the word invited people to treat
|
|
274
|
+
it as if it were not.
|
|
275
|
+
|
|
276
|
+
What did NOT change, deliberately: the label KEYS are still `salt.title`,
|
|
277
|
+
`salt.help` and so on -- renaming them would silently break every translation
|
|
278
|
+
file already written against them -- and `FORMAT.md`, the storage key
|
|
279
|
+
`annotepage/salt/<project>`, and the MCP's `salt` configuration field are
|
|
280
|
+
unchanged, because those are a format and a contract rather than prose.
|
|
281
|
+
|
|
203
282
|
## Translating, or changing a word
|
|
204
283
|
|
|
205
284
|
Every text shown is in `src/15-labels.js`, in a flat object, in English. Two
|
package/dist/HASHES.txt
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
2.0
|
|
1
|
+
2.1.0 sha384-tMQlD7gryDE13ShW/GCVLMhJU8tqODcREl9RksXuf67eectM4YPTUZXLfkmakFoO 142816 bytes
|
|
2
|
+
2.0.2 sha384-IeG681tUVluUYNg632FP8SMWIHZp1pEEP5/s6XT+PIJpbp7JJDkGThQPVx1xf31p 142816 bytes
|
|
3
|
+
2.0.1 sha384-aSkx1XQLhcYpDQrpoD8yUn0Wu00BHbi1KktWiXqMQufioNtReQw+tm804/ZzNfo8 134184 bytes
|
|
2
4
|
2.0.0 sha384-hqDI4neEKI/hhkIKK5P4GVNRQrJ5dFYvBu9JovxWccyK9irmi+ouvhrgrumlXnXl 134205 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.0
|
|
4
|
+
Package version : 2.1.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.0
|
|
19
|
+
const TOOL_VERSION = "2.1.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/* ----------------------------------------------------------------------------\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.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";
|
|
22
22
|
|
|
23
23
|
/* ==== 00-preamble.js ==== */
|
|
24
24
|
|
|
@@ -62,7 +62,44 @@
|
|
|
62
62
|
and a page that never shows a single note. */
|
|
63
63
|
const DECLARED_PROJECT = read('project');
|
|
64
64
|
const PROJECT_WELL_FORMED = /^[A-Za-z0-9_-]{22}$/.test(DECLARED_PROJECT);
|
|
65
|
-
const
|
|
65
|
+
/* NOT a const: with data-key the id is DERIVED rather than declared, and
|
|
66
|
+
90-boot writes it here once derive() has produced it. There is one PROJECT
|
|
67
|
+
in this scope and everything downstream reads it -- two would have
|
|
68
|
+
diverged. */
|
|
69
|
+
let PROJECT = PROJECT_WELL_FORMED ? DECLARED_PROJECT : '';
|
|
70
|
+
|
|
71
|
+
/* THE KEY, WRITTEN IN THE TAG -- and that attribute IS the mode.
|
|
72
|
+
|
|
73
|
+
data-key the key itself: the project is PUBLIC. Whoever can load the
|
|
74
|
+
page can read the notes and write them. Nothing is asked for,
|
|
75
|
+
nothing is stored, and no id is declared: derive() already
|
|
76
|
+
produces it from the key (HKDF label "id"), so writing both
|
|
77
|
+
would be writing the same fact twice in a tag people copy by
|
|
78
|
+
hand -- where the two can disagree.
|
|
79
|
+
data-project the id alone: confidential. The key is asked for once per
|
|
80
|
+
browser, and until it is there nothing is fetched and nothing
|
|
81
|
+
is decrypted. That is the behaviour of every version so far.
|
|
82
|
+
data-setup neither, temporarily.
|
|
83
|
+
|
|
84
|
+
THE KEY IS NOT DERIVED FROM THE DOMAIN, and it never will be. The browser
|
|
85
|
+
hands the relay an Origin header on every request (FORMAT.md section 6.2),
|
|
86
|
+
so a relay knows the domain of every project writing to it: a key that was
|
|
87
|
+
a function of the domain would be a key the relay can compute, and with it
|
|
88
|
+
the id, and with both every note it stores. That is plain mode sold as
|
|
89
|
+
encrypted. The key is random, it lives in the page, and the page is the
|
|
90
|
+
one thing the server never sees.
|
|
91
|
+
|
|
92
|
+
The SHAPE is not checked here: saltFromText() in 20-crypto is the single
|
|
93
|
+
judge of what a key looks like, and it lives in the section that owns the
|
|
94
|
+
format. What is recorded here is whether the attribute was WRITTEN at all
|
|
95
|
+
-- an empty data-key is a tag somebody meant to fill in, and it gets said
|
|
96
|
+
rather than ignored. */
|
|
97
|
+
const DECLARED_KEY = read('key');
|
|
98
|
+
const KEY_DECLARED = Object.prototype.hasOwnProperty.call(data, 'key');
|
|
99
|
+
|
|
100
|
+
/* True once the key in the tag has been checked and adopted. It is what the
|
|
101
|
+
interface says out loud, at every draw: see PUBLIC_KEY in 60-ui. */
|
|
102
|
+
let PUBLIC_KEY = false;
|
|
66
103
|
|
|
67
104
|
/* The write mode for the notes TO COME. Encrypted by default: it is the only
|
|
68
105
|
default that does not ask the installer to understand the threat model
|
|
@@ -452,10 +489,10 @@
|
|
|
452
489
|
'{n} notes were written by a more recent version of annotepage and '
|
|
453
490
|
+ 'could not be read.',
|
|
454
491
|
'read.unreadable_one':
|
|
455
|
-
'1 note could not be decrypted. The
|
|
492
|
+
'1 note could not be decrypted. The key in this browser may not be '
|
|
456
493
|
+ 'the one it was written with.',
|
|
457
494
|
'read.unreadable_n':
|
|
458
|
-
'{n} notes could not be decrypted. The
|
|
495
|
+
'{n} notes could not be decrypted. The key in this browser may not be '
|
|
459
496
|
+ 'the one they were written with.',
|
|
460
497
|
'read.unknown_one':
|
|
461
498
|
'1 note is written in a mode this tool does not know, and was not '
|
|
@@ -470,52 +507,81 @@
|
|
|
470
507
|
'marker.n': '{n} notes here',
|
|
471
508
|
|
|
472
509
|
/* -- The salt: the only secret, and it cannot be recovered ---------- */
|
|
473
|
-
'salt.title': 'The
|
|
510
|
+
'salt.title': 'The key of this project is needed',
|
|
474
511
|
'salt.help':
|
|
475
512
|
'The notes of this project are encrypted in your browser. Without the '
|
|
476
|
-
+ 'project
|
|
513
|
+
+ 'project key, this browser can neither read them nor write any. Ask '
|
|
477
514
|
+ 'whoever installed the tool for it, and paste it below. It will be '
|
|
478
515
|
+ 'remembered by this browser, for this site.',
|
|
479
|
-
'salt.label': 'The project
|
|
480
|
-
'salt.confirm': 'Use this
|
|
481
|
-
'salt.empty': 'Paste the
|
|
516
|
+
'salt.label': 'The project key (43 characters)',
|
|
517
|
+
'salt.confirm': 'Use this key',
|
|
518
|
+
'salt.empty': 'Paste the key before confirming.',
|
|
482
519
|
'salt.shape':
|
|
483
|
-
'This is not a
|
|
520
|
+
'This is not a key: 43 characters are expected, from A-Z a-z 0-9 - _, '
|
|
484
521
|
+ 'with no space and no decorative dash. Copy it in one block.',
|
|
485
522
|
'salt.wrong':
|
|
486
|
-
'This
|
|
487
|
-
+ 'was decrypted. Check that you are pasting the
|
|
523
|
+
'This key is not the one for this project. Nothing was sent, nothing '
|
|
524
|
+
+ 'was decrypted. Check that you are pasting the key of the right '
|
|
488
525
|
+ 'project.',
|
|
489
526
|
'salt.origin_changed':
|
|
490
|
-
'This
|
|
527
|
+
'This key is remembered per browser AND per domain. The day staging '
|
|
491
528
|
+ 'becomes production, it has to be pasted once more on the new domain '
|
|
492
529
|
+ '-- the notes themselves do not move.',
|
|
493
530
|
'salt.not_kept':
|
|
494
|
-
'This browser refuses to remember the
|
|
495
|
-
+ 'storage blocked). The tool works for this page, but the
|
|
531
|
+
'This browser refuses to remember the key (private browsing, or '
|
|
532
|
+
+ 'storage blocked). The tool works for this page, but the key will '
|
|
496
533
|
+ 'have to be pasted again on the next load.',
|
|
497
|
-
'salt.replace': 'Paste another
|
|
498
|
-
'salt.forget': 'Forget the
|
|
534
|
+
'salt.replace': 'Paste another key',
|
|
535
|
+
'salt.forget': 'Forget the key on this browser',
|
|
536
|
+
|
|
537
|
+
/* -- A project whose key is IN the page ---------------------------
|
|
538
|
+
Said where the notes are, at every draw, and not once at startup: it
|
|
539
|
+
is a standing property of the project, not an event. The write half
|
|
540
|
+
is the one nobody expects -- the key gives both, and this format has
|
|
541
|
+
no reader-only role. */
|
|
542
|
+
'public.notice':
|
|
543
|
+
'End-to-end encrypted, and the key of this project is public: it is '
|
|
544
|
+
+ 'written into this page. Anyone who can open the page can read these '
|
|
545
|
+
+ 'notes AND write them -- the key gives both, and this format has no '
|
|
546
|
+
+ 'reader-only role.',
|
|
547
|
+
|
|
548
|
+
/* -- A tag that cannot be used as it stands ------------------------
|
|
549
|
+
Somebody put that tag there on purpose, so we speak instead of staying
|
|
550
|
+
silent -- and we refuse exactly as a wrong pasted key is refused
|
|
551
|
+
today: nothing sent, nothing decrypted. */
|
|
552
|
+
'tag.title': 'This annotepage tag cannot be used',
|
|
553
|
+
'tag.key_shape':
|
|
554
|
+
'The data-key attribute of the tag on this page is not a key: 43 '
|
|
555
|
+
+ 'characters are expected, from A-Z a-z 0-9 - _, with no space and no '
|
|
556
|
+
+ 'decorative dash. Nothing was sent and nothing was decrypted. '
|
|
557
|
+
+ 'Whoever installed the tool has to copy the key again, in one block.',
|
|
558
|
+
'tag.key_mismatch':
|
|
559
|
+
'The tag on this page carries a key and a project id that do not go '
|
|
560
|
+
+ 'together: the key does not derive that id. Nothing was sent and '
|
|
561
|
+
+ 'nothing was decrypted, and the tool does not guess which of the two '
|
|
562
|
+
+ 'is right. A public tag needs the key alone -- the id is derived '
|
|
563
|
+
+ 'from it -- so remove data-project, or correct whichever of the two '
|
|
564
|
+
+ 'is wrong.',
|
|
499
565
|
|
|
500
566
|
/* -- Setup --------------------------------------------------------- */
|
|
501
567
|
'setup.title': 'Install annotepage on this site',
|
|
502
|
-
'setup.generate': 'Generate a
|
|
568
|
+
'setup.generate': 'Generate a key and create the project',
|
|
503
569
|
'setup.warning_title': 'Read this before continuing',
|
|
504
570
|
'setup.warning':
|
|
505
|
-
'The
|
|
571
|
+
'The key below is the ONLY secret of the project, and nobody else has '
|
|
506
572
|
+ 'it: not the server, not the author of the tool, nobody you can ask. '
|
|
507
|
-
+ '
|
|
573
|
+
+ 'KEY LOST = NOTES LOST, for good, with no recovery. Put it away now, '
|
|
508
574
|
+ 'where your team keeps its passwords, before continuing.',
|
|
509
|
-
'setup.salt': 'The project
|
|
575
|
+
'setup.salt': 'The project key -- keep it',
|
|
510
576
|
'setup.project': 'The project id -- public, it goes into the page',
|
|
511
577
|
'setup.tag': 'The tag to paste at the end of <body>, on the pages to annotate',
|
|
512
578
|
'setup.server': 'To declare in the server configuration',
|
|
513
579
|
'setup.copy': 'Copy',
|
|
514
580
|
'setup.copied': 'Copied',
|
|
515
581
|
'setup.copy_failed': 'Select the text and copy it by hand.',
|
|
516
|
-
'setup.continue': 'I have put the
|
|
582
|
+
'setup.continue': 'I have put the key away, continue',
|
|
517
583
|
'setup.done':
|
|
518
|
-
'The
|
|
584
|
+
'The key is remembered by this browser. Paste the tag above into the '
|
|
519
585
|
+ 'pages, declare the project on the server, then reload this page: the '
|
|
520
586
|
+ 'tool takes over.',
|
|
521
587
|
'setup.no_server':
|
|
@@ -1788,6 +1854,23 @@
|
|
|
1788
1854
|
empty(ui.body);
|
|
1789
1855
|
empty(ui.footer);
|
|
1790
1856
|
|
|
1857
|
+
/* THE PROJECT RUNS ON A KEY THAT IS IN THE PAGE, AND IT SAYS SO -- here,
|
|
1858
|
+
where the notes are, at every draw and not once at startup.
|
|
1859
|
+
|
|
1860
|
+
It sits above everything else, failures included, because it is not an
|
|
1861
|
+
event: it describes what the notes underneath ARE. A one-off message
|
|
1862
|
+
at load time would be read by whoever happened to be looking, once,
|
|
1863
|
+
and by nobody who opens this panel a week later.
|
|
1864
|
+
|
|
1865
|
+
The half that has to survive being skim-read is the WRITE half: the
|
|
1866
|
+
key gives read and write, this format has no reader role, so a page
|
|
1867
|
+
anybody can open is a page anybody can post to. */
|
|
1868
|
+
if (PUBLIC_KEY) {
|
|
1869
|
+
const notice = create('div', 'ap-public', T('public.notice'));
|
|
1870
|
+
notice.setAttribute('role', 'note');
|
|
1871
|
+
ui.body.appendChild(notice);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1791
1874
|
if (currentFailure) {
|
|
1792
1875
|
ui.body.appendChild(failureBlock(currentFailure, () => {
|
|
1793
1876
|
currentFailure = null;
|
|
@@ -1871,7 +1954,10 @@
|
|
|
1871
1954
|
origin and the salt has to be pasted once more, on every browser.
|
|
1872
1955
|
Without this button, one would have to clear the storage by hand to
|
|
1873
1956
|
get there. */
|
|
1874
|
-
|
|
1957
|
+
/* Not offered when the key comes from the tag: there is nothing stored
|
|
1958
|
+
to replace, and a key pasted here would be overruled by the tag on the
|
|
1959
|
+
next load -- while quietly leaving a copy in localStorage. */
|
|
1960
|
+
if (PROJECT && saltText && !PUBLIC_KEY) {
|
|
1875
1961
|
const changeSalt = create('button', 'ap-link', T('salt.replace'));
|
|
1876
1962
|
changeSalt.type = 'button';
|
|
1877
1963
|
changeSalt.title = T('salt.origin_changed');
|
|
@@ -2419,6 +2505,23 @@
|
|
|
2419
2505
|
});
|
|
2420
2506
|
};
|
|
2421
2507
|
|
|
2508
|
+
/* -- A tag that refuses itself ------------------------------------------
|
|
2509
|
+
|
|
2510
|
+
Same rule as a key pasted wrong: nothing sent, nothing decrypted, and the
|
|
2511
|
+
reason said out loud rather than a tool that quietly does not appear.
|
|
2512
|
+
|
|
2513
|
+
There is no field to correct here, and that is the difference with the
|
|
2514
|
+
salt screen: the mistake is in the page's source, not in this browser. So
|
|
2515
|
+
the screen names what has to change in the tag, and stops. */
|
|
2516
|
+
|
|
2517
|
+
const openTagScreen = (detail) => {
|
|
2518
|
+
const screen = blockingScreen(T('tag.title'), false);
|
|
2519
|
+
const block = create('div', 'ap-error');
|
|
2520
|
+
block.setAttribute('role', 'alert');
|
|
2521
|
+
block.appendChild(create('p', 'ap-error-detail', detail));
|
|
2522
|
+
screen.body.appendChild(block);
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2422
2525
|
/* -- The "this browser cannot" screen ------------------------------------ */
|
|
2423
2526
|
|
|
2424
2527
|
const openContextScreen = () => {
|
|
@@ -2566,7 +2669,47 @@
|
|
|
2566
2669
|
if (!CRYPTO) {
|
|
2567
2670
|
// Without a secure context nothing is possible -- but if somebody
|
|
2568
2671
|
// declared a project here, they have a right to know why.
|
|
2569
|
-
if (PROJECT || SETUP_REQUESTED) showScreen(openContextScreen);
|
|
2672
|
+
if (PROJECT || KEY_DECLARED || SETUP_REQUESTED) showScreen(openContextScreen);
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
/* THE TAG CARRIES THE KEY: the project is public, and that settles the
|
|
2677
|
+
mode before anything else. Nothing is asked, nothing is read from
|
|
2678
|
+
localStorage and NOTHING IS WRITTEN TO IT -- the key is in the page,
|
|
2679
|
+
and a stored copy would buy nothing except a divergent state on the
|
|
2680
|
+
day the tag changes. The interface then says so at every draw
|
|
2681
|
+
(PUBLIC_KEY, 60-ui). */
|
|
2682
|
+
if (KEY_DECLARED) {
|
|
2683
|
+
const keyBytes = saltFromText(DECLARED_KEY);
|
|
2684
|
+
if (!keyBytes) {
|
|
2685
|
+
/* An attribute somebody wrote on purpose, and it is not a key.
|
|
2686
|
+
Staying silent here would be the behaviour of a tag carrying
|
|
2687
|
+
no project at all, and the difference is exactly what nobody
|
|
2688
|
+
would find. */
|
|
2689
|
+
showScreen(() => openTagScreen(T('tag.key_shape')));
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
derive(keyBytes).then((derived) => {
|
|
2694
|
+
/* Both attributes on one tag: they have to AGREE, and the id is
|
|
2695
|
+
the one thing the key can check itself against (FORMAT.md
|
|
2696
|
+
1.2). Disagreement is refused exactly as a wrongly pasted key
|
|
2697
|
+
is -- nothing sent, nothing decrypted -- and no winner is
|
|
2698
|
+
picked: one of the two is a typo, and guessing buries it in a
|
|
2699
|
+
project whose notes nobody will ever see. */
|
|
2700
|
+
if (DECLARED_PROJECT && derived.id !== DECLARED_PROJECT) {
|
|
2701
|
+
showScreen(() => openTagScreen(T('tag.key_mismatch')));
|
|
2702
|
+
return null;
|
|
2703
|
+
}
|
|
2704
|
+
// The id is DERIVED, never declared twice: see 00-preamble.
|
|
2705
|
+
PROJECT = derived.id;
|
|
2706
|
+
PUBLIC_KEY = true;
|
|
2707
|
+
return startWithSalt(DECLARED_KEY, derived);
|
|
2708
|
+
}, () => {
|
|
2709
|
+
// derive() only fails when WebCrypto itself does, which is what
|
|
2710
|
+
// that screen is about.
|
|
2711
|
+
showScreen(openContextScreen);
|
|
2712
|
+
});
|
|
2570
2713
|
return;
|
|
2571
2714
|
}
|
|
2572
2715
|
|
package/labels/fr.json
CHANGED
|
@@ -65,40 +65,44 @@
|
|
|
65
65
|
"error.hide": "Masquer",
|
|
66
66
|
"read.newer_one": "1 note a été écrite par une version plus récente d’annotepage et n’a pas pu être lue.",
|
|
67
67
|
"read.newer_n": "{n} notes ont été écrites par une version plus récente d’annotepage et n’ont pas pu être lues.",
|
|
68
|
-
"read.unreadable_one": "1 note n’a pas pu être déchiffrée.
|
|
69
|
-
"read.unreadable_n": "{n} notes n’ont pas pu être déchiffrées.
|
|
68
|
+
"read.unreadable_one": "1 note n’a pas pu être déchiffrée. La clé de ce navigateur n’est peut-être pas celle avec laquelle elle a été écrite.",
|
|
69
|
+
"read.unreadable_n": "{n} notes n’ont pas pu être déchiffrées. La clé de ce navigateur n’est peut-être pas celle avec laquelle elles ont été écrites.",
|
|
70
70
|
"read.unknown_one": "1 note est écrite dans un mode que cet outil ne connaît pas et n’a pas été lue.",
|
|
71
71
|
"read.unknown_n": "{n} notes sont écrites dans un mode que cet outil ne connaît pas et n’ont pas été lues.",
|
|
72
72
|
"read.title_partial": "Certaines notes n’ont pas pu être lues",
|
|
73
73
|
"marker.one": "1 note ici",
|
|
74
74
|
"marker.n": "{n} notes ici",
|
|
75
|
-
"salt.title": "
|
|
76
|
-
"salt.help": "Les notes de ce projet sont chiffrées dans votre navigateur. Sans
|
|
77
|
-
"salt.label": "
|
|
78
|
-
"salt.confirm": "Utiliser
|
|
79
|
-
"salt.empty": "Collez
|
|
80
|
-
"salt.shape": "Ce n’est pas
|
|
81
|
-
"salt.wrong": "
|
|
82
|
-
"salt.origin_changed": "
|
|
83
|
-
"salt.not_kept": "Ce navigateur refuse de retenir
|
|
84
|
-
"salt.replace": "Coller
|
|
85
|
-
"salt.forget": "Oublier
|
|
75
|
+
"salt.title": "La clé de ce projet est nécessaire",
|
|
76
|
+
"salt.help": "Les notes de ce projet sont chiffrées dans votre navigateur. Sans la clé du projet, ce navigateur ne peut ni les lire, ni en écrire. Demandez-la à la personne qui a installé l’outil, et collez-la ci-dessous. Elle sera retenue par ce navigateur, pour ce site.",
|
|
77
|
+
"salt.label": "La clé du projet (43 caractères)",
|
|
78
|
+
"salt.confirm": "Utiliser cette clé",
|
|
79
|
+
"salt.empty": "Collez la clé avant de valider.",
|
|
80
|
+
"salt.shape": "Ce n’est pas une clé : on attend 43 caractères parmi A-Z a-z 0-9 - _, sans espace ni tiret décoratif. Recopiez-la d’un seul bloc.",
|
|
81
|
+
"salt.wrong": "Cette clé n’est pas celle de ce projet. Rien n’a été envoyé, rien n’a été déchiffré. Vérifiez que vous collez la clé du bon projet.",
|
|
82
|
+
"salt.origin_changed": "Cette clé est retenue par navigateur ET par domaine. Le jour où la préproduction devient la production, il faut la recoller une fois sur le nouveau domaine — les notes, elles, ne bougent pas.",
|
|
83
|
+
"salt.not_kept": "Ce navigateur refuse de retenir la clé (navigation privée, ou stockage bloqué). L’outil fonctionne pour cette page, mais la clé sera à recoller au prochain chargement.",
|
|
84
|
+
"salt.replace": "Coller une autre clé",
|
|
85
|
+
"salt.forget": "Oublier la clé sur ce navigateur",
|
|
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
|
+
"tag.title": "Cette balise annotepage est inutilisable telle quelle",
|
|
88
|
+
"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
|
+
"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.",
|
|
86
90
|
"setup.title": "Installer annotepage sur ce site",
|
|
87
|
-
"setup.generate": "Engendrer
|
|
91
|
+
"setup.generate": "Engendrer une clé et créer le projet",
|
|
88
92
|
"setup.warning_title": "À lire avant de continuer",
|
|
89
|
-
"setup.warning": "
|
|
90
|
-
"setup.salt": "
|
|
93
|
+
"setup.warning": "La clé ci-dessous est le SEUL secret du projet, et personne d’autre ne l’a : ni le serveur, ni l’auteur de l’outil, ni personne à qui la demander. CLÉ PERDUE = NOTES PERDUES, définitivement, sans recours. Rangez-la maintenant, là où votre équipe range ses mots de passe, avant de continuer.",
|
|
94
|
+
"setup.salt": "La clé du projet — à conserver",
|
|
91
95
|
"setup.project": "L’identifiant du projet — public, il va dans la page",
|
|
92
96
|
"setup.tag": "La balise à coller en fin de <body>, sur les pages à annoter",
|
|
93
97
|
"setup.server": "À déclarer dans la configuration du serveur",
|
|
94
98
|
"setup.copy": "Copier",
|
|
95
99
|
"setup.copied": "Copié",
|
|
96
100
|
"setup.copy_failed": "Sélectionnez le texte et copiez-le à la main.",
|
|
97
|
-
"setup.continue": "J’ai rangé
|
|
98
|
-
"setup.done": "
|
|
101
|
+
"setup.continue": "J’ai rangé la clé, continuer",
|
|
102
|
+
"setup.done": "La clé est retenue par ce navigateur. Collez la balise ci-dessus dans les pages, déclarez le projet côté serveur, puis rechargez cette page : l’outil prend la main.",
|
|
99
103
|
"setup.no_server": "Aucune adresse de serveur n’est déclarée sur la balise (data-server), et le client ne vient pas du site : il ne peut pas deviner où écrire. Ajoutez data-server à la balise.",
|
|
100
104
|
"setup.plain_mode": "Ce projet est déclaré en mode CLAIR : le serveur lira les remarques, les noms et les chemins. Ce mode n’est acceptable que si le serveur est le site lui-même, derrière la même restriction d’accès. Un relais le refusera.",
|
|
101
105
|
"context.title": "annotepage ne peut pas fonctionner sur cette page",
|
|
102
106
|
"context.help": "Le chiffrement des notes et le regroupement par page reposent sur WebCrypto, que le navigateur ne fournit que dans un contexte sûr : https, ou localhost. Cette page n’en est pas un. Rien ne peut être écrit ni relu ici tant qu’elle est servie ainsi.",
|
|
103
107
|
"date.unknown": "date inconnue"
|
|
104
|
-
}
|
|
108
|
+
}
|
package/package.json
CHANGED