@appsoftwareltd/etherpk-extension-api 0.10.2 → 0.11.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 CHANGED
@@ -123,11 +123,101 @@ person sets it, and hears every change with `context.settings.subscribe`. A swit
123
123
  graph, the settings follow them to their other devices, encrypted so that only those devices can read
124
124
  them.
125
125
 
126
+ `context.settings.set('api-key', value)` sets one of the manifest's settings, as the person would in
127
+ Settings, or clears it with `null`. It is for a screen of the extension's own where the person types
128
+ the value, such as a setup guide. It throws for a setting the manifest does not declare, and a switch
129
+ takes only `'true'` or `'false'`.
130
+
126
131
  ### Storage
127
132
 
128
133
  `context.storage` keeps the extension's own values on the device, as JSON, with `get`, `set` and
129
134
  `remove`. It is for what the extension works out or caches, not for what the person sets.
130
135
 
136
+ ### Records kept with the person
137
+
138
+ `context.personStorage` keeps the extension's own records with the person rather than on one
139
+ device, such as what signing in to another service gave it. It has `get`, `set`, `remove` and
140
+ `subscribe`, and keeps each record as JSON, at most 64 KB of it, under a key of letters, digits,
141
+ `.`, `_` and `-`. Where the person's account syncs a graph, the records follow them to their other
142
+ devices in the account's vault, encrypted so that only those devices can read them, and the later
143
+ change to a record wins. The person never sees them, and uninstalling the extension clears them on
144
+ every device.
145
+
146
+ A record is no more private than a setting. Any code in EtherPK's page can read it on the device,
147
+ and so can an agent the person gave an agent token for their account, so say in the extension what
148
+ it keeps.
149
+
150
+ ### Signing in to another service
151
+
152
+ `context.auth` runs an OAuth 2.0 sign-in in a popup. The service sends the popup back to the
153
+ Client's one return page, `context.auth.redirectUri` (the Client's address followed by
154
+ `/extension-sign-in`), which the person registers with the service once, for every extension.
155
+
156
+ ```ts
157
+ const answer = await context.auth.authorize(`https://accounts.example.com/authorize?client_id=…&redirect_uri=${encodeURIComponent(context.auth.redirectUri)}&state=${state}&…`, { signal })
158
+ if (answer.error) throw new Error(answer.error)
159
+ // answer.code: exchange it at the service's token endpoint.
160
+ ```
161
+
162
+ - The address must carry a `state`: only a return with the same `state` answers the call.
163
+ - Call `authorize` inside the click that asked for it, with nothing awaited before it, because a
164
+ browser blocks a popup opened later. Work that takes time, such as a PKCE challenge, is done
165
+ before the click.
166
+ - `authorize` resolves with the parameters the service sent back, `error` among them when the
167
+ person refused. It rejects when the browser blocks the popup, when `signal` aborts, or after ten
168
+ minutes. It does not reject when the person closes the popup, which the Client cannot always tell,
169
+ so offer a Cancel that aborts `signal`.
170
+ - The extension builds the request and exchanges the code itself: the Client knows no service.
171
+
172
+ ### Journal Panels
173
+
174
+ A Journal Panel draws beneath a journal entry's text, above its backlinks, about that day, such as
175
+ the day's events from a calendar. It is outside the document, so nothing in it is synced with the
176
+ graph, searched or published.
177
+
178
+ ```ts
179
+ context.journalPanels.register({
180
+ id: 'weather.day',
181
+ mount(element, { day, visibility }) {
182
+ // `day` is YYYY-MM-DD. Load once `visibility.onScreen` is true.
183
+ element.textContent = `Forecast for ${day}`
184
+ return { destroy: () => element.replaceChildren() }
185
+ },
186
+ })
187
+ ```
188
+
189
+ The Client mounts a panel for each journal tab's own entry, a day with nothing written included, and
190
+ never for the older days shown beneath it. A panel is as tall as what it draws, so one that draws
191
+ nothing takes no space, and it draws its own heading and spacing. It mounts in a shadow root with the
192
+ extension's stylesheets, as a View does.
193
+
194
+ ### Graph Sidebar buttons
195
+
196
+ A button in the Graph Sidebar, below Search and today's date, runs a Command:
197
+ `context.graphSidebar.register({ id, title, description, icon, order, when, command })`. These rules
198
+ keep the buttons readable together:
199
+
200
+ - **`title`** is short. Two buttons sit side by side in the sidebar at its default width of 304
201
+ pixels, and "Graph Kanban", with its icon, is about the widest that fits.
202
+ - **`description`** says what the button does, such as "Open a map of every place and route in this
203
+ graph". The Client shows it as the button's help text. Don't write a keyboard shortcut in it: the
204
+ Client adds the key chords bound to the button's Command itself.
205
+ - **`icon`** is clear on its own. On a phone the buttons show their icons only.
206
+ - **`when`** hides the button where it does not apply. A button for the desktop alone passes
207
+ `when: () => context.layout.isDesktop()`. A `when` that throws hides the button too.
208
+ - **`order`** places the button among the others. The Graph Map's is 20 and the Graph Kanban's is
209
+ 30.
210
+
211
+ ### Opening documents, the theme and Events
212
+
213
+ - `context.layout.openDocument(concept)` opens a document as following a wikilink does. A day's
214
+ name, `YYYY-MM-DD`, opens its journal entry.
215
+ - `context.theme.isDark()` says whether the dark theme shows, and the `theme:changed` Event reports a
216
+ switch. EtherPK's `--gk-*` colour tokens reach into a shadow root and change with the theme by
217
+ themselves.
218
+ - `context.events.on(name, listener)` hears the Client's Events: `document:active-changed`,
219
+ `concept:renamed`, `document:deleted`, `documents:changed` and `theme:changed`.
220
+
131
221
  ### Fence widgets
132
222
 
133
223
  A fence widget draws over a fenced block in a document, in place of its text. The extension
@@ -254,33 +344,15 @@ code runs under them.
254
344
  The Proposed parts are for the extensions that come with EtherPK, and may change in any release.
255
345
  They include:
256
346
 
257
- - Context Menu rows (`contextMenu`), key chords (`keybindings`) and Graph Sidebar buttons
258
- (`graphSidebar`).
259
- - Events (`events`), questions to the graph's index (`index`) and the rules for naming a concept
260
- (`concepts`).
261
- - The theme (`theme`), the Client's icons (`icons`) and a View that lives in a Sidebar (`resident`).
262
- - Running another extension's Commands (`commands.execute`) and opening documents
263
- (`layout.openDocument`).
347
+ - Context Menu rows (`contextMenu`) and key chords (`keybindings`).
348
+ - Questions to the graph's index (`index`) and the rules for naming a concept (`concepts`).
349
+ - The Client's icons (`icons`) and a View that lives in a Sidebar (`resident`).
350
+ - Running another extension's Commands (`commands.execute`), and the document the person is working
351
+ in (`layout.activeDocument`).
264
352
 
265
353
  `contributions`, which reaches the Client's own registries, stays Proposed for good, since its
266
354
  shape is the Client's internals.
267
355
 
268
- ### Graph Sidebar buttons
269
-
270
- A button in the Graph Sidebar, below Search and today's date, runs a Command. These rules keep the
271
- buttons readable together:
272
-
273
- - **`title`** is short. Two buttons sit side by side in the sidebar at its default width of 304
274
- pixels, and "Graph Kanban", with its icon, is about the widest that fits.
275
- - **`description`** says what the button does, such as "Open a map of every place and route in this
276
- graph". The Client shows it as the button's help text. Don't write a keyboard shortcut in it: the
277
- Client adds the key chords bound to the button's Command itself.
278
- - **`icon`** is clear on its own. On a phone the buttons show their icons only.
279
- - **`when`** hides the button where it does not apply. A button for the desktop alone passes
280
- `when: () => context.layout.isDesktop()`. A `when` that throws hides the button too.
281
- - **`order`** places the button among the others. The Graph Map's is 20 and the Graph Kanban's is
282
- 30.
283
-
284
356
  ## Trying an extension
285
357
 
286
358
  To try an extension, build it, and start a development copy of the EtherPK Client with the
package/dist/auth.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Signing in to another service from an extension (ADR 0140), such as a calendar, with OAuth 2.0 in
3
+ * a popup. Extensions cannot add pages to the Client, so the Client has one page every sign-in
4
+ * returns to, `redirectUri`, which passes what the service sent back to the window that opened the
5
+ * popup. The extension builds the sign-in request itself (its client id, scopes, PKCE and `state`),
6
+ * so nothing in the Client is tied to one service.
7
+ */
8
+ /**
9
+ * How long a sign-in may take, and how to give up on it.
10
+ *
11
+ * @public
12
+ */
13
+ export interface AuthorizeOptions {
14
+ /** Aborting it gives up on the sign-in: `authorize` rejects, and its popup is closed. */
15
+ signal?: AbortSignal;
16
+ }
17
+ /**
18
+ * Signing in to another service.
19
+ *
20
+ * @public
21
+ */
22
+ export interface ExtensionAuth {
23
+ /**
24
+ * The address a sign-in returns to: this Client's origin followed by `/extension-sign-in`. It is
25
+ * the same for every extension and every graph, so the person registers it with the service
26
+ * once.
27
+ */
28
+ readonly redirectUri: string;
29
+ /**
30
+ * Open `url`, the service's sign-in address, in a popup, and wait for the service to send the
31
+ * popup back to `redirectUri`. Resolves with the parameters it came back with, from its query
32
+ * and its fragment, such as `code` and `state`, or `error` when the person refused. The popup is
33
+ * closed by then.
34
+ *
35
+ * `url` must carry a `state` parameter: only a return with the same `state` answers this call,
36
+ * so two sign-ins at once, in this tab or another, never take each other's answer. Call it from
37
+ * a click or a key press, because a browser blocks a popup opened at any other time. Rejects when
38
+ * the popup is blocked, when `options.signal` aborts, or after ten minutes. A person who closes
39
+ * the popup without finishing is not reported, since the Client cannot always tell, so the
40
+ * extension offers a way to give up (`options.signal`).
41
+ */
42
+ authorize(url: string, options?: AuthorizeOptions): Promise<Record<string, string>>;
43
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Signing in to another service from an extension (ADR 0140), such as a calendar, with OAuth 2.0 in
3
+ * a popup. Extensions cannot add pages to the Client, so the Client has one page every sign-in
4
+ * returns to, `redirectUri`, which passes what the service sent back to the window that opened the
5
+ * popup. The extension builds the sign-in request itself (its client id, scopes, PKCE and `state`),
6
+ * so nothing in the Client is tied to one service.
7
+ */
8
+ export {};
package/dist/context.d.ts CHANGED
@@ -7,11 +7,13 @@
7
7
  * only through the index's answers and its own fences, and changes them only through its fences,
8
8
  * the blocks it puts at the caret and the Client's Commands.
9
9
  */
10
+ import type { ExtensionAuth } from './auth';
10
11
  import type { ExtensionDialogs } from './dialogs';
11
12
  import type { ExtensionEditor } from './editor';
12
13
  import type { ExtensionEmbeds } from './embeds';
13
14
  import type { ExtensionEvents } from './events';
14
15
  import type { ExtensionIndex } from './graph-index';
16
+ import type { ExtensionJournalPanels } from './journal';
15
17
  import type { CommandMenuItem, ContextMenuItem, GraphSidebarButton } from './menus';
16
18
  import type { ViewContribution, ViewRef } from './views';
17
19
  /**
@@ -74,7 +76,7 @@ export interface ExtensionCommandMenu {
74
76
  /**
75
77
  * Adding buttons to the Graph Sidebar's group of action buttons, after Search and today's date.
76
78
  *
77
- * @beta
79
+ * @public
78
80
  */
79
81
  export interface ExtensionGraphSidebar {
80
82
  /** Add a button, its id under the extension's id. */
@@ -128,9 +130,8 @@ export interface ExtensionLayout {
128
130
  /**
129
131
  * Open the document of a concept, under the name it resolves to, as following a wikilink
130
132
  * does. `line` scrolls to and selects that line, `inPaneOf` and `beside` place it as for
131
- * {@link ExtensionLayout.openView}.
132
- *
133
- * @beta
133
+ * {@link ExtensionLayout.openView}. A day's name, `YYYY-MM-DD`, opens its Journal Entry, or a Draft
134
+ * of it when nothing is written for that day yet.
134
135
  */
135
136
  openDocument(concept: string, options?: {
136
137
  line?: number;
@@ -175,7 +176,8 @@ export interface ExtensionStorage {
175
176
  * The person's answers to the settings the extension's manifest declares (`settings`, ADR 0134).
176
177
  * The Client draws them under the extension's entry in Settings and Extensions → Extensions, and
177
178
  * keeps them with the person: on this device, and in their account where it syncs a graph, so they
178
- * follow the person to their other devices. The extension only reads them.
179
+ * follow the person to their other devices. The extension reads them, and may set them where it
180
+ * explains them itself, such as a setup screen asking for a key.
179
181
  *
180
182
  * @public
181
183
  */
@@ -191,11 +193,49 @@ export interface ExtensionSettings {
191
193
  * Returns the way to stop.
192
194
  */
193
195
  subscribe(listener: () => void): () => void;
196
+ /**
197
+ * Set one of the settings the manifest declares, as the person would in Settings and Extensions,
198
+ * or clear it with null or empty text. A `boolean` setting takes `'true'` or `'false'`. Throws
199
+ * for a setting the manifest does not declare. Call it only for what the person typed or chose:
200
+ * the setting is theirs.
201
+ */
202
+ set(id: string, value: string | null): void;
203
+ }
204
+ /**
205
+ * The extension's own records, kept with the person (ADR 0140): on this device, and where the
206
+ * person's account syncs a graph, in that account's vault too, encrypted on the device so only the
207
+ * person's own devices can read them. So a record follows the person to their other devices, as
208
+ * Extension Settings do, such as what signing in to another service gave the extension. The person
209
+ * never sees these records, and uninstalling the extension clears them on every device.
210
+ *
211
+ * Each record is kept as JSON, at most 64 KB of it. Between two devices the later change to a
212
+ * record wins, record by record. A record is readable on the device by any code in the page, as
213
+ * localStorage is, and by a Headless Client given an agent token for the same account, which opens
214
+ * the account's vault.
215
+ *
216
+ * @public
217
+ */
218
+ export interface ExtensionPersonStorage {
219
+ /** The record under `key`, or undefined when there is none or it cannot be read. */
220
+ get<T>(key: string): T | undefined;
221
+ /**
222
+ * Keep a record under `key`: letters, digits, `.`, `_` and `-`, at most 100 of them. Setting
223
+ * undefined or null removes it. Throws when the key is not one of those, or the record's JSON is
224
+ * longer than 64 KB.
225
+ */
226
+ set(key: string, value: unknown): void;
227
+ /** Remove the record under `key`. */
228
+ remove(key: string): void;
229
+ /**
230
+ * Hear every change to the extension's records, made on this device or arriving from another.
231
+ * Returns the way to stop.
232
+ */
233
+ subscribe(listener: () => void): () => void;
194
234
  }
195
235
  /**
196
236
  * The Client's light or dark theme.
197
237
  *
198
- * @beta
238
+ * @public
199
239
  */
200
240
  export interface ExtensionTheme {
201
241
  /** True while the dark theme shows. The `theme:changed` Event reports a switch. */
@@ -293,12 +333,10 @@ export interface ExtensionContext {
293
333
  readonly contextMenu: ExtensionContextMenu;
294
334
  /** Adding Command Menu rows. */
295
335
  readonly commandMenu: ExtensionCommandMenu;
296
- /**
297
- * Adding buttons to the Graph Sidebar.
298
- *
299
- * @beta
300
- */
336
+ /** Adding buttons to the Graph Sidebar. */
301
337
  readonly graphSidebar: ExtensionGraphSidebar;
338
+ /** Adding panels beneath each Journal Entry. */
339
+ readonly journalPanels: ExtensionJournalPanels;
302
340
  /**
303
341
  * Adding key chords.
304
342
  *
@@ -311,11 +349,7 @@ export interface ExtensionContext {
311
349
  * @beta
312
350
  */
313
351
  readonly contributions: ExtensionContributions;
314
- /**
315
- * Listening to Events.
316
- *
317
- * @beta
318
- */
352
+ /** Listening to Events. */
319
353
  readonly events: ExtensionEvents;
320
354
  /**
321
355
  * Questions to the open graph's index.
@@ -335,11 +369,11 @@ export interface ExtensionContext {
335
369
  readonly storage: ExtensionStorage;
336
370
  /** The person's answers to the settings the manifest declares. */
337
371
  readonly settings: ExtensionSettings;
338
- /**
339
- * The Client's light or dark theme.
340
- *
341
- * @beta
342
- */
372
+ /** The extension's own records, kept with the person and following them to their devices. */
373
+ readonly personStorage: ExtensionPersonStorage;
374
+ /** Signing in to another service. */
375
+ readonly auth: ExtensionAuth;
376
+ /** The Client's light or dark theme. */
343
377
  readonly theme: ExtensionTheme;
344
378
  /**
345
379
  * The Client's icons and the extension's own.
package/dist/events.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * Each Event's name and what it carries. Every Event belongs to the open graph, named by
7
7
  * `graphId`.
8
8
  *
9
- * @beta
9
+ * @public
10
10
  */
11
11
  export interface ExtensionEventPayloads {
12
12
  /** The document the user is working in changed, or there is none (`documentId` is null). */
@@ -47,13 +47,13 @@ export interface ExtensionEventPayloads {
47
47
  /**
48
48
  * An Event's name.
49
49
  *
50
- * @beta
50
+ * @public
51
51
  */
52
52
  export type ExtensionEventName = keyof ExtensionEventPayloads;
53
53
  /**
54
54
  * Listening to Events.
55
55
  *
56
- * @beta
56
+ * @public
57
57
  */
58
58
  export interface ExtensionEvents {
59
59
  /** Listen to one Event until the returned function is called or the extension stops. */
package/dist/index.d.ts CHANGED
@@ -14,12 +14,14 @@
14
14
  *
15
15
  * @packageDocumentation
16
16
  */
17
- export type { CommandHandler, ExtensionCommandMenu, ExtensionCommands, ExtensionConcepts, ExtensionContext, ExtensionContextMenu, ExtensionContributions, ExtensionGraphSidebar, ExtensionIcons, ExtensionKeybindings, ExtensionLayout, ExtensionModule, ExtensionSettings, ExtensionStorage, ExtensionTheme, ExtensionViews, IconMarkup, Keybinding, } from './context';
17
+ export type { CommandHandler, ExtensionCommandMenu, ExtensionCommands, ExtensionConcepts, ExtensionContext, ExtensionContextMenu, ExtensionContributions, ExtensionGraphSidebar, ExtensionIcons, ExtensionKeybindings, ExtensionLayout, ExtensionModule, ExtensionPersonStorage, ExtensionSettings, ExtensionStorage, ExtensionTheme, ExtensionViews, IconMarkup, Keybinding, } from './context';
18
+ export type { AuthorizeOptions, ExtensionAuth } from './auth';
18
19
  export type { ExtensionDialogs, PromptOptions } from './dialogs';
19
20
  export type { ExtensionEditor, FenceContext, FenceEdit, FenceInfo, FenceOptions, FencePresentation, FenceWidget, MountedFence, PublishedFence, PublishFenceRequest, } from './editor';
20
21
  export type { ExtensionEmbeds, FrameOptions } from './embeds';
21
22
  export type { ExtensionEventName, ExtensionEventPayloads, ExtensionEvents } from './events';
22
23
  export type { ConceptCandidate, ConceptKind, ExtensionIndex, IndexUpdate, LinkGraph, LinkGraphConcept, LinkGraphLink } from './graph-index';
24
+ export type { ExtensionJournalPanels, JournalPanel, JournalPanelProps, MountedJournalPanel } from './journal';
23
25
  export type { ExtensionIsolation, ExtensionManifest, ExtensionPackage, ReadExtensionPackageResult, SettingDeclaration, ViewDeclaration, ViewRegion, } from './manifest';
24
26
  export { readExtensionPackage } from './manifest';
25
27
  export type { CommandMenuContext, CommandMenuItem, ContextMenuItem, DocumentMenuTarget, ExtensionMenuTarget, GraphSidebarButton, MenuTarget, TabMenuTarget, WikilinkMenuTarget, } from './menus';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The [[Journal Panel]]: a strip an extension draws beneath a Journal Entry's text and above its
3
+ * Backlinks, about the entry's day, such as that day's events from a calendar. It is outside the
4
+ * document: nothing in it is part of the entry's text, so it is never synced with the graph,
5
+ * searched, exported or published.
6
+ */
7
+ import type { ViewVisibility } from './views';
8
+ /**
9
+ * What a Journal Panel is mounted with.
10
+ *
11
+ * @public
12
+ */
13
+ export interface JournalPanelProps {
14
+ /** The Journal Entry's day, `YYYY-MM-DD`. */
15
+ day: string;
16
+ /**
17
+ * The id of the panel the entry's tab is open in, on a desktop: what a Layout call names to open
18
+ * something in the same Pane or beside it. A phone passes none. It is fixed for the panel's
19
+ * life: a tab moved to another Pane is drawn afresh there.
20
+ */
21
+ panelId?: string;
22
+ /**
23
+ * Whether the entry's tab can be seen right now, and when that changes. A panel that loads
24
+ * anything waits until it can be seen.
25
+ */
26
+ visibility: ViewVisibility;
27
+ }
28
+ /**
29
+ * A Journal Panel the extension has drawn, as the Client drives it.
30
+ *
31
+ * @public
32
+ */
33
+ export interface MountedJournalPanel {
34
+ /** The tab closed, the graph closed, or the extension was switched off. */
35
+ destroy(): void;
36
+ }
37
+ /**
38
+ * A Journal Panel an extension registers.
39
+ *
40
+ * @public
41
+ */
42
+ export interface JournalPanel {
43
+ /** Under the extension's id. */
44
+ id: string;
45
+ /**
46
+ * Draw the panel into `element`, which is empty and is the extension's alone. For a loaded
47
+ * extension it sits inside a shadow root that holds the extension's own stylesheets, as a View
48
+ * does, and it is as tall as what the extension draws in it: a panel that draws nothing takes no
49
+ * space. The Client calls it once for each journal tab, the tab's own entry only, a Draft
50
+ * included, and never for the older entries of the Journal Roll. A file dropped on the panel is
51
+ * added to the end of the entry, as one dropped below the text is, unless the panel's own drop
52
+ * handler calls `preventDefault`.
53
+ */
54
+ mount(element: HTMLElement, props: JournalPanelProps): MountedJournalPanel;
55
+ }
56
+ /**
57
+ * Adding Journal Panels.
58
+ *
59
+ * @public
60
+ */
61
+ export interface ExtensionJournalPanels {
62
+ /**
63
+ * Add a panel beneath every journal tab's entry, those open already included. Panels from
64
+ * several extensions stack in the order they were added.
65
+ */
66
+ register(panel: JournalPanel): () => void;
67
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/menus.d.ts CHANGED
@@ -154,7 +154,7 @@ export interface CommandMenuItem {
154
154
  * to what the extension shows for the whole graph, such as the Graph Map. Pressing it runs
155
155
  * `command`.
156
156
  *
157
- * @beta
157
+ * @public
158
158
  */
159
159
  export interface GraphSidebarButton {
160
160
  /** Under the extension's id. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsoftwareltd/etherpk-extension-api",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "description": "The API an EtherPK extension is written against: the context it is started with, its manifest, and the contract its Views follow.",
6
6
  "type": "module",