@appsoftwareltd/etherpk-extension-api 0.0.0-stage → 0.9.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 App Software Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ # EtherPK Extension API
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The types an [EtherPK](https://etherpk.com) extension is written against, and the function that
4
+ checks an extension's manifest the way the EtherPK Client does. MIT licensed.
5
+
6
+ An extension is an npm package. Its package.json carries an `etherpk` manifest, and its main
7
+ module exports `activate(context)`, which the Client calls as each graph opens:
8
+
9
+ ```ts
10
+ import type { ExtensionContext } from '@appsoftwareltd/etherpk-extension-api'
11
+
12
+ export function activate(context: ExtensionContext): void {
13
+ context.commands.register('hello.greet', () => context.notify('Hello from an extension.'))
14
+ }
15
+ ```
16
+
17
+ ```json
18
+ {
19
+ "name": "etherpk-extension-hello",
20
+ "version": "1.0.0",
21
+ "etherpk": {
22
+ "id": "hello",
23
+ "displayName": "Hello",
24
+ "publisher": "You",
25
+ "minClientVersion": "0.8.54",
26
+ "main": "./dist/main.js"
27
+ }
28
+ }
29
+ ```
30
+
31
+ Everything here is Proposed (`@beta` in the type declarations) and may change in any release until
32
+ extensions from other publishers can be installed. From then on, a Stable declaration (`@public`)
33
+ never breaks. `etc/` holds the API reports CI checks every change against.
34
+
35
+ ## Settings
36
+
37
+ A value the person sets, such as a key for a service, is declared in the manifest, and the Client
38
+ draws it under the extension's entry in Settings and Extensions → Extensions:
39
+
40
+ ```json
41
+ "settings": [
42
+ {
43
+ "id": "api-key",
44
+ "type": "secret",
45
+ "title": "Weather service key",
46
+ "description": "Forecasts come from your own account with the weather service.",
47
+ "placeholder": "wk_…",
48
+ "link": { "title": "Getting a key", "url": "https://weather.example.com/keys" }
49
+ }
50
+ ]
51
+ ```
52
+
53
+ `type` is `text`, `secret` or `boolean`. A secret stays masked until the person shows it. A
54
+ `boolean` is a switch, which may name its starting position with `"default": true`. The person can
55
+ set them while the extension is switched off. The extension reads the value with
56
+ `context.settings.get('api-key')`, which is undefined until it is set, and hears every change with
57
+ `context.settings.subscribe`. A switch always reads `'true'` or `'false'`, its default until the
58
+ person changes it. Settings belong to the person rather than a graph. Where their
59
+ account syncs a graph, they follow the person to their other devices, encrypted so only those
60
+ devices can read them.
61
+
62
+ ## Writing to the page
63
+
64
+ The Client enforces [Trusted Types](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API),
65
+ and extension code runs under it:
66
+
67
+ - **An extension cannot create a Trusted Types policy.** The Client's Content Security Policy names
68
+ the policies it allows, and each belongs to the Client or a library it ships.
69
+ - **Write no HTML.** Build elements with the DOM (`createElement`, `createElementNS`,
70
+ `textContent`). A string written to an HTML sink such as `innerHTML` is sanitized by the Client's
71
+ default policy, which may remove what you wrote.
72
+ - **Draw icons with `context.icons.svg`.** It returns `IconMarkup`, which Svelte's `{@html}` takes
73
+ as it is. The icons your manifest adds are sanitized as SVG before they are drawn, so they can
74
+ hold only SVG.
@@ -0,0 +1,291 @@
1
+ /**
2
+ * The context an extension is started with: one open graph, as far as an extension may reach it.
3
+ *
4
+ * Everything an extension adds through it sits under the extension's id and is removed when the
5
+ * graph closes or the extension is switched off, so an extension never has to undo its own
6
+ * registrations. It holds no document store, no keys and no sync: an extension reads documents
7
+ * only through the index's answers, and changes them only by running the Client's Commands.
8
+ */
9
+ import type { ExtensionEvents } from './events';
10
+ import type { ExtensionIndex } from './graph-index';
11
+ import type { CommandMenuItem, ContextMenuItem, GraphSidebarButton } from './menus';
12
+ import type { ViewContribution, ViewRef } from './views';
13
+ /**
14
+ * A Command's handler. What it returns, or resolves to, is what `execute` resolves to.
15
+ *
16
+ * @beta
17
+ */
18
+ export type CommandHandler = (arg?: unknown) => unknown;
19
+ /**
20
+ * Running and adding Commands.
21
+ *
22
+ * @beta
23
+ */
24
+ export interface ExtensionCommands {
25
+ /** Add a Command, its id under the extension's id. Throws if the id is taken or outside it. */
26
+ register(id: string, handler: CommandHandler): () => void;
27
+ /** Run any Command, the Client's or an extension's. Rejects if no Command has the id. */
28
+ execute(id: string, arg?: unknown): Promise<unknown>;
29
+ /** Whether any Command has the id. */
30
+ has(id: string): boolean;
31
+ }
32
+ /**
33
+ * Supplying the Views the manifest declares.
34
+ *
35
+ * @beta
36
+ */
37
+ export interface ExtensionViews {
38
+ /**
39
+ * Supply the View for a kind the manifest declares, so its tabs draw. Throws for a kind the
40
+ * manifest does not declare.
41
+ */
42
+ register(kind: string, view: ViewContribution): () => void;
43
+ }
44
+ /**
45
+ * Adding Context Menu rows.
46
+ *
47
+ * @beta
48
+ */
49
+ export interface ExtensionContextMenu {
50
+ /** Add a row, its id under the extension's id. */
51
+ register(item: ContextMenuItem): () => void;
52
+ }
53
+ /**
54
+ * Adding Command Menu rows.
55
+ *
56
+ * @beta
57
+ */
58
+ export interface ExtensionCommandMenu {
59
+ /** Add a row, its id under the extension's id. */
60
+ register(item: CommandMenuItem): () => void;
61
+ }
62
+ /**
63
+ * Adding buttons to the Graph Sidebar, under Today's journal.
64
+ *
65
+ * @beta
66
+ */
67
+ export interface ExtensionGraphSidebar {
68
+ /** Add a button, its id under the extension's id. */
69
+ register(button: GraphSidebarButton): () => void;
70
+ }
71
+ /**
72
+ * A key chord that runs a Command.
73
+ *
74
+ * @beta
75
+ */
76
+ export interface Keybinding {
77
+ /** `Alt+M`, `Mod+Shift+K`: `Mod` is Cmd on Apple devices and Ctrl elsewhere. */
78
+ key: string;
79
+ /** The Command it runs. */
80
+ command: string;
81
+ /** What the keyboard shortcuts card lists it as. */
82
+ label: string;
83
+ /** The card's heading it is listed under. */
84
+ group?: string;
85
+ }
86
+ /**
87
+ * Adding key chords.
88
+ *
89
+ * @beta
90
+ */
91
+ export interface ExtensionKeybindings {
92
+ /** Add a chord. Throws if the Client or another extension already binds it. */
93
+ register(binding: Keybinding): () => void;
94
+ }
95
+ /**
96
+ * Opening Views and documents in the open graph's Layout.
97
+ *
98
+ * @beta
99
+ */
100
+ export interface ExtensionLayout {
101
+ /**
102
+ * Open a View. With `inPaneOf`, a panel id, it opens as a tab in that panel's Pane. With
103
+ * `beside`, it opens in another Pane beside that panel, so both show at once. Otherwise it
104
+ * opens where its kind belongs, or comes to the front if it is open already.
105
+ */
106
+ openView(view: ViewRef, options?: {
107
+ inPaneOf?: string;
108
+ beside?: string;
109
+ }): void;
110
+ /** Bring a Sidebar resident to the front of its expanded Sidebar. */
111
+ reveal(kind: string): void;
112
+ /**
113
+ * Open the document of a concept, under the name it resolves to, as following a wikilink
114
+ * does. `line` scrolls to and selects that line, `inPaneOf` and `beside` place it as for
115
+ * {@link ExtensionLayout.openView}.
116
+ */
117
+ openDocument(concept: string, options?: {
118
+ line?: number;
119
+ inPaneOf?: string;
120
+ beside?: string;
121
+ }): void;
122
+ /** The document the user is working in, or null. */
123
+ activeDocument(): string | null;
124
+ /** True while the desktop presenter is showing, 1024 pixels wide and more. */
125
+ isDesktop(): boolean;
126
+ }
127
+ /**
128
+ * The rules for naming a concept, which every index answer follows.
129
+ *
130
+ * @beta
131
+ */
132
+ export interface ExtensionConcepts {
133
+ /** The case-insensitive key of a name. */
134
+ key(name: string): string;
135
+ /** The name a concept resolves to: an alias's page, a page's own title, or the name itself. */
136
+ canonicalName(name: string): string;
137
+ }
138
+ /**
139
+ * The extension's own storage on this device, under `etherpk-<extension id>:` in the browser's
140
+ * localStorage. Values are stored as JSON.
141
+ *
142
+ * @beta
143
+ */
144
+ export interface ExtensionStorage {
145
+ /** The value stored under `key`, or undefined when there is none or it cannot be read. */
146
+ get<T>(key: string): T | undefined;
147
+ /** Store a value. A browser that refuses storage (a private window) keeps nothing, silently. */
148
+ set(key: string, value: unknown): void;
149
+ /** Forget the value under `key`. */
150
+ remove(key: string): void;
151
+ }
152
+ /**
153
+ * The person's answers to the settings the extension's manifest declares (`settings`, ADR 0134).
154
+ * The Client draws them under the extension's entry in Settings and Extensions → Extensions, and
155
+ * keeps them with the person: on this device, and in their account where it syncs a graph, so they
156
+ * follow the person to their other devices. The extension only reads them.
157
+ *
158
+ * @beta
159
+ */
160
+ export interface ExtensionSettings {
161
+ /**
162
+ * A setting's value, by its id in the manifest, or undefined when the person has not set it. A
163
+ * `boolean` setting is always `'true'` or `'false'`: its manifest's `default` until the person
164
+ * changes it.
165
+ */
166
+ get(id: string): string | undefined;
167
+ /**
168
+ * Hear every change to the person's settings, made on this device or arriving from another.
169
+ * Returns the way to stop.
170
+ */
171
+ subscribe(listener: () => void): () => void;
172
+ }
173
+ /**
174
+ * The Client's light or dark theme.
175
+ *
176
+ * @beta
177
+ */
178
+ export interface ExtensionTheme {
179
+ /** True while the dark theme shows. The `theme:changed` Event reports a switch. */
180
+ isDark(): boolean;
181
+ }
182
+ /**
183
+ * SVG markup the Client made, for an HTML sink such as Svelte's `{@html}`. It is not a string, so a
184
+ * string cannot stand in for it. The Client enforces Trusted Types, and this is trusted markup there:
185
+ * it goes into the page as it is. A string built from it is a plain string again, which the
186
+ * Client's default policy sanitizes.
187
+ *
188
+ * @beta
189
+ */
190
+ export interface IconMarkup {
191
+ /** Never present at run time. It marks the type, so a plain string is not one. */
192
+ readonly __brand: 'IconMarkup';
193
+ }
194
+ /**
195
+ * Drawing the Client's icons and the extension's own.
196
+ *
197
+ * @beta
198
+ */
199
+ export interface ExtensionIcons {
200
+ /**
201
+ * An icon as SVG markup: the extension's own, by the name its manifest gives it, or one of the
202
+ * Client's. An unknown name draws a small dot. The Client sanitizes an extension's own icon
203
+ * before it draws it, so its markup can hold only SVG.
204
+ */
205
+ svg(name: string, options?: {
206
+ size?: number;
207
+ strokeWidth?: number;
208
+ label?: string;
209
+ }): IconMarkup;
210
+ }
211
+ /**
212
+ * One registry of every Contribution Point, by kind, for the kinds the typed parts of the context
213
+ * do not cover. Only a Built-in Extension uses it.
214
+ *
215
+ * @beta
216
+ */
217
+ export interface ExtensionContributions {
218
+ /** Add a contribution. Throws if the kind already holds the id. */
219
+ register(kind: string, id: string, value: unknown): () => void;
220
+ /** Remove one of the extension's own contributions. */
221
+ unregister(kind: string, id: string): void;
222
+ /** The contribution at (`kind`, `id`), anyone's, or undefined. */
223
+ get(kind: string, id: string): unknown;
224
+ /** Whether anyone has a contribution at (`kind`, `id`). */
225
+ has(kind: string, id: string): boolean;
226
+ /** Every contribution of a kind, the extension's and everyone else's, in the order added. */
227
+ list(kind: string): {
228
+ id: string;
229
+ value: unknown;
230
+ }[];
231
+ }
232
+ /**
233
+ * What an extension is started with, once for each graph that opens.
234
+ *
235
+ * @beta
236
+ */
237
+ export interface ExtensionContext {
238
+ /** The extension's id, from its manifest. */
239
+ readonly extensionId: string;
240
+ /** The open graph. */
241
+ readonly graphId: string;
242
+ /** The Client's version, `x.y.z`. */
243
+ readonly clientVersion: string;
244
+ /** True in a development build of the Client, where tests may reach in. */
245
+ readonly dev: boolean;
246
+ /** Supplying the Views the manifest declares. */
247
+ readonly views: ExtensionViews;
248
+ /** Running and adding Commands. */
249
+ readonly commands: ExtensionCommands;
250
+ /** Adding Context Menu rows. */
251
+ readonly contextMenu: ExtensionContextMenu;
252
+ /** Adding Command Menu rows. */
253
+ readonly commandMenu: ExtensionCommandMenu;
254
+ /** Adding buttons to the Graph Sidebar. */
255
+ readonly graphSidebar: ExtensionGraphSidebar;
256
+ /** Adding key chords. */
257
+ readonly keybindings: ExtensionKeybindings;
258
+ /** Every Contribution Point by kind, for the kinds the parts above do not cover. */
259
+ readonly contributions: ExtensionContributions;
260
+ /** Listening to Events. */
261
+ readonly events: ExtensionEvents;
262
+ /** Questions to the open graph's index. */
263
+ readonly index: ExtensionIndex;
264
+ /** Opening Views and documents. */
265
+ readonly layout: ExtensionLayout;
266
+ /** The rules for naming a concept. */
267
+ readonly concepts: ExtensionConcepts;
268
+ /** The extension's own storage on this device. */
269
+ readonly storage: ExtensionStorage;
270
+ /** The person's answers to the settings the manifest declares. */
271
+ readonly settings: ExtensionSettings;
272
+ /** The Client's light or dark theme. */
273
+ readonly theme: ExtensionTheme;
274
+ /** The Client's icons and the extension's own. */
275
+ readonly icons: ExtensionIcons;
276
+ /** Say something in passing, as the Client's own notices do: "Coordinates copied." */
277
+ notify(text: string): void;
278
+ }
279
+ /**
280
+ * What an extension's main module exports.
281
+ *
282
+ * @beta
283
+ */
284
+ export interface ExtensionModule {
285
+ /**
286
+ * Called as each graph opens, or when the extension is switched on with a graph open. What the
287
+ * extension registered through the context is removed by the Client. A returned function is
288
+ * called after that, for anything else the extension holds.
289
+ */
290
+ activate(context: ExtensionContext): void | (() => void);
291
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The Events an extension may listen to. An Event reports something that happened in the Client.
3
+ * A listener reacts to it but cannot change it, and a listener that throws harms nothing else.
4
+ */
5
+ /**
6
+ * Each Event's name and what it carries. Every Event belongs to the open graph, named by
7
+ * `graphId`.
8
+ *
9
+ * @beta
10
+ */
11
+ export interface ExtensionEventPayloads {
12
+ /** The document the user is working in changed, or there is none (`documentId` is null). */
13
+ 'document:active-changed': {
14
+ graphId: string;
15
+ documentId: string | null;
16
+ };
17
+ /**
18
+ * A concept was renamed: its page, when it has one, and every wikilink naming it. The Client's
19
+ * own tabs, and the extension's Views about a concept, follow by themselves; this is for state
20
+ * an extension keeps under the old name, and it is reported before those Views move.
21
+ *
22
+ * A rename that moves concepts scoped by the renamed one reports each concept it moved, once,
23
+ * under the exact name it now lives on as: the scoped concepts first and the renamed concept
24
+ * last. A listener that moves what it keeps by exact name, Event by Event, moves each thing
25
+ * once, even when the new name is one the old name scopes (`Acme` to `[[Acme]] Archive`).
26
+ */
27
+ 'concept:renamed': {
28
+ graphId: string;
29
+ from: string;
30
+ to: string;
31
+ };
32
+ /** A document was deleted. */
33
+ 'document:deleted': {
34
+ graphId: string;
35
+ documentId: string;
36
+ };
37
+ /** The set of documents changed: one was added, removed or renamed. */
38
+ 'documents:changed': {
39
+ graphId: string;
40
+ };
41
+ /** The light or dark theme was switched on. */
42
+ 'theme:changed': {
43
+ graphId: string;
44
+ dark: boolean;
45
+ };
46
+ }
47
+ /**
48
+ * An Event's name.
49
+ *
50
+ * @beta
51
+ */
52
+ export type ExtensionEventName = keyof ExtensionEventPayloads;
53
+ /**
54
+ * Listening to Events.
55
+ *
56
+ * @beta
57
+ */
58
+ export interface ExtensionEvents {
59
+ /** Listen to one Event until the returned function is called or the extension stops. */
60
+ on<K extends ExtensionEventName>(name: K, listener: (payload: ExtensionEventPayloads[K]) => void): () => void;
61
+ }
package/dist/events.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The Events an extension may listen to. An Event reports something that happened in the Client.
3
+ * A listener reacts to it but cannot change it, and a listener that throws harms nothing else.
4
+ */
5
+ export {};
@@ -0,0 +1,103 @@
1
+ /**
2
+ * What an extension may ask the Derived Index, the open graph's searchable copy of its documents.
3
+ * Every question is read-only, and a Protected Document contributes only what it does everywhere:
4
+ * its name, never its content.
5
+ */
6
+ /**
7
+ * What kind of name a concept is.
8
+ *
9
+ * @beta
10
+ */
11
+ export type ConceptKind = 'page' | 'journal' | 'alias' | 'pageless';
12
+ /**
13
+ * One name the index knows: a page, a journal entry, an alias, or a Pageless Concept (a name
14
+ * wikilinks use that has no page).
15
+ *
16
+ * @beta
17
+ */
18
+ export interface ConceptCandidate {
19
+ /** The name as written. */
20
+ display: string;
21
+ /** The case-insensitive key every index answer uses for it. */
22
+ key: string;
23
+ /** What kind of name it is. */
24
+ kind: ConceptKind;
25
+ /** For an alias: the name of the page it resolves to. */
26
+ canonical?: string;
27
+ /** For a Pageless Concept: how many wikilinks name it. */
28
+ references?: number;
29
+ /** Present, and true, when the name opens a Protected Document. */
30
+ protected?: true;
31
+ }
32
+ /**
33
+ * One concept in the link graph.
34
+ *
35
+ * @beta
36
+ */
37
+ export interface LinkGraphConcept {
38
+ /** The case-insensitive key every other answer uses. */
39
+ key: string;
40
+ /** The page's title, or for a Pageless Concept the casing most of its mentions use. */
41
+ name: string;
42
+ /** A page, a journal entry, or a name only wikilinks use. */
43
+ kind: 'page' | 'journal' | 'pageless';
44
+ /** For a journal entry named for a day that exists: that day, `YYYY-MM-DD`. */
45
+ day?: string;
46
+ /** Present, and true, only for a Protected Document. */
47
+ protected?: true;
48
+ }
49
+ /**
50
+ * One line, from the document whose text holds the wikilinks to the concept they name. Both ends
51
+ * are positions in {@link LinkGraph.concepts}.
52
+ *
53
+ * @beta
54
+ */
55
+ export interface LinkGraphLink {
56
+ /** The document whose text holds the wikilinks, by its position in `concepts`. */
57
+ source: number;
58
+ /** The concept they name, by its position in `concepts`. */
59
+ target: number;
60
+ /** How many wikilinks in the source name the target, under its name or any alias. */
61
+ mentions: number;
62
+ /** Present, and true, when the source's own name is scoped by the target. */
63
+ inTitle?: true;
64
+ }
65
+ /**
66
+ * Every concept and every line a wikilink draws between two of them.
67
+ *
68
+ * @beta
69
+ */
70
+ export interface LinkGraph {
71
+ /** Sorted by key, so two answers over the same index are identical. */
72
+ concepts: LinkGraphConcept[];
73
+ /** Sorted by source, then target. A pair linked both ways is two links. */
74
+ links: LinkGraphLink[];
75
+ }
76
+ /**
77
+ * What changed in one update the index committed.
78
+ *
79
+ * @beta
80
+ */
81
+ export interface IndexUpdate {
82
+ /** The whole index was read again, so anything may have changed. */
83
+ full: boolean;
84
+ /** The keys of the concepts whose documents changed. */
85
+ changedConceptKeys: ReadonlySet<string>;
86
+ /** The keys of the concepts whose backlinks changed. */
87
+ backlinkTargetsChanged: ReadonlySet<string>;
88
+ /** A place or route may have changed. Always true for a full update. */
89
+ mapsChanged: boolean;
90
+ }
91
+ /**
92
+ * The questions an extension may ask the open graph's index.
93
+ *
94
+ * @beta
95
+ */
96
+ export interface ExtensionIndex {
97
+ /** Every name the index knows, aliases and Pageless Concepts included. */
98
+ allConcepts(): readonly ConceptCandidate[];
99
+ /** Every concept, and every pair a wikilink joins. */
100
+ linkGraph(): Promise<LinkGraph>;
101
+ /** Called after each update the index commits. Returns the unsubscribe. */
102
+ onUpdated(listener: (update: IndexUpdate) => void): () => void;
103
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * What an extension may ask the Derived Index, the open graph's searchable copy of its documents.
3
+ * Every question is read-only, and a Protected Document contributes only what it does everywhere:
4
+ * its name, never its content.
5
+ */
6
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The API an EtherPK extension is written against.
3
+ *
4
+ * An extension is an npm package whose package.json carries an `etherpk` manifest and whose main
5
+ * module exports `activate(context)`. The Client starts it as each graph opens, with an
6
+ * {@link ExtensionContext} for that graph. This package holds the types of that context, of the
7
+ * manifest and of the contract a View follows, and {@link readExtensionPackage}, which checks a
8
+ * manifest the way the Client does. It holds none of the Client's implementation.
9
+ *
10
+ * Every declaration is Proposed (`@beta`) until extensions from other publishers can be installed:
11
+ * until then it may change in any release. Stable declarations (`@public`) will never break.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ export type { CommandHandler, ExtensionCommandMenu, ExtensionCommands, ExtensionConcepts, ExtensionContext, ExtensionContextMenu, ExtensionContributions, ExtensionGraphSidebar, ExtensionIcons, ExtensionKeybindings, ExtensionLayout, ExtensionModule, ExtensionSettings, ExtensionStorage, ExtensionTheme, ExtensionViews, IconMarkup, Keybinding, } from './context';
16
+ export type { ExtensionEventName, ExtensionEventPayloads, ExtensionEvents } from './events';
17
+ export type { ConceptCandidate, ConceptKind, ExtensionIndex, IndexUpdate, LinkGraph, LinkGraphConcept, LinkGraphLink } from './graph-index';
18
+ export type { ExtensionManifest, ExtensionPackage, ReadExtensionPackageResult, SettingDeclaration, ViewDeclaration, ViewRegion, } from './manifest';
19
+ export { readExtensionPackage } from './manifest';
20
+ export type { CommandMenuContext, CommandMenuItem, ContextMenuItem, DocumentMenuTarget, ExtensionMenuTarget, GraphSidebarButton, MenuTarget, TabMenuTarget, WikilinkMenuTarget, } from './menus';
21
+ export type { MountedView, ViewContribution, ViewMountProps, ViewRef, ViewVisibility } from './views';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The API an EtherPK extension is written against.
3
+ *
4
+ * An extension is an npm package whose package.json carries an `etherpk` manifest and whose main
5
+ * module exports `activate(context)`. The Client starts it as each graph opens, with an
6
+ * {@link ExtensionContext} for that graph. This package holds the types of that context, of the
7
+ * manifest and of the contract a View follows, and {@link readExtensionPackage}, which checks a
8
+ * manifest the way the Client does. It holds none of the Client's implementation.
9
+ *
10
+ * Every declaration is Proposed (`@beta`) until extensions from other publishers can be installed:
11
+ * until then it may change in any release. Stable declarations (`@public`) will never break.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ export { readExtensionPackage } from './manifest';
@@ -0,0 +1,153 @@
1
+ /**
2
+ * An extension's manifest: the `etherpk` field of its package.json (ADR 0121), and the one helper
3
+ * that reads and checks it. The Client reads the same field with the same function, so an author
4
+ * and the Client never disagree about what a manifest means.
5
+ */
6
+ /**
7
+ * Where a View kind opens when nothing says otherwise.
8
+ *
9
+ * @beta
10
+ */
11
+ export type ViewRegion = 'main' | 'left-sidebar' | 'right-sidebar';
12
+ /**
13
+ * One View kind an extension adds, declared so the Client can show its tabs, titles and loading
14
+ * state before any of the extension's code has run.
15
+ *
16
+ * @beta
17
+ */
18
+ export interface ViewDeclaration {
19
+ /** The kind, `<extension id>.<name>`: the key every saved Layout stores it under. */
20
+ kind: string;
21
+ /**
22
+ * The target of a kind with one View per graph, a Sidebar resident or a whole-graph View
23
+ * (`"whole"`). A kind whose Views differ by target, one board per concept, leaves it out.
24
+ */
25
+ target?: string;
26
+ /**
27
+ * The tab's title. `{target}` stands for the View's target, so `"Kanban: {target}"` titles a
28
+ * board by its concept. The text before `{target}` is a fixed label the tab keeps room for.
29
+ */
30
+ title: string;
31
+ /** An icon drawn before the title: one of the extension's own icons, or one of the Client's. */
32
+ icon?: string;
33
+ /** Where the kind opens when nothing says otherwise. */
34
+ region: ViewRegion;
35
+ /**
36
+ * The View lives in a Sidebar from the moment a graph opens, beside the Client's own residents,
37
+ * and the Sidebar's toggle brings it back if it was closed. `desktopOnly` keeps it off a phone.
38
+ */
39
+ resident?: {
40
+ side: 'left' | 'right';
41
+ desktopOnly?: boolean;
42
+ };
43
+ /**
44
+ * A short address of the View's own: `/g/<graph>/<segment>`, followed by the target when
45
+ * `target` is `concept`. Only a Built-in Extension may declare one. A View kind whose address
46
+ * takes a concept follows that concept through a rename.
47
+ */
48
+ address?: {
49
+ segment: string;
50
+ target?: 'concept';
51
+ };
52
+ }
53
+ /**
54
+ * One thing an extension asks the person to set (ADR 0134). The Client draws it under the
55
+ * extension's entry in Settings and Extensions → Extensions, where it can be set while the extension
56
+ * is off, and keeps the answer with the person: on this device, and in their account where it syncs
57
+ * a graph, so it follows them to their other devices. The extension reads it through
58
+ * `context.settings`.
59
+ *
60
+ * @beta
61
+ */
62
+ export interface SettingDeclaration {
63
+ /** Unique within the extension: lowercase letters, digits and hyphens, such as `mapbox-token`. */
64
+ id: string;
65
+ /**
66
+ * `text`; `secret` for a key or a password, which is shown masked until the person asks to see
67
+ * it; or `boolean`, a switch, which `context.settings` reads as `'true'` or `'false'`.
68
+ */
69
+ type: 'text' | 'secret' | 'boolean';
70
+ /** The field's label, or for a switch what it turns on. */
71
+ title: string;
72
+ /** A switch's position until the person changes it. Only on a `boolean` setting, and off when left out. */
73
+ default?: boolean;
74
+ /** Plain text under the field: what the setting does, and what the person needs for it. */
75
+ description?: string;
76
+ /** Example text shown in the empty field. */
77
+ placeholder?: string;
78
+ /** A page that explains the setting, linked under its description. An `https` address. */
79
+ link?: {
80
+ title: string;
81
+ url: string;
82
+ };
83
+ }
84
+ /**
85
+ * The `etherpk` field of an extension's package.json.
86
+ *
87
+ * @beta
88
+ */
89
+ export interface ExtensionManifest {
90
+ /**
91
+ * Lowercase letters, digits and hyphens, starting with a letter. It prefixes every View kind,
92
+ * Command id, icon name and storage key the extension owns, so it is short.
93
+ */
94
+ id: string;
95
+ /** The name people see, in Settings and wherever the extension is listed. */
96
+ displayName: string;
97
+ /** Who made the extension. */
98
+ publisher: string;
99
+ /** The lowest Client version the extension runs on, `x.y.z`. Required of a loaded extension. */
100
+ minClientVersion?: string;
101
+ /** The module that exports `activate`, relative to the package's root. */
102
+ main: string;
103
+ /**
104
+ * Stylesheets adopted into the shadow root of each of the extension's Views, relative to the
105
+ * package's root. Only a loaded extension has them: a compiled-in one shares the Client's.
106
+ */
107
+ styles?: string[];
108
+ /** Icons by name: the inner markup of a 16 by 16 SVG drawn with `currentColor` strokes. */
109
+ icons?: Record<string, string>;
110
+ /** The View kinds the extension adds. */
111
+ views?: ViewDeclaration[];
112
+ /** What the extension asks the person to set, in the order it is shown. */
113
+ settings?: SettingDeclaration[];
114
+ }
115
+ /**
116
+ * An extension's package as the Client reads it: the standard package.json fields it uses and the
117
+ * manifest.
118
+ *
119
+ * @beta
120
+ */
121
+ export interface ExtensionPackage {
122
+ /** The npm package name. */
123
+ name: string;
124
+ /** The package version, which is the extension's version. */
125
+ version: string;
126
+ /** The package's description, when it has one. */
127
+ description?: string;
128
+ /** The package's licence, an SPDX identifier, when it names one. */
129
+ license?: string;
130
+ /** The `etherpk` field. */
131
+ manifest: ExtensionManifest;
132
+ }
133
+ /**
134
+ * The answer of {@link readExtensionPackage}: the package, or every way its manifest is wrong.
135
+ *
136
+ * @beta
137
+ */
138
+ export type ReadExtensionPackageResult = {
139
+ ok: true;
140
+ extension: ExtensionPackage;
141
+ } | {
142
+ ok: false;
143
+ errors: string[];
144
+ };
145
+ /**
146
+ * Read an extension's package.json, already parsed, and check its manifest.
147
+ *
148
+ * Checks what the Client depends on: names it uses as keys, paths it resolves, and declarations
149
+ * it registers before any of the extension's code runs. It does not check that files exist.
150
+ *
151
+ * @beta
152
+ */
153
+ export declare function readExtensionPackage(packageJson: unknown): ReadExtensionPackageResult;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * An extension's manifest: the `etherpk` field of its package.json (ADR 0121), and the one helper
3
+ * that reads and checks it. The Client reads the same field with the same function, so an author
4
+ * and the Client never disagree about what a manifest means.
5
+ */
6
+ const NAME = /^[a-z][a-z0-9-]*$/;
7
+ const VERSION = /^\d+\.\d+\.\d+$/;
8
+ const REGIONS = ['main', 'left-sidebar', 'right-sidebar'];
9
+ /**
10
+ * Read an extension's package.json, already parsed, and check its manifest.
11
+ *
12
+ * Checks what the Client depends on: names it uses as keys, paths it resolves, and declarations
13
+ * it registers before any of the extension's code runs. It does not check that files exist.
14
+ *
15
+ * @beta
16
+ */
17
+ export function readExtensionPackage(packageJson) {
18
+ const errors = [];
19
+ const json = isRecord(packageJson) ? packageJson : {};
20
+ const name = json.name;
21
+ const version = json.version;
22
+ if (typeof name !== 'string' || name === '')
23
+ errors.push('package.json has no "name".');
24
+ if (typeof version !== 'string' || !VERSION.test(version))
25
+ errors.push('package.json\'s "version" is not x.y.z.');
26
+ const field = json.etherpk;
27
+ if (!isRecord(field)) {
28
+ errors.push('package.json has no "etherpk" field, so it is not an EtherPK extension.');
29
+ return { ok: false, errors };
30
+ }
31
+ const id = field.id;
32
+ if (typeof id !== 'string' || !NAME.test(id))
33
+ errors.push('"etherpk.id" must be lowercase letters, digits and hyphens, starting with a letter.');
34
+ const ownId = typeof id === 'string' ? id : '';
35
+ for (const key of ['displayName', 'publisher', 'main']) {
36
+ if (typeof field[key] !== 'string' || field[key].trim() === '')
37
+ errors.push(`"etherpk.${key}" must be a non-empty string.`);
38
+ }
39
+ if (typeof field.main === 'string' && field.main.trim() !== '' && !relativePath(field.main))
40
+ errors.push('"etherpk.main" must be a path inside the package.');
41
+ if (field.minClientVersion !== undefined && (typeof field.minClientVersion !== 'string' || !VERSION.test(field.minClientVersion))) {
42
+ errors.push('"etherpk.minClientVersion" must be x.y.z.');
43
+ }
44
+ if (field.styles !== undefined && (!Array.isArray(field.styles) || !field.styles.every((path) => typeof path === 'string' && relativePath(path)))) {
45
+ errors.push('"etherpk.styles" must be a list of paths inside the package.');
46
+ }
47
+ if (field.icons !== undefined) {
48
+ if (!isRecord(field.icons))
49
+ errors.push('"etherpk.icons" must map names to SVG markup.');
50
+ else
51
+ for (const [iconName, markup] of Object.entries(field.icons)) {
52
+ if (!NAME.test(iconName))
53
+ errors.push(`Icon "${iconName}" must be lowercase letters, digits and hyphens.`);
54
+ if (typeof markup !== 'string' || markup.trim() === '')
55
+ errors.push(`Icon "${iconName}" has no markup.`);
56
+ }
57
+ }
58
+ const views = field.views === undefined ? [] : field.views;
59
+ if (!Array.isArray(views))
60
+ errors.push('"etherpk.views" must be a list.');
61
+ else {
62
+ const kinds = new Set();
63
+ const segments = new Set();
64
+ views.forEach((view, index) => checkView(view, index, ownId, kinds, segments, errors));
65
+ }
66
+ const settings = field.settings === undefined ? [] : field.settings;
67
+ if (!Array.isArray(settings))
68
+ errors.push('"etherpk.settings" must be a list.');
69
+ else {
70
+ const ids = new Set();
71
+ settings.forEach((setting, index) => checkSetting(setting, index, ids, errors));
72
+ }
73
+ if (errors.length > 0)
74
+ return { ok: false, errors };
75
+ return {
76
+ ok: true,
77
+ extension: {
78
+ name: name,
79
+ version: version,
80
+ ...(typeof json.description === 'string' ? { description: json.description } : {}),
81
+ ...(typeof json.license === 'string' ? { license: json.license } : {}),
82
+ manifest: field,
83
+ },
84
+ };
85
+ }
86
+ const SETTING_TYPES = ['text', 'secret', 'boolean'];
87
+ function checkSetting(setting, index, ids, errors) {
88
+ const where = `"etherpk.settings[${index}]"`;
89
+ if (!isRecord(setting)) {
90
+ errors.push(`${where} must be an object.`);
91
+ return;
92
+ }
93
+ if (typeof setting.id !== 'string' || !NAME.test(setting.id))
94
+ errors.push(`${where}.id must be lowercase letters, digits and hyphens, starting with a letter.`);
95
+ else if (ids.has(setting.id))
96
+ errors.push(`${where}.id "${setting.id}" is declared twice.`);
97
+ else
98
+ ids.add(setting.id);
99
+ if (!SETTING_TYPES.includes(setting.type))
100
+ errors.push(`${where}.type must be one of ${SETTING_TYPES.join(', ')}.`);
101
+ if (typeof setting.title !== 'string' || setting.title.trim() === '')
102
+ errors.push(`${where}.title must be a non-empty string.`);
103
+ for (const key of ['description', 'placeholder']) {
104
+ if (setting[key] !== undefined && typeof setting[key] !== 'string')
105
+ errors.push(`${where}.${key} must be a string.`);
106
+ }
107
+ if (setting.default !== undefined && (setting.type !== 'boolean' || typeof setting.default !== 'boolean')) {
108
+ errors.push(`${where}.default must be true or false, and only on a boolean setting.`);
109
+ }
110
+ if (setting.link !== undefined) {
111
+ const link = setting.link;
112
+ if (!isRecord(link) || typeof link.title !== 'string' || link.title.trim() === '' || typeof link.url !== 'string' || !httpsUrl(link.url)) {
113
+ errors.push(`${where}.link must be { title, url }, the url an https address.`);
114
+ }
115
+ }
116
+ }
117
+ function httpsUrl(value) {
118
+ try {
119
+ return new URL(value).protocol === 'https:';
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ function checkView(view, index, id, kinds, segments, errors) {
126
+ const where = `"etherpk.views[${index}]"`;
127
+ if (!isRecord(view)) {
128
+ errors.push(`${where} must be an object.`);
129
+ return;
130
+ }
131
+ const kind = view.kind;
132
+ const prefix = `${id}.`;
133
+ if (typeof kind !== 'string' || !kind.startsWith(prefix) || !NAME.test(kind.slice(prefix.length))) {
134
+ errors.push(`${where}.kind must be "${prefix}<name>", the name in lowercase letters, digits and hyphens.`);
135
+ }
136
+ else if (kinds.has(kind))
137
+ errors.push(`${where}.kind "${kind}" is declared twice.`);
138
+ else
139
+ kinds.add(kind);
140
+ if (view.target !== undefined && (typeof view.target !== 'string' || view.target === ''))
141
+ errors.push(`${where}.target must be a non-empty string.`);
142
+ if (typeof view.title !== 'string' || view.title.trim() === '')
143
+ errors.push(`${where}.title must be a non-empty string.`);
144
+ if (view.icon !== undefined && typeof view.icon !== 'string')
145
+ errors.push(`${where}.icon must be an icon's name.`);
146
+ if (!REGIONS.includes(view.region))
147
+ errors.push(`${where}.region must be one of ${REGIONS.join(', ')}.`);
148
+ if (view.resident !== undefined) {
149
+ const resident = view.resident;
150
+ if (!isRecord(resident) || (resident.side !== 'left' && resident.side !== 'right') || (resident.desktopOnly !== undefined && typeof resident.desktopOnly !== 'boolean')) {
151
+ errors.push(`${where}.resident must be { side: "left" | "right", desktopOnly?: boolean }.`);
152
+ }
153
+ }
154
+ if (view.address !== undefined) {
155
+ const address = view.address;
156
+ if (!isRecord(address) || typeof address.segment !== 'string' || !NAME.test(address.segment)) {
157
+ errors.push(`${where}.address.segment must be lowercase letters, digits and hyphens.`);
158
+ }
159
+ else if (segments.has(address.segment))
160
+ errors.push(`${where}.address.segment "${address.segment}" is declared twice.`);
161
+ else
162
+ segments.add(address.segment);
163
+ if (isRecord(address) && address.target !== undefined && address.target !== 'concept')
164
+ errors.push(`${where}.address.target may only be "concept".`);
165
+ }
166
+ }
167
+ /** A path that stays inside the package: relative, and never climbing out of it. */
168
+ function relativePath(path) {
169
+ return path !== '' && !path.startsWith('/') && !path.includes('\\') && !path.split('/').includes('..');
170
+ }
171
+ function isRecord(value) {
172
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
173
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Rows an extension adds to the Client's menus. A row runs a Command, so every row is reachable
3
+ * from code, keybindings and other menus as well.
4
+ */
5
+ /**
6
+ * A document, reached from its tab, a Favourite, Recents or an All Documents row.
7
+ *
8
+ * @beta
9
+ */
10
+ export interface DocumentMenuTarget {
11
+ /** Where the menu was raised: the document's tab, or a row naming it. */
12
+ kind: 'document-tab' | 'favourite' | 'recent' | 'document-row';
13
+ /** The concept the document is about. */
14
+ concept: string;
15
+ /** The panel it is open in, when the target is a tab. */
16
+ panelId?: string;
17
+ /** The tab is a row in a vertical list, as a phone draws its open documents. */
18
+ vertical?: boolean;
19
+ }
20
+ /**
21
+ * A tab that is not a document's.
22
+ *
23
+ * @beta
24
+ */
25
+ export interface TabMenuTarget {
26
+ /** Any tab that is not a document's. */
27
+ kind: 'tab';
28
+ /** The panel the tab belongs to. */
29
+ panelId: string;
30
+ /** The tab is a row in a vertical list, as a phone draws its open documents. */
31
+ vertical?: boolean;
32
+ }
33
+ /**
34
+ * A wikilink in an editor.
35
+ *
36
+ * @beta
37
+ */
38
+ export interface WikilinkMenuTarget {
39
+ /** A wikilink in an editor. */
40
+ kind: 'wikilink';
41
+ /** The concept the link names. */
42
+ concept: string;
43
+ /** The editor's panel. */
44
+ panelId?: string;
45
+ }
46
+ /**
47
+ * A thing an extension raises a menu on, of a kind the extension names under its own id
48
+ * (`kanban.card`). Only rows that check for that kind apply to it.
49
+ *
50
+ * @beta
51
+ */
52
+ export interface ExtensionMenuTarget {
53
+ /** `<extension id>.<name>`. */
54
+ kind: `${string}.${string}`;
55
+ /** Whatever the extension's own rows need to know about the thing. */
56
+ [field: string]: unknown;
57
+ }
58
+ /**
59
+ * What a Context Menu can be raised on that an extension may add rows to. The Client also raises
60
+ * menus on things this list does not name, an Asset or a misspelt word, so a row's `when` answers
61
+ * false for any kind it does not check for.
62
+ *
63
+ * @beta
64
+ */
65
+ export type MenuTarget = DocumentMenuTarget | TabMenuTarget | WikilinkMenuTarget | ExtensionMenuTarget;
66
+ /**
67
+ * One Context Menu row.
68
+ *
69
+ * @beta
70
+ */
71
+ export interface ContextMenuItem {
72
+ /** Under the extension's id: `graph-view.showConcept`. */
73
+ id: string;
74
+ /** The row's text, which may read the target. */
75
+ label: string | ((target: MenuTarget) => string);
76
+ /** Lower sorts earlier. Rows with equal values keep the order they were added in. */
77
+ order?: number;
78
+ /** The Command the row runs, given the target. */
79
+ command: string;
80
+ /** An icon drawn left of the label: the extension's own or one of the Client's. */
81
+ icon?: string;
82
+ /** A separator above the row. */
83
+ separatorBefore?: boolean | ((target: MenuTarget) => boolean);
84
+ /** Whether the row applies to a target. Absent means every target. */
85
+ when?: (target: MenuTarget) => boolean;
86
+ }
87
+ /**
88
+ * What a Command Menu row's `when` may read about the editor it was opened in.
89
+ *
90
+ * @beta
91
+ */
92
+ export interface CommandMenuContext {
93
+ /** The caret is in a table. */
94
+ inTable: boolean;
95
+ /** A table may be inserted at the caret. */
96
+ tableInsertable: boolean;
97
+ /** The document's body may be written: false in a locked Protected Document. */
98
+ bodyWritable: boolean;
99
+ /**
100
+ * The document is a Protected Document, locked or not: where a row whose widget draws no
101
+ * protected content, such as `/map`, is not offered. Absent reads as false.
102
+ */
103
+ protectedDocument?: boolean;
104
+ /**
105
+ * The concepts a View about one concept can be opened for from the caret, nearest first: the
106
+ * caret line's links, the links above it in the outline, the document's own concept, then the
107
+ * scopes in its name. Names are as written, an alias as the alias. Worked out only when a row
108
+ * reads it. Absent reads as none.
109
+ */
110
+ readonly conceptsAtCaret?: readonly string[];
111
+ }
112
+ /**
113
+ * One row of the Command Menu, the menu `/` opens in the editor.
114
+ *
115
+ * @beta
116
+ */
117
+ export interface CommandMenuItem {
118
+ /** Under the extension's id. */
119
+ id: string;
120
+ /** The row's text. */
121
+ title: string;
122
+ /**
123
+ * A line under the title, or a function that works it out from the editor the menu opened in,
124
+ * each time the menu opens. A function that throws shows no line.
125
+ */
126
+ detail?: string | ((context: CommandMenuContext) => string | undefined);
127
+ /** An icon drawn before the title: the extension's own or one of the Client's. */
128
+ icon?: string;
129
+ /** Rows are grouped under this heading. */
130
+ group?: string;
131
+ /** Lower sorts earlier within the group. */
132
+ order?: number;
133
+ /** More words the menu's filter matches. */
134
+ keywords?: string[];
135
+ /** Whether the row is offered here. Absent means everywhere. */
136
+ when?: (context: CommandMenuContext) => boolean;
137
+ /** The Command the row runs. */
138
+ command: string;
139
+ /** What the Command is given. */
140
+ args?: unknown;
141
+ }
142
+ /**
143
+ * A button in the Graph Sidebar, under Today's journal: a way in to what the extension shows for
144
+ * the whole graph, such as the Graph Map View. Pressing it runs `command`.
145
+ *
146
+ * @beta
147
+ */
148
+ export interface GraphSidebarButton {
149
+ /** Under the extension's id. */
150
+ id: string;
151
+ /** The button's text. */
152
+ title: string;
153
+ /** An icon drawn before the title: the extension's own or one of the Client's. */
154
+ icon?: string;
155
+ /** Lower lists earlier. A button without one lists after every button that has one. */
156
+ order?: number;
157
+ /** The Command the button runs. */
158
+ command: string;
159
+ /** What the Command is given. */
160
+ args?: unknown;
161
+ }
package/dist/menus.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Rows an extension adds to the Client's menus. A row runs a Command, so every row is reachable
3
+ * from code, keybindings and other menus as well.
4
+ */
5
+ export {};
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The contract an extension's View follows (ADR 0121): a function that draws into an element the
3
+ * Client gives it, tied to no framework, so no Svelte or React version is ever part of the API.
4
+ */
5
+ /**
6
+ * Which View: its kind and what it is about. A View with no subject has an empty target.
7
+ *
8
+ * @beta
9
+ */
10
+ export interface ViewRef {
11
+ /** The View kind, `<extension id>.<name>` for an extension's. */
12
+ kind: string;
13
+ /** What the View is about: a concept's name for a board or a map, empty for a View with none. */
14
+ target: string;
15
+ }
16
+ /**
17
+ * Whether a View can be seen: its tab in front, its Pane given room, and the browser tab showing.
18
+ * The desktop keeps every tab mounted, so a View that does work while shown (drawing, reading the
19
+ * index) stops it while this is false.
20
+ *
21
+ * @beta
22
+ */
23
+ export interface ViewVisibility {
24
+ /** True while the View can be seen. */
25
+ readonly onScreen: boolean;
26
+ /** Called with each new value of `onScreen`, never with a repeat. Returns the unsubscribe. */
27
+ subscribe(listener: (onScreen: boolean) => void): () => void;
28
+ }
29
+ /**
30
+ * What a View is mounted with.
31
+ *
32
+ * @beta
33
+ */
34
+ export interface ViewMountProps {
35
+ /** The View being mounted. */
36
+ view: ViewRef;
37
+ /**
38
+ * The id of the panel the View is open in, on a desktop: what a Layout call names to open
39
+ * something in the same Pane or beside it. A phone passes none.
40
+ */
41
+ panelId?: string;
42
+ /** Whether the View can be seen right now, and when that changes. */
43
+ visibility: ViewVisibility;
44
+ }
45
+ /**
46
+ * A View the extension has drawn, as the Client drives it.
47
+ *
48
+ * @beta
49
+ */
50
+ export interface MountedView {
51
+ /** The same View with new props: a rename re-keyed its target, or it moved to another Pane. */
52
+ update?(props: ViewMountProps): void;
53
+ /** The tab closed, the graph closed, or the extension was switched off. */
54
+ destroy(): void;
55
+ }
56
+ /**
57
+ * What an extension registers for a View kind its manifest declares.
58
+ *
59
+ * @beta
60
+ */
61
+ export interface ViewContribution {
62
+ /**
63
+ * Draw the View into `element`, which is empty and is the extension's alone. For a loaded
64
+ * extension it sits inside a shadow root that holds the extension's own stylesheets, so its CSS
65
+ * reaches nothing outside, and nothing of the Client's reaches in but the `--gk-*` tokens and
66
+ * inherited text styles.
67
+ */
68
+ mount(element: HTMLElement, props: ViewMountProps): MountedView;
69
+ }
package/dist/views.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The contract an extension's View follows (ADR 0121): a function that draws into an element the
3
+ * Client gives it, tied to no framework, so no Svelte or React version is ever part of the API.
4
+ */
5
+ export {};
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@appsoftwareltd/etherpk-extension-api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.9.6",
4
+ "license": "MIT",
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
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/appsoftwareltd/etherpk-client.git",
25
+ "directory": "packages/extension-api"
26
+ },
27
+ "devDependencies": {
28
+ "@microsoft/api-extractor": "^7.59.2",
29
+ "@vitest/coverage-v8": "^4.1.4",
30
+ "typescript": "^6.0.2",
31
+ "vitest": "^4.1.4"
32
+ },
33
+ "scripts": {
34
+ "check": "tsc --noEmit && pnpm api:check",
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "api": "pnpm build && api-extractor run --local",
37
+ "api:check": "pnpm build && api-extractor run",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "test:coverage": "vitest run --coverage"
41
+ }
6
42
  }