@rigee/plugin-api 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/host.d.ts ADDED
@@ -0,0 +1,460 @@
1
+ /**
2
+ * The shell, as a plugin's renderer half sees it.
3
+ *
4
+ * WHY THIS IS A SHIM AND NOT AN IMPLEMENTATION. A plugin bundled with the app could simply import
5
+ * the shell's modules; a plugin from somebody else's repository cannot — it is built separately,
6
+ * against this package, and only meets the shell at runtime. So the package declares the SHAPE and
7
+ * the shell installs the BEHAVIOUR, once, before any plugin activates. Both kinds of plugin then
8
+ * write the same import and get the same object, and there is exactly one implementation to keep
9
+ * correct.
10
+ *
11
+ * It matters that there is exactly one copy of this module in the window. Everything below reads
12
+ * module-level state, so two copies would mean a plugin talking to a host nobody installed. For
13
+ * bundled plugins the build guarantees it (one alias, one chunk); for disk plugins the renderer's
14
+ * import map points `@rigee/plugin-api` at the shell's own copy, which is the entire reason that
15
+ * import map exists.
16
+ *
17
+ * CORDIS-FREE, ON PURPOSE. A plugin uses `ctx` for things that must be revoked when it is
18
+ * disposed — binding components, subscribing to its main half — and this module for everything
19
+ * else. A cordis service access costs ~15 us against ~8 ns for a plain call, which is unacceptable
20
+ * inside a React render, and none of this needs a lifecycle.
21
+ */
22
+ import type { FileStat, TabId, Workspace, WorkspaceId } from './data';
23
+ import type { PluginId, ViewType } from './manifest';
24
+ import type { IconSet, NoticeProps } from './ui';
25
+ import type { ComponentType, ReactElement } from 'react';
26
+ export interface PluginTabRequest {
27
+ viewType: ViewType;
28
+ title: string;
29
+ /** Hover text for the tab strip — the path, the URL, whatever identifies it to a reader. */
30
+ tooltip?: string;
31
+ state: Record<string, unknown>;
32
+ /**
33
+ * Identity for de-duplication. Opening the same key twice re-activates the existing tab rather
34
+ * than adding a second one; leave it out when duplicates are legitimate.
35
+ *
36
+ * De-duplication is PER EDITOR GROUP: the same key open in two columns is two tabs, because the
37
+ * same document side by side is what splitting is for.
38
+ */
39
+ key?: string;
40
+ /**
41
+ * Open beside this tab — in whichever group holds it — instead of in the active group.
42
+ *
43
+ * For a tab opened BY another tab rather than by the user. A `window.open` popup from a page in
44
+ * the right-hand group belongs in the right-hand group: the user's focus may be anywhere by the
45
+ * time the page gets round to opening it, and landing in the wrong column looks like the click
46
+ * did something else entirely. Ignored when the tab is gone or lives in another workspace.
47
+ */
48
+ nearTabId?: TabId;
49
+ }
50
+ /** Open a file in whichever viewer claims it. What clicking a file in a tree should do. */
51
+ export interface OpenFileRequest {
52
+ /** Absolute path. */
53
+ path: string;
54
+ /** Tab title. Defaults to the basename. */
55
+ title?: string;
56
+ readOnly?: boolean;
57
+ /**
58
+ * Open it in this editor instead of the one the shell would pick — one `editorsOnRequest`
59
+ * offered. Ignored unless that editor claims the file, so a stale menu still opens something.
60
+ */
61
+ viewType?: ViewType;
62
+ /** As on `PluginTabRequest`: put it beside this tab rather than in the active group. */
63
+ nearTabId?: TabId;
64
+ /**
65
+ * Where to put the cursor — a search result's match. Delivered to the editor through
66
+ * `onRevealInFile`, whether the tab is new or was already open.
67
+ */
68
+ selection?: TextSelection;
69
+ /**
70
+ * Leave the keyboard where it is instead of handing it to the file's viewer. What a single click
71
+ * in a file tree does, so the tree's arrow keys and Delete go on acting on the tree. Without it
72
+ * the viewer takes the keyboard — as it mounts, or at once if the file is already on screen.
73
+ */
74
+ preserveFocus?: boolean;
75
+ }
76
+ /** A file or folder a plugin renamed or moved: where it was, and where it is now. */
77
+ export interface FileMove {
78
+ from: string;
79
+ to: string;
80
+ }
81
+ /** A place in a text file. 1-based, columns in UTF-16 code units, as Monaco counts them. */
82
+ export interface TextSelection {
83
+ line: number;
84
+ column: number;
85
+ /** Defaults to `line`. */
86
+ endLine?: number;
87
+ /** Defaults to `column`: a caret, not a range. */
88
+ endColumn?: number;
89
+ }
90
+ /**
91
+ * How the shell saves or drops a file's unsaved changes, when a tab closing or the app quitting
92
+ * asks the user and the user answers. Both reject to say they could not.
93
+ */
94
+ export interface FileHandlers {
95
+ save: () => Promise<void>;
96
+ discard: () => Promise<void>;
97
+ }
98
+ /**
99
+ * One run of a line of code that tokenises as one thing: where it starts, and what it is.
100
+ *
101
+ * `type` is a Monarch token type, as the text editor's tokenizers name them — `keyword.ts`,
102
+ * `string.escape.js`, `attribute.value.html`. The first segment is the kind; the last is the
103
+ * language and rarely matters.
104
+ */
105
+ export interface CodeToken {
106
+ /** In UTF-16 code units from the start of the line. */
107
+ offset: number;
108
+ type: string;
109
+ }
110
+ /** What `pluginTabs` reports about one of a plugin's own tabs. */
111
+ /**
112
+ * One row of a context menu a plugin asks the shell to show: a command, or a separator. Plain data;
113
+ * what a command does stays with the plugin, which gets back the `id` of the row picked.
114
+ */
115
+ export type MenuItem = {
116
+ kind: 'item';
117
+ id: string;
118
+ label: string;
119
+ enabled?: boolean;
120
+ /**
121
+ * The shortcut drawn beside the label, in Electron's accelerator syntax: `CmdOrCtrl+C`. Shown,
122
+ * not listened for — the plugin answers the key itself, wherever it has the keyboard.
123
+ */
124
+ accelerator?: string;
125
+ } | {
126
+ kind: 'separator';
127
+ };
128
+ export interface PluginTabInfo {
129
+ tabId: TabId;
130
+ state: Record<string, unknown>;
131
+ key?: string;
132
+ }
133
+ /** An editor that opens a file only when asked, as a file's menu offers it. */
134
+ export interface EditorOffer {
135
+ viewType: ViewType;
136
+ /** In the editor's own words, from its manifest: `Open Preview`. */
137
+ label: string;
138
+ }
139
+ /**
140
+ * Everything the shell provides. Implemented once, in the shell; see `installShellHost`.
141
+ *
142
+ * Adding a method here is backwards compatible. Removing or reshaping one is not — bump
143
+ * `PLUGIN_API_VERSION` when you do, because a disk plugin built against the old shape is refused
144
+ * at discovery rather than left to fail at a call site.
145
+ */
146
+ export interface ShellHost {
147
+ isMac: () => boolean;
148
+ /** The font stack the shell's editors use, so a plugin's does not look foreign. */
149
+ monoFamily: () => string;
150
+ editorFontSize: () => number;
151
+ activeWorkspaceId: () => WorkspaceId | null;
152
+ /** React hook. Re-renders when the workspace changes. */
153
+ useActiveWorkspace: () => Workspace | null;
154
+ /**
155
+ * The workspace's own folder — the one its `.code-workspace` file is in, where its terminals
156
+ * start — or null for a workspace with no file: never saved, a plain folder, Scratch. Not
157
+ * necessarily one of `folders`, and usually not: those are the repos it groups.
158
+ */
159
+ workspaceDir: (workspace: Workspace) => string | null;
160
+ addWorkspaceFolders: (paths: string[]) => Promise<void>;
161
+ /**
162
+ * Take some of the active workspace's `folders` out of it, and out of its `.code-workspace` file
163
+ * when it has one. Only the workspace changes: the folders stay on disk, and so do the tabs of
164
+ * files in them. Paths it does not list are ignored.
165
+ */
166
+ removeWorkspaceFolders: (paths: string[]) => Promise<void>;
167
+ /**
168
+ * Open a terminal in the active workspace whose shell starts in `cwd` — a folder the user picked,
169
+ * so it never stops to ask about saving the workspace. `tab` (the default) is a new terminal tab;
170
+ * `split` is a new pane beside the active one in the terminal on screen, or a new tab when no
171
+ * terminal is showing. Returns the tab it is in, or null with no workspace to put it in.
172
+ */
173
+ openTerminal: (request: {
174
+ cwd: string;
175
+ where?: 'tab' | 'split';
176
+ }) => TabId | null;
177
+ openPluginTab: (pluginId: PluginId, request: PluginTabRequest) => TabId | null;
178
+ openFile: (request: OpenFileRequest) => TabId | null;
179
+ editorsOnRequest: (path: string) => EditorOffer[];
180
+ patchPluginTab: (tabId: TabId, patch: {
181
+ title?: string;
182
+ tooltip?: string;
183
+ key?: string;
184
+ state?: Record<string, unknown>;
185
+ }) => void;
186
+ closePluginTab: (tabId: TabId) => void;
187
+ activateTab: (tabId: TabId) => void;
188
+ pluginTabs: (viewType: ViewType) => PluginTabInfo[];
189
+ liveTabIds: () => Set<TabId>;
190
+ subscribeTabs: (listener: (live: Set<TabId>) => void) => () => void;
191
+ duplicatedFrom: (tabId: TabId) => TabId | null;
192
+ focusedTabId: () => TabId | null;
193
+ useFocusedTabId: () => TabId | null;
194
+ mountedTabIds: () => TabId[];
195
+ subscribeMountedTabs: (listener: (tabIds: TabId[]) => void) => () => void;
196
+ pluginState: (pluginId: PluginId, workspaceId: WorkspaceId) => Record<string, unknown> | undefined;
197
+ usePluginState: (pluginId: PluginId, workspaceId: WorkspaceId | null) => Record<string, unknown> | undefined;
198
+ setPluginState: (pluginId: PluginId, workspaceId: WorkspaceId, patch: Record<string, unknown>) => void;
199
+ setFileDirty: (path: string, dirty: boolean, handlers?: FileHandlers) => void;
200
+ onRevealInFile: (tabId: TabId, cb: (selection: TextSelection) => void) => () => void;
201
+ onViewRevealed: (viewId: string, cb: () => void) => () => void;
202
+ openedInBackground: (tabId: TabId) => boolean;
203
+ filesMoved: (moves: FileMove[]) => void;
204
+ filesDeleted: (paths: string[]) => void;
205
+ unsavedFiles: (paths: string[]) => string[];
206
+ setFileDecorations: (source: string, decorations: Record<string, FileDecoration>) => void;
207
+ useFileDecoration: (path: string) => FileDecoration | undefined;
208
+ readFile: (path: string) => Promise<{
209
+ content: string;
210
+ truncated: boolean;
211
+ tooLarge: boolean;
212
+ notUtf8?: boolean;
213
+ }>;
214
+ writeFile: (path: string, content: string) => Promise<void>;
215
+ listDir: (path: string) => Promise<FileStat[]>;
216
+ tokenizeCode: (text: string, language: string) => Promise<CodeToken[][] | null>;
217
+ workerUrl: (script: string | URL) => string;
218
+ /**
219
+ * A native context menu at the pointer: the id of the row picked, or null when dismissed. Native
220
+ * because a browser tab's page is drawn over the whole document, and a menu drawn in it can end
221
+ * up underneath.
222
+ */
223
+ popupMenu: (items: MenuItem[]) => Promise<string | null>;
224
+ icons: IconSet;
225
+ Notice: ComponentType<NoticeProps>;
226
+ }
227
+ /**
228
+ * Called by the shell, exactly once, before any plugin activates.
229
+ *
230
+ * Throwing on a second call is not pedantry: a second install means a second copy of this module
231
+ * got loaded, and the plugins that imported the first copy are now talking to a host the shell has
232
+ * stopped updating. That failure is invisible — components render, calls resolve, and one plugin's
233
+ * tabs quietly go to the wrong place. Far better to fail at the install.
234
+ */
235
+ export declare function installShellHost(impl: ShellHost): void;
236
+ /** True once the shell has installed itself. For the shell's own start-up assertions. */
237
+ export declare function shellHostInstalled(): boolean;
238
+ /**
239
+ * The shell's icons, filled in at install.
240
+ *
241
+ * A mutable object rather than named exports because named exports cannot be assigned from
242
+ * outside the module, and a per-icon forwarding component would add a React layer to every row of
243
+ * a file tree. After install these are the shell's own components, reached by one property read.
244
+ */
245
+ export declare const icons: IconSet;
246
+ export declare const isMac: () => boolean;
247
+ export declare const monoFamily: () => string;
248
+ export declare const editorFontSize: () => number;
249
+ export declare const activeWorkspaceId: () => WorkspaceId | null;
250
+ export declare const useActiveWorkspace: () => Workspace | null;
251
+ export declare const workspaceDir: (workspace: Workspace) => string | null;
252
+ export declare const addWorkspaceFolders: (paths: string[]) => Promise<void>;
253
+ export declare const removeWorkspaceFolders: (paths: string[]) => Promise<void>;
254
+ export declare const openTerminal: (request: {
255
+ cwd: string;
256
+ where?: "tab" | "split";
257
+ }) => TabId | null;
258
+ export declare const popupMenu: (items: MenuItem[]) => Promise<string | null>;
259
+ /**
260
+ * Open (or re-activate) a tab this plugin owns, in the active workspace's active group.
261
+ *
262
+ * Returns the tab's id, or null when there is no workspace to put it in. The view type must be one
263
+ * this plugin declared; the shell throws if it belongs to somebody else.
264
+ */
265
+ export declare const openPluginTab: (pluginId: PluginId, request: PluginTabRequest) => TabId | null;
266
+ /**
267
+ * Open a file in whichever editor claims it.
268
+ *
269
+ * The resolution is the SHELL's — it reads the manifests, so no plugin is loaded to answer "who
270
+ * opens a .pdf" and the caller does not need to know that editors have priorities. This is what a
271
+ * file tree calls; before it existed the explorer reached into the store, invented a tab id and
272
+ * resolved the view type itself, which meant three shell internals in one click handler.
273
+ */
274
+ export declare const openFile: (request: OpenFileRequest) => TabId | null;
275
+ /**
276
+ * The editors that open this file only when asked — a Markdown file's preview — in the words to
277
+ * offer them in. What a file tree puts in a file's menu; picking one is `openFile({ viewType })`.
278
+ * Read off the manifests, so nothing is loaded to answer, and empty for most files.
279
+ */
280
+ export declare const editorsOnRequest: (path: string) => EditorOffer[];
281
+ export declare const patchPluginTab: (tabId: TabId, patch: {
282
+ title?: string;
283
+ tooltip?: string;
284
+ key?: string;
285
+ state?: Record<string, unknown>;
286
+ }) => void;
287
+ export declare const closePluginTab: (tabId: TabId) => void;
288
+ /**
289
+ * Make this tab the one its column shows, and its column the active one. For content that takes
290
+ * the keyboard where the shell cannot see it — a click inside a native view — so that the shell's
291
+ * idea of "where the user is working", which every shortcut acts on, follows. A no-op when it is
292
+ * already so.
293
+ */
294
+ export declare const activateTab: (tabId: TabId) => void;
295
+ /**
296
+ * Every open tab of one of this plugin's own view types, across every workspace.
297
+ *
298
+ * Exists for one job: repairing tabs restored from a persisted state older than the plugin. The
299
+ * shell can move an old tab's fields into `state`, but it cannot invent a dedup key — only the
300
+ * plugin knows what identifies one of its tabs.
301
+ */
302
+ export declare const pluginTabs: (viewType: ViewType) => PluginTabInfo[];
303
+ /**
304
+ * Every tab id that exists, in every group of every workspace.
305
+ *
306
+ * For plugins holding a resource per tab — a native browser view, a parsed document — that has to
307
+ * be released when the tab goes. Callers treat anything absent from this set as closed, so it spans
308
+ * every workspace deliberately: switching workspace must not read as "every tab was closed".
309
+ */
310
+ export declare const liveTabIds: () => Set<TabId>;
311
+ /** Watch for tabs disappearing. Wrap the returned undo in `ctx.effect` so it dies with the plugin. */
312
+ export declare const subscribeTabs: (listener: (live: Set<TabId>) => void) => (() => void);
313
+ /**
314
+ * The tab this one was split from, or null.
315
+ *
316
+ * CONSUME-ONCE. Read it on first mount and keep what you got; the second caller, and every render
317
+ * after the first, gets null. StrictMode's double-invoked effect counts as a second caller, so this
318
+ * belongs in a `useState` initialiser or a ref, never in an effect body.
319
+ */
320
+ export declare const duplicatedFrom: (tabId: TabId) => TabId | null;
321
+ /**
322
+ * The one tab with the keyboard: the active group's active tab, in the active workspace.
323
+ *
324
+ * One rule, in one place. Two copies of it is how two visible panes end up disagreeing about which
325
+ * of them should be typing.
326
+ */
327
+ export declare const focusedTabId: () => TabId | null;
328
+ /** The same rule as a hook, for a pane that has to re-render when focus moves to another column. */
329
+ export declare const useFocusedTabId: () => TabId | null;
330
+ /**
331
+ * The active tab of every group in the active workspace, left to right.
332
+ *
333
+ * What is actually on screen. A plugin compositing something outside the DOM — a native view — has
334
+ * to know exactly this set, and only for the active workspace: the tabs of the workspace you
335
+ * switched away from are alive but not visible.
336
+ */
337
+ export declare const mountedTabIds: () => TabId[];
338
+ /** Fires when `mountedTabIds` changes — a split, a drag between groups, a workspace switch. */
339
+ export declare const subscribeMountedTabs: (listener: (tabIds: TabId[]) => void) => (() => void);
340
+ /**
341
+ * A plugin's own persisted state for one workspace.
342
+ *
343
+ * Saved and restored with the workspace by the shell, which never looks inside it. For the things
344
+ * a panel wants to remember per project — which directories are expanded, a filter, a scroll
345
+ * position. Undefined means "nothing recorded yet", which is different from "recorded as empty":
346
+ * the file tree opens its root folders on first sight and must not do it again after the user
347
+ * collapses everything.
348
+ */
349
+ export declare const pluginState: (pluginId: PluginId, workspaceId: WorkspaceId) => Record<string, unknown> | undefined;
350
+ /** The same, as a hook. Null workspace yields undefined rather than throwing. */
351
+ export declare const usePluginState: (pluginId: PluginId, workspaceId: WorkspaceId | null) => Record<string, unknown> | undefined;
352
+ /** Merge into this plugin's state for one workspace, and persist it. */
353
+ export declare const setPluginState: (pluginId: PluginId, workspaceId: WorkspaceId, patch: Record<string, unknown>) => void;
354
+ /**
355
+ * Say whether a file has unsaved changes, and how to save or drop them.
356
+ *
357
+ * The shell marks its tabs with a dot and, when one would close — or the app would quit — with
358
+ * the changes unsaved, asks the user and calls `save` or `discard`. Keyed by PATH, not tab: one
359
+ * document can be open in two columns, and it is the document that is unsaved.
360
+ */
361
+ export declare const setFileDirty: (path: string, dirty: boolean, handlers?: FileHandlers) => void;
362
+ /** How a file's name is drawn in the tree: source control's colours, in VS Code's manner. */
363
+ export interface FileDecoration {
364
+ /** A CSS colour, usually one of the theme's variables: `var(--git-modified)`. */
365
+ color: string;
366
+ /** One or two characters shown at the end of the row — `M`, `U`, `A`. */
367
+ badge?: string;
368
+ tooltip?: string;
369
+ /** Which wins when two decorations meet on one folder. Higher is more important. */
370
+ weight?: number;
371
+ }
372
+ /**
373
+ * Replace every decoration one source publishes, keyed by absolute path. A path ending in a
374
+ * separator covers the folder and everything inside it. An empty record withdraws the source.
375
+ * Folders above a decorated path take its colour, so collapsed trees still show where changes are.
376
+ */
377
+ export declare const setFileDecorations: (source: string, decorations: Record<string, FileDecoration>) => void;
378
+ /** The decoration to draw for a path. A hook: re-renders when that path's decoration changes. */
379
+ export declare const useFileDecoration: (path: string) => FileDecoration | undefined;
380
+ /**
381
+ * Where the cursor should go in the file this tab shows — the target of a search result or any
382
+ * other `openFile({ selection })`. A request made before the editor subscribed is delivered as it
383
+ * subscribes, so subscribe on mount. Wrap the returned undo in the unmount.
384
+ */
385
+ export declare const onRevealInFile: (tabId: TabId, cb: (selection: TextSelection) => void) => (() => void);
386
+ /**
387
+ * Called when the user asks for this side panel by keyboard — the panel should take the keyboard,
388
+ * as a search box does. Delivered on subscribe if the request came before the panel mounted.
389
+ */
390
+ export declare const onViewRevealed: (viewId: string, cb: () => void) => (() => void);
391
+ /**
392
+ * Whether this tab was opened with `preserveFocus`, so its viewer should leave the keyboard where
393
+ * it is instead of taking it as it mounts. Answers true once, for the open that asked: call it at
394
+ * the moment the viewer would focus itself, not before.
395
+ */
396
+ export declare const openedInBackground: (tabId: TabId) => boolean;
397
+ /**
398
+ * Files a plugin has just renamed or moved for the user — a file tree's Rename, its Cut and Paste,
399
+ * a drag — so the tabs showing them follow, under their new names. A folder covers everything in
400
+ * it. Files moved by other programs are not reported here, and their tabs stay where they were.
401
+ */
402
+ export declare const filesMoved: (moves: FileMove[]) => void;
403
+ /**
404
+ * Files a plugin has just deleted for the user. Their tabs close, and unsaved changes in them are
405
+ * dropped — so ask about those first, with `unsavedFiles`. A folder covers everything in it.
406
+ */
407
+ export declare const filesDeleted: (paths: string[]) => void;
408
+ /** Which files at these paths, or inside them, have unsaved changes. */
409
+ export declare const unsavedFiles: (paths: string[]) => string[];
410
+ /**
411
+ * Needs `files:read`. `notUtf8` means the bytes were not valid UTF-8 and `content` is a lossy
412
+ * decoding — show it, but never write it back.
413
+ */
414
+ export declare const readFile: (path: string) => Promise<{
415
+ content: string;
416
+ truncated: boolean;
417
+ tooLarge: boolean;
418
+ notUtf8?: boolean;
419
+ }>;
420
+ /** Needs `files:write`. */
421
+ export declare const writeFile: (path: string, content: string) => Promise<void>;
422
+ /** Needs `files:read`. */
423
+ export declare const listDir: (path: string) => Promise<FileStat[]>;
424
+ /**
425
+ * Tokenise code with the text editor's own tokenizers: the tokens of each line, in order, or null
426
+ * when no language goes by that name. The name may be a language id, an alias or an extension —
427
+ * `ts`, `typescript` and `TypeScript` all find TypeScript — so a Markdown fence's word can be
428
+ * passed as it is.
429
+ *
430
+ * The tokens' text is the caller's: slice each line at the offsets. That is the point of asking
431
+ * for tokens rather than coloured HTML — HTML from the editor writes spaces as no-break spaces, so
432
+ * code copied out of it would not be the code that was written.
433
+ *
434
+ * Loads the editor's tokenizers the first time, which is megabytes: call it for code that is about
435
+ * to be shown, not speculatively. Since Rigee 1.1.0.
436
+ */
437
+ export declare const tokenizeCode: (text: string, language: string) => Promise<CodeToken[][] | null>;
438
+ /**
439
+ * The URL to start a Web Worker from, for a script that ships with this plugin.
440
+ *
441
+ * A window may only start a worker from its own origin, and a plugin's files are served from
442
+ * another — `rigee-plugin://` — so `new Worker(new URL('./x.js', import.meta.url))` is refused.
443
+ * Pass the same URL here instead and start the worker from what comes back, as a module worker:
444
+ *
445
+ * new Worker(workerUrl(new URL('./parse.worker.js', import.meta.url)), { type: 'module' })
446
+ *
447
+ * The script must be self-contained: a worker has no import map, so `react` and this package
448
+ * cannot be imported from inside one. Messages posted before the script has loaded are held until
449
+ * it has, and then delivered. The worker gets no Content Security Policy of its own, so it may
450
+ * compile WebAssembly, which the window may not. Since Rigee 1.1.0.
451
+ */
452
+ export declare const workerUrl: (script: string | URL) => string;
453
+ /**
454
+ * The shell's "this pane has nothing to show" state.
455
+ *
456
+ * A wrapper rather than an entry in `icons`, because a notice is rendered once per failed pane
457
+ * where an icon is rendered once per row — the extra component layer costs nothing here and reads
458
+ * better at the call site.
459
+ */
460
+ export declare function Notice(props: NoticeProps): ReactElement;