@appsoftwareltd/etherpk-extension-api 0.13.0 → 0.14.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
@@ -54,7 +54,8 @@ documentation says which, with `@public` for Stable and `@beta` for Proposed.
54
54
  it. In an extension installed from the Extension Directory, a call to a Proposed member throws an
55
55
  error that names it.
56
56
 
57
- An extension listed in the Extension Directory uses only the Stable part.
57
+ Since 0.14.0 every part is Stable but `contributions` (see *Proposed parts* below). An extension
58
+ listed in the Extension Directory uses only the Stable part.
58
59
  [`etc/etherpk-extension-api.public.api.md`](etc/etherpk-extension-api.public.api.md) lists the
59
60
  Stable part, and [`etc/etherpk-extension-api.beta.api.md`](etc/etherpk-extension-api.beta.api.md)
60
61
  lists everything. A Proposed part becomes Stable when an extension from another publisher needs it.
@@ -91,6 +92,18 @@ A row can have a `detail` line, an `icon`, `keywords` that find it, a `group` an
91
92
  `when(menu)` says whether it is offered at the caret, from what `menu` reports: `bodyWritable`,
92
93
  `inTable` and `protectedDocument`.
93
94
 
95
+ `context.commands.execute(id, arg)` runs a Command, and `context.commands.has(id)` says whether there
96
+ is one. An extension may run three kinds of Command, by `execute` or as the Command of a row or a
97
+ button it adds:
98
+
99
+ - Its own.
100
+ - Another running extension's, by its id (`kanban.open`).
101
+ - One of the Client's that `CLIENT_COMMANDS` names. Each keeps its id and argument in every release.
102
+
103
+ The Client's other Commands may change, so `execute` rejects for them, and a row or button naming one
104
+ is refused. `CLIENT_COMMANDS` holds `agents.settings` (open Settings at the Agents tab), and more are
105
+ added as extensions need them.
106
+
94
107
  ### Notices
95
108
 
96
109
  `context.notify(text)` shows a short notice, as the Client's own notices do.
@@ -212,6 +225,13 @@ keep the buttons readable together:
212
225
 
213
226
  - `context.layout.openDocument(concept)` opens a document as following a wikilink does. A day's
214
227
  name, `YYYY-MM-DD`, opens its journal entry.
228
+ - `context.layout.activeDocument()` names the document the person is working in, as its tab names
229
+ it, or null.
230
+ - `context.layout.openWindow({ title, mount })` draws something over the whole window, with a heading
231
+ and a close button the Client draws. One is open at a time: opening another closes it, even
232
+ another extension's.
233
+ - `context.layout.reveal(kind)` brings one of the extension's own Sidebar Views to the front (see
234
+ *Views* below). It does nothing for any other kind.
215
235
  - `context.theme.isDark()` says whether the dark theme shows, and the `theme:changed` Event reports a
216
236
  switch. EtherPK's `--gk-*` colour tokens reach into a shadow root and change with the theme by
217
237
  themselves.
@@ -272,6 +292,91 @@ The third argument to `registerFence` adds options:
272
292
  goes in. It returns false where nothing went in, such as a document that cannot be written, a
273
293
  Protected Document, or a caret in the frontmatter, in a fence or in a table.
274
294
 
295
+ `context.editor.openPanel({ mount, at })` draws a panel of the extension's own in the document,
296
+ beneath the caret's line, or beneath the document's last line with `at: 'end'`, without changing
297
+ the text. It returns null where no panel can go, by the same rules as `insertBlock`. A panel closes
298
+ when the extension stops.
299
+
300
+ ### Context Menu rows and Command Bar buttons
301
+
302
+ `context.contextMenu.register({ id, label, command, when })` adds a row to the menu raised on a
303
+ document's tab, a Favourite, a Recent, an All Documents row, any other tab, or a wikilink. The row's
304
+ Command is given the target, and its `when(target)` says whether it applies. More kinds of target may
305
+ be added, and when the Client stops raising a menu on one, the rows for it are no longer shown, so a
306
+ `when` answers false for any kind it does not check. A row that deletes or removes something for good
307
+ sets `danger: true`, and is drawn in the danger colour.
308
+
309
+ `context.commandBar.register({ id, title, icon, command, pinned })` adds a button to the row of
310
+ buttons across the bottom of a phone's screen: after the editing buttons that scroll, or with
311
+ `pinned`, at the bar's right end, where it never scrolls away.
312
+
313
+ ### Key chords
314
+
315
+ `context.keybindings.register({ key, command, label })` binds a chord such as `Alt+M` to one of the
316
+ extension's own Commands. `Mod` is Cmd on Apple devices and Ctrl elsewhere. When the Client or another
317
+ extension binds the chord already, the chord stays theirs: the call still succeeds, the binding is
318
+ left out, and the browser's console says so. So a shortcut the Client adds in a later release never
319
+ stops an extension from starting.
320
+
321
+ ### The graph's index and the names of concepts
322
+
323
+ - `context.index.allConcepts()` lists the graph's concepts, `context.index.linkGraph()` gives every
324
+ concept and the links between them, and `context.index.onUpdated(listener)` hears each change to
325
+ the index.
326
+ - `context.concepts.canonicalName(name)` gives the name a concept resolves to: an alias's page, a
327
+ page's own title, or the name itself. `context.concepts.key(name)` gives the key the Client tells
328
+ two names apart by. The way the key is made may change, so compare keys from `key`, and never make
329
+ one another way.
330
+
331
+ ### Reading documents
332
+
333
+ `context.documents` reads the graph's documents for the person, for example for a language model they
334
+ use: `list`, `read`, `links`, `search`, `backlinks` and `tasks`. Every answer leaves out each Protected
335
+ Document, its name included, and a document holding an encrypted fence. Answers are bounded. A page
336
+ of a listing holds at most 200 documents or tasks, and a page of a search 50 results, and a
337
+ document's text is cut after 200,000 characters, saying so. `formatGuide()` describes how the
338
+ graph's documents are written, in the words the Client gives agents. Its wording changes as the
339
+ format does, so pass it on rather than read meaning into it.
340
+
341
+ ### Records for one graph
342
+
343
+ `context.records` keeps the extension's records for the open graph on this device alone, such as a
344
+ chat the person has not saved: `get`, `set`, `remove` and `keys`. They are never synced, and they go
345
+ when the graph's data leaves the device. A record may hold a `Blob`, an `ArrayBuffer`, a `Date` or
346
+ plain data, at most 4 MB of it, and a larger one is refused. A write the browser refuses, such as on
347
+ a full disk, rejects.
348
+
349
+ ### Images as assets
350
+
351
+ `context.assets.prepareImage(blob)` makes an image smaller, as a pasted image is.
352
+ `context.assets.saveImage(blob, name)` keeps it as one of the graph's assets and answers the markdown
353
+ that shows it, such as `![photo](assets/photo.3f9a1c2e.webp)`. The file's name ends in a hash of its
354
+ bytes, so two images never take the same name, and the same image kept twice is one file. An asset
355
+ is part of the graph: it syncs, and every member of a shared graph can see it.
356
+
357
+ ### Icons
358
+
359
+ `context.icons.svg(name)` draws one of the extension's own icons or one of the Client's, as markup the
360
+ Client trusts. The Client's icons, which every `icon` field may name too, are `alias`, `backlinks`, `bold`, `bullet`, `calendar`, `check`, `clock`, `close`, `code`, `copy`, `download`, `fit`, `flag`, `fold`, `frontmatter`, `globe`, `graph`, `grip`, `headless-client`, `highlight`, `image`, `import`, `indent`, `italic`, `list`, `lock`, `lock-open`, `more`, `move`, `move-down`, `move-up`, `new-page`, `open-external`, `outdent`, `page`, `pageless`, `pin`, `redo`, `rename`, `reset`, `search`, `slash`, `spell-check`, `table`, `table-add-column`, `table-add-row`, `table-format`, `table-remove-column`, `table-remove-row`, `task`, `template`, `text-lines`, `today`, `todays-journal`, `trash`, `undo`, `wikilink-close`, `wikilink-open`, `zoom-in`, `zoom-out`. An
361
+ icon may be added in a later release, never taken away. An unknown name draws a small dot.
362
+
363
+ ### Settings sections
364
+
365
+ The main module can export `settingsSections`, for settings that need more than the fields a
366
+ manifest declares, such as a list of accounts with a button that connects one. Each section has an
367
+ `id`, a `tab` and a `mount(element, context)` that draws it. The context holds the extension's
368
+ settings, its records kept with the person, signing in, the theme and `notify`, and nothing from a
369
+ graph.
370
+
371
+ ```ts
372
+ export const settingsSections: SettingsSection[] = [{ id: 'hello.accounts', tab: 'extension', mount: drawAccounts }]
373
+ ```
374
+
375
+ `tab: 'extension'` draws the section under the extension's own entry in Settings and Extensions,
376
+ beside its declared settings, and `tab: 'agents'` in the Agents tab. The Client draws an extension's
377
+ sections in a graph's settings while the extension runs there. An extension's code is loaded only
378
+ once a graph opens, so its sections are not drawn on the Graphs page.
379
+
275
380
  ### Dialogs
276
381
 
277
382
  `context.dialogs.prompt(options)` asks for one line of text in a dialog the Client draws. Its
@@ -313,6 +418,10 @@ and offers to install it, if the directory lists it.
313
418
  `context.layout.openView({ kind, target })` opens a View. `context.layout.isDesktop()` says whether
314
419
  the desktop layout is showing, which it is at 1024 pixels wide and more.
315
420
 
421
+ A View kind can live in a Sidebar from the moment a graph opens, beside the Client's own Sidebar
422
+ Views, with `"resident": { "side": "left" }` (or `"right"`, and `"desktopOnly": true` for the desktop
423
+ alone). `context.layout.reveal(kind)` brings it to the front of its Sidebar.
424
+
316
425
  ## Styles
317
426
 
318
427
  The manifest's `styles` are the extension's stylesheets. By default, each widget and View the
@@ -329,6 +438,54 @@ An extension's stylesheet can't register one of them with `@property`.
329
438
  Either way, the shadow root and the check keep styles apart, and nothing more. They don't limit
330
439
  what the extension's code can do in the page.
331
440
 
441
+ ## Controls
442
+
443
+ The Client draws its own buttons, fields, links and menus with a set of classes, and gives the same
444
+ stylesheet to every shadow root an extension draws in, ahead of the extension's own stylesheets. An
445
+ extension mounted in the page has it from the Client's own stylesheet. So an element with one of
446
+ these classes looks like the Client's control, in the light and dark themes alike, and keeps
447
+ looking like it as the Client changes. The classes are Stable, and the Client has them from
448
+ EtherPK 0.14.0. The stylesheet is in this package as `controls.css`, for reference and for an
449
+ extension's own tests.
450
+
451
+ | Class | Use it on |
452
+ | --- | --- |
453
+ | `gk-button` | A `<button>` or `<a>`, as the Client's everyday button: a thin border, filled faintly on hover. |
454
+ | `gk-button--primary` | With `gk-button`, the one main action of a form or dialog. |
455
+ | `gk-button--danger` | With `gk-button`, an action that removes or ends something. |
456
+ | `gk-button--quiet` | With `gk-button`, a button with no border, in a toolbar or beside other text. |
457
+ | `gk-button--small` | With `gk-button`, a button on the line of a heading or a row, 24px tall. |
458
+ | `gk-button--raised` | With `gk-button` or `gk-icon-button`, a button over content such as a map, a picture or code: a solid surface and a small shadow. |
459
+ | `gk-icon-button` | A square button holding only an icon. Give it an `aria-label`. |
460
+ | `gk-link` | A `<button>` or `<a>` that reads as a link: the accent colour, underlined. |
461
+ | `gk-field` | An element holding a field's label, its control and any hint or error, stacked. |
462
+ | `gk-label` | A field's `<label>`. |
463
+ | `gk-hint` | Help text under a field. |
464
+ | `gk-error` | An error under a field. Set `aria-invalid="true"` on the control too. |
465
+ | `gk-input` | An `<input>` or `<textarea>`, the full width of its column. |
466
+ | `gk-select` | A `<select>`. Where the browser can, it draws the option list in the menu's colours, with `<optgroup>` labels and a tick on the chosen option. Elsewhere it shows the browser's own list. |
467
+ | `gk-checkbox` | An `<input type="checkbox">`. |
468
+ | `gk-menu` | A list of actions, such as a menu a button opens. |
469
+ | `gk-menu-item` | A row of a `gk-menu`. |
470
+ | `gk-menu-item--danger` | With `gk-menu-item`, a row that removes something. |
471
+ | `gk-menu-separator` | A rule between groups of rows in a `gk-menu`. |
472
+
473
+ A disabled control (`disabled`, or `aria-disabled="true"`) recedes by colour. A toggle button with
474
+ `aria-pressed="true"` is drawn as the primary button while it is on. On a touch screen buttons,
475
+ fields and menu rows are at least 44px tall.
476
+
477
+ ```ts
478
+ const save = document.createElement('button')
479
+ save.className = 'gk-button gk-button--primary'
480
+ save.textContent = 'Save'
481
+ ```
482
+
483
+ The rules are in the `components` cascade layer, after the layer order Tailwind v4 uses
484
+ (`properties`, `theme`, `base`, `components`, `utilities`). So Tailwind's utilities can adjust a control
485
+ (`gk-button w-full`), and Tailwind's preflight doesn't undo one. A rule of yours outside any layer
486
+ wins over every layered rule, so keep element resets such as `button { color: inherit }` in
487
+ `@layer base`, or they will override the controls.
488
+
332
489
  ## Writing to the page
333
490
 
334
491
  The Client enforces
@@ -345,24 +502,19 @@ code runs under them.
345
502
 
346
503
  ## Proposed parts
347
504
 
348
- The Proposed parts are for the extensions that come with EtherPK, and may change in any release.
349
- They include:
350
-
351
- - Context Menu rows (`contextMenu`) and key chords (`keybindings`).
352
- - Questions to the graph's index (`index`) and the rules for naming a concept (`concepts`).
353
- - The Client's icons (`icons`) and a View that lives in a Sidebar (`resident`).
354
- - Running another extension's Commands (`commands.execute`), and the document the person is working
355
- in (`layout.activeDocument`).
356
-
357
- `contributions`, which reaches the Client's own registries, stays Proposed for good, since its
358
- shape is the Client's internals.
505
+ The one Proposed part is `contributions`, which reaches the Client's own registries by kind. Only
506
+ the extensions that come with EtherPK may use it, and it stays Proposed for good: its kinds and
507
+ their values are the Client's internals, which change between releases, and through it one
508
+ extension could take another's entries. Where an extension from another Publisher needs what a
509
+ registry gives, the Client adds a checked part for it, as it did for fences and Journal Panels.
359
510
 
360
511
  ## Trying an extension
361
512
 
362
513
  To try an extension, build it, and start a development copy of the EtherPK Client with the
363
514
  extension's folder named in `ETHERPK_DEV_EXTENSIONS`. Separate several folders with commas. The
364
515
  Client loads each one as it loads the extensions it comes with, and one with the same id replaces
365
- it.
516
+ it. An extension loaded this way gets what a Directory release gets, the Stable part alone, unless it
517
+ stands in for one that comes with EtherPK, so a Proposed part it calls fails while you test.
366
518
 
367
519
  ```bash
368
520
  ETHERPK_DEV_EXTENSIONS=/path/to/my-extension pnpm dev
package/controls.css ADDED
@@ -0,0 +1,391 @@
1
+ /*
2
+ * EtherPK's controls: the buttons, fields, links and menus the Client draws, as classes an
3
+ * extension can use (ADR 0151). The Client supplies this stylesheet to every shadow root an
4
+ * extension draws in, ahead of the extension's own stylesheets, and its own pages import it, so a
5
+ * `gk-button` in an extension is the Client's own button, in the light and dark themes alike.
6
+ *
7
+ * Every colour is one of the Client's `--gk-*` tokens, and no text is under 14px. The class names
8
+ * are part of the Stable API: none is renamed or removed.
9
+ *
10
+ * The rules sit in the `components` layer, after declaring the layer order Tailwind v4 uses, so
11
+ * in a shadow root that also holds a Tailwind stylesheet the layers keep Tailwind's order:
12
+ * Tailwind's preflight cannot undo a control, and a utility class can still adjust one
13
+ * (`gk-button w-full`). A rule outside any layer wins over all of them, so an extension's own
14
+ * element resets belong in `@layer base`.
15
+ */
16
+ @layer properties, theme, base, components, utilities;
17
+
18
+ @layer components {
19
+ /* ── Buttons ─────────────────────────────────────────────────────────────── */
20
+
21
+ /* The everyday button: a thin border, in the control ink, filled faintly on hover. */
22
+ .gk-button {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ gap: 0.375rem;
27
+ padding: 0.375rem 0.75rem;
28
+ border: 1px solid var(--gk-control-border);
29
+ border-radius: 0.5rem;
30
+ background: transparent;
31
+ color: var(--gk-control-text);
32
+ font-family: inherit;
33
+ font-size: 0.875rem;
34
+ font-weight: 500;
35
+ line-height: 1.25rem;
36
+ text-align: center;
37
+ text-decoration: none;
38
+ white-space: nowrap;
39
+ cursor: pointer;
40
+ }
41
+ .gk-button:hover {
42
+ background: var(--gk-control-hover);
43
+ }
44
+ .gk-button:focus-visible,
45
+ .gk-icon-button:focus-visible,
46
+ .gk-link:focus-visible {
47
+ outline: 2px solid var(--gk-accent);
48
+ outline-offset: 2px;
49
+ }
50
+ /* A button that does nothing yet keeps its shape, and recedes by colour, never by opacity. */
51
+ .gk-button:disabled,
52
+ .gk-button[aria-disabled='true'] {
53
+ background: transparent;
54
+ color: var(--gk-text-subtle);
55
+ cursor: not-allowed;
56
+ }
57
+
58
+ /* The one main action of a form or dialog: filled in the darkest ink (the lightest, in dark). */
59
+ .gk-button--primary {
60
+ border-color: var(--gk-primary-bg);
61
+ background: var(--gk-primary-bg);
62
+ color: var(--gk-primary-text);
63
+ }
64
+ .gk-button--primary:hover {
65
+ border-color: var(--gk-primary-hover);
66
+ background: var(--gk-primary-hover);
67
+ }
68
+ .gk-button--primary:disabled,
69
+ .gk-button--primary[aria-disabled='true'] {
70
+ border-color: var(--gk-surface-2);
71
+ background: var(--gk-surface-2);
72
+ color: var(--gk-text-muted);
73
+ }
74
+
75
+ /* An action that removes or ends something. */
76
+ .gk-button--danger {
77
+ border-color: var(--gk-danger-border);
78
+ color: var(--gk-text-danger);
79
+ }
80
+ .gk-button--danger:hover {
81
+ background: var(--gk-danger-hover);
82
+ }
83
+
84
+ /* A button with no border, in a toolbar or beside other text. */
85
+ .gk-button--quiet {
86
+ padding: 0.25rem 0.5rem;
87
+ border-color: transparent;
88
+ border-radius: 0.375rem;
89
+ color: var(--gk-text-muted);
90
+ }
91
+ .gk-button--quiet:hover {
92
+ background: var(--gk-menu-hover);
93
+ color: var(--gk-text-strong);
94
+ }
95
+
96
+ /* A button on the line of a heading or a row: 24px tall. */
97
+ .gk-button--small {
98
+ padding: 0.0625rem 0.5rem;
99
+ border-radius: 0.375rem;
100
+ }
101
+
102
+ /* A button or icon button over content, such as a map, a picture or code: a solid surface and
103
+ a small shadow, so what is under it never shows through. The hover tint is laid over the
104
+ surface, since it is translucent. */
105
+ .gk-button--raised,
106
+ .gk-button--raised:disabled,
107
+ .gk-button--raised[aria-disabled='true'] {
108
+ border: 1px solid var(--gk-control-border);
109
+ background: var(--gk-surface-0);
110
+ box-shadow: var(--gk-control-shadow);
111
+ }
112
+ .gk-button--raised:hover {
113
+ background: linear-gradient(var(--gk-control-hover), var(--gk-control-hover)), var(--gk-surface-0);
114
+ }
115
+ .gk-icon-button.gk-button--raised:hover {
116
+ background: linear-gradient(var(--gk-menu-hover), var(--gk-menu-hover)), var(--gk-surface-0);
117
+ }
118
+
119
+ /* A toggle that is on (`aria-pressed="true"`) is drawn as the primary button, as the Client's
120
+ own toggles are. Raised or not: they come after `gk-button--raised`, and outweigh it. */
121
+ .gk-button[aria-pressed='true'],
122
+ .gk-icon-button[aria-pressed='true'] {
123
+ border-color: var(--gk-primary-bg);
124
+ background: var(--gk-primary-bg);
125
+ color: var(--gk-primary-text);
126
+ }
127
+ .gk-button[aria-pressed='true']:hover,
128
+ .gk-icon-button[aria-pressed='true']:hover {
129
+ border-color: var(--gk-primary-hover);
130
+ background: var(--gk-primary-hover);
131
+ }
132
+
133
+ /* A square button holding only an icon. Give it an accessible name (`aria-label`). */
134
+ .gk-icon-button {
135
+ display: inline-flex;
136
+ flex: none;
137
+ align-items: center;
138
+ justify-content: center;
139
+ width: 2rem;
140
+ height: 2rem;
141
+ padding: 0;
142
+ border: 0;
143
+ border-radius: 0.5rem;
144
+ background: transparent;
145
+ color: var(--gk-text-muted);
146
+ cursor: pointer;
147
+ }
148
+ .gk-icon-button:hover {
149
+ background: var(--gk-menu-hover);
150
+ color: var(--gk-text-strong);
151
+ }
152
+ .gk-icon-button:disabled,
153
+ .gk-icon-button[aria-disabled='true'] {
154
+ background: transparent;
155
+ color: var(--gk-text-subtle);
156
+ cursor: not-allowed;
157
+ }
158
+
159
+ /* A button or a link that reads as a link: the accent, underlined, so a link inside a sentence
160
+ is told from the text around it by more than its colour. */
161
+ .gk-link {
162
+ display: inline;
163
+ padding: 0;
164
+ border: 0;
165
+ border-radius: 0.125rem;
166
+ background: none;
167
+ color: var(--gk-accent);
168
+ font: inherit;
169
+ font-weight: 500;
170
+ text-decoration: underline;
171
+ text-underline-offset: 2px;
172
+ cursor: pointer;
173
+ }
174
+ .gk-link:hover {
175
+ color: var(--gk-accent-hover);
176
+ }
177
+
178
+ /* ── Fields ──────────────────────────────────────────────────────────────── */
179
+
180
+ /* A field's name, then its control, then any hint or error, stacked. */
181
+ .gk-field {
182
+ display: grid;
183
+ gap: 0.375rem;
184
+ }
185
+ .gk-label {
186
+ color: var(--gk-text-strong);
187
+ font-size: 0.875rem;
188
+ font-weight: 500;
189
+ line-height: 1.25rem;
190
+ }
191
+ .gk-hint {
192
+ color: var(--gk-text-muted);
193
+ font-size: 0.875rem;
194
+ line-height: 1.25rem;
195
+ }
196
+ .gk-error {
197
+ color: var(--gk-text-danger);
198
+ font-size: 0.875rem;
199
+ line-height: 1.25rem;
200
+ }
201
+
202
+ /* A text field or text area, the full width of its column. */
203
+ .gk-input,
204
+ .gk-select {
205
+ min-width: 0;
206
+ max-width: 100%;
207
+ padding: 0.5rem 0.75rem;
208
+ border: 1px solid var(--gk-control-border);
209
+ border-radius: 0.5rem;
210
+ color: var(--gk-field-text);
211
+ font-family: inherit;
212
+ font-size: 0.875rem;
213
+ line-height: 1.25rem;
214
+ }
215
+ .gk-input {
216
+ display: block;
217
+ width: 100%;
218
+ background: var(--gk-field-bg);
219
+ }
220
+ textarea.gk-input {
221
+ resize: vertical;
222
+ }
223
+ .gk-input::placeholder {
224
+ color: var(--gk-text-muted);
225
+ opacity: 1;
226
+ }
227
+ /* A select's background is solid: the browser's own option list takes the select's colours. */
228
+ .gk-select {
229
+ background-color: var(--gk-menu-bg);
230
+ }
231
+ .gk-input:focus,
232
+ .gk-select:focus {
233
+ border-color: var(--gk-field-focus);
234
+ outline: none;
235
+ box-shadow: 0 0 0 1px var(--gk-field-focus);
236
+ }
237
+ .gk-input[aria-invalid='true'],
238
+ .gk-select[aria-invalid='true'] {
239
+ border-color: var(--gk-text-danger);
240
+ }
241
+ .gk-input:disabled,
242
+ .gk-select:disabled {
243
+ background: var(--gk-surface-1);
244
+ color: var(--gk-text-muted);
245
+ cursor: not-allowed;
246
+ }
247
+
248
+ .gk-checkbox {
249
+ flex: none;
250
+ width: 1rem;
251
+ height: 1rem;
252
+ margin: 0;
253
+ accent-color: var(--gk-accent);
254
+ cursor: pointer;
255
+ }
256
+
257
+ /* ── Menus ───────────────────────────────────────────────────────────────── */
258
+
259
+ /* A list of actions, as the Client's context menu draws one. Rows are `gk-menu-item`. */
260
+ .gk-menu {
261
+ min-width: 12rem;
262
+ padding: 0.25rem 0;
263
+ border: 1px solid var(--gk-menu-border);
264
+ border-radius: 0.5rem;
265
+ background: var(--gk-menu-bg);
266
+ color: var(--gk-control-text);
267
+ box-shadow: var(--gk-menu-shadow);
268
+ }
269
+ .gk-menu-item {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 0.5rem;
273
+ width: 100%;
274
+ padding: 0.375rem 0.75rem;
275
+ border: 0;
276
+ background: transparent;
277
+ color: var(--gk-control-text);
278
+ font-family: inherit;
279
+ font-size: 0.875rem;
280
+ line-height: 1.25rem;
281
+ text-align: left;
282
+ text-decoration: none;
283
+ cursor: pointer;
284
+ }
285
+ .gk-menu-item:hover,
286
+ .gk-menu-item:focus-visible {
287
+ background: var(--gk-menu-hover);
288
+ outline: none;
289
+ }
290
+ .gk-menu-item--danger {
291
+ color: var(--gk-text-danger);
292
+ }
293
+ .gk-menu-separator {
294
+ height: 1px;
295
+ margin: 0.25rem 0;
296
+ border: 0;
297
+ background: var(--gk-menu-border);
298
+ }
299
+
300
+ /* A touch screen gets rows, buttons and fields a finger can hit. */
301
+ @media (pointer: coarse) {
302
+ .gk-button,
303
+ .gk-menu-item,
304
+ .gk-input,
305
+ .gk-select {
306
+ min-height: 2.75rem;
307
+ }
308
+ .gk-button--small {
309
+ min-height: 2.25rem;
310
+ }
311
+ .gk-icon-button {
312
+ width: 2.75rem;
313
+ height: 2.75rem;
314
+ }
315
+ }
316
+ }
317
+
318
+ /*
319
+ * A select whose option list is drawn here (the customizable select, `appearance: base-select`),
320
+ * in the menu's colours: each `<optgroup>` label above its rows, and a tick on the row chosen.
321
+ * Keyboard use and where the list opens stay the browser's. A browser without it skips this whole
322
+ * block, since an unknown pseudo-element voids any rule it is in, and shows its own list under the
323
+ * same closed control.
324
+ */
325
+ @supports (appearance: base-select) {
326
+ @layer components {
327
+ .gk-select,
328
+ .gk-select::picker(select) {
329
+ appearance: base-select;
330
+ }
331
+ .gk-select {
332
+ display: inline-flex;
333
+ align-items: center;
334
+ gap: 0.5rem;
335
+ }
336
+ /* A chevron drawn from two borders, in the muted ink. */
337
+ .gk-select::picker-icon {
338
+ content: '';
339
+ flex: none;
340
+ width: 0.4375rem;
341
+ height: 0.4375rem;
342
+ margin: 0 0.1875rem 0.1875rem auto;
343
+ border-right: 1.5px solid var(--gk-text-muted);
344
+ border-bottom: 1.5px solid var(--gk-text-muted);
345
+ transform: rotate(45deg);
346
+ }
347
+ .gk-select::picker(select) {
348
+ max-block-size: min(20rem, 60vh);
349
+ overflow-y: auto;
350
+ margin-block: 0.25rem;
351
+ padding: 0.25rem 0;
352
+ border: 1px solid var(--gk-menu-border);
353
+ border-radius: 0.5rem;
354
+ background: var(--gk-menu-bg);
355
+ color: var(--gk-control-text);
356
+ box-shadow: var(--gk-menu-shadow);
357
+ }
358
+ .gk-select optgroup {
359
+ padding-block-start: 0.25rem;
360
+ color: var(--gk-text-muted);
361
+ font-weight: 600;
362
+ }
363
+ .gk-select optgroup + optgroup {
364
+ margin-block-start: 0.25rem;
365
+ border-block-start: 1px solid var(--gk-menu-border);
366
+ }
367
+ .gk-select option {
368
+ gap: 0.5rem;
369
+ padding: 0.375rem 0.75rem;
370
+ color: var(--gk-control-text);
371
+ font-weight: 400;
372
+ line-height: 1.25rem;
373
+ cursor: pointer;
374
+ }
375
+ .gk-select option:hover,
376
+ .gk-select option:focus-visible {
377
+ background: var(--gk-menu-hover);
378
+ outline: none;
379
+ }
380
+ .gk-select option:checked {
381
+ font-weight: 600;
382
+ }
383
+ /* The tick, at the end of the row chosen. */
384
+ .gk-select option::checkmark {
385
+ order: 1;
386
+ margin-inline-start: auto;
387
+ color: var(--gk-accent);
388
+ line-height: 1;
389
+ }
390
+ }
391
+ }
package/dist/assets.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * The open graph's assets, as far as an extension may add to them.
7
7
  *
8
- * @beta
8
+ * @public
9
9
  */
10
10
  export interface ExtensionAssets {
11
11
  /**
@@ -15,9 +15,12 @@ export interface ExtensionAssets {
15
15
  */
16
16
  prepareImage(image: Blob): Promise<Blob>;
17
17
  /**
18
- * Keep an image as one of the graph's assets, and answer the markdown that shows it in a
19
- * document, such as `![photo](assets/photo.webp)`. Null where the graph keeps no assets here,
20
- * or the image was refused.
18
+ * Keep an image as one of the graph's assets, made smaller first as {@link ExtensionAssets.prepareImage}
19
+ * does, and answer the markdown that shows it in a document, such as
20
+ * `![photo](assets/photo.3f9a1c2e.webp)`. The file is named from `name` and a short hash of its
21
+ * bytes, so two images never take the same name, and the same image kept twice is one file.
22
+ * An asset is part of the graph: it syncs, and every member of a shared graph can see it. Null
23
+ * where the graph keeps no assets here, or the image was refused.
21
24
  */
22
25
  saveImage(image: Blob, name: string): Promise<string | null>;
23
26
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The Client's own Commands that any extension may run (ADR 0120, amended 2026-10-17): through
3
+ * `commands.execute`, or as the Command of a menu row or a button it adds. Each one keeps its id
4
+ * and the argument it takes in every release, as the rest of the Stable API does. An extension may
5
+ * also run its own Commands and those of the other extensions running, by their ids.
6
+ *
7
+ * - `agents.settings`: open Settings and Extensions at the Agents tab. It takes no argument.
8
+ *
9
+ * @public
10
+ */
11
+ export declare const CLIENT_COMMANDS: readonly string[];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The Client's own Commands that any extension may run (ADR 0120, amended 2026-10-17): through
3
+ * `commands.execute`, or as the Command of a menu row or a button it adds. Each one keeps its id
4
+ * and the argument it takes in every release, as the rest of the Stable API does. An extension may
5
+ * also run its own Commands and those of the other extensions running, by their ids.
6
+ *
7
+ * - `agents.settings`: open Settings and Extensions at the Agents tab. It takes no argument.
8
+ *
9
+ * @public
10
+ */
11
+ export const CLIENT_COMMANDS = ['agents.settings'];