@silo-code/sdk 0.6.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/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/context-keys.d.ts +19 -0
- package/dist/context-keys.d.ts.map +1 -0
- package/dist/context-keys.js +2 -0
- package/dist/context-keys.js.map +1 -0
- package/dist/dnd-service.d.ts +140 -0
- package/dist/dnd-service.d.ts.map +1 -0
- package/dist/dnd-service.js +17 -0
- package/dist/dnd-service.js.map +1 -0
- package/dist/domain-types.d.ts +237 -0
- package/dist/domain-types.d.ts.map +1 -0
- package/dist/domain-types.js +11 -0
- package/dist/domain-types.js.map +1 -0
- package/dist/editor-service.d.ts +175 -0
- package/dist/editor-service.d.ts.map +1 -0
- package/dist/editor-service.js +2 -0
- package/dist/editor-service.js.map +1 -0
- package/dist/extension-storage.d.ts +26 -0
- package/dist/extension-storage.d.ts.map +1 -0
- package/dist/extension-storage.js +2 -0
- package/dist/extension-storage.js.map +1 -0
- package/dist/file-service.d.ts +84 -0
- package/dist/file-service.d.ts.map +1 -0
- package/dist/file-service.js +2 -0
- package/dist/file-service.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/layout-service.d.ts +46 -0
- package/dist/layout-service.d.ts.map +1 -0
- package/dist/layout-service.js +2 -0
- package/dist/layout-service.js.map +1 -0
- package/dist/permissions.d.ts +41 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +40 -0
- package/dist/permissions.js.map +1 -0
- package/dist/process-service.d.ts +132 -0
- package/dist/process-service.d.ts.map +1 -0
- package/dist/process-service.js +2 -0
- package/dist/process-service.js.map +1 -0
- package/dist/terminal-service.d.ts +38 -0
- package/dist/terminal-service.d.ts.map +1 -0
- package/dist/terminal-service.js +2 -0
- package/dist/terminal-service.js.map +1 -0
- package/dist/theme-service.d.ts +87 -0
- package/dist/theme-service.d.ts.map +1 -0
- package/dist/theme-service.js +2 -0
- package/dist/theme-service.js.map +1 -0
- package/dist/types.d.ts +495 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-service.d.ts +469 -0
- package/dist/ui-service.d.ts.map +1 -0
- package/dist/ui-service.js +2 -0
- package/dist/ui-service.js.map +1 -0
- package/dist/use-focus-group.d.ts +202 -0
- package/dist/use-focus-group.d.ts.map +1 -0
- package/dist/use-focus-group.js +236 -0
- package/dist/use-focus-group.js.map +1 -0
- package/dist/use-service-state.d.ts +36 -0
- package/dist/use-service-state.d.ts.map +1 -0
- package/dist/use-service-state.js +25 -0
- package/dist/use-service-state.js.map +1 -0
- package/dist/workspace-service.d.ts +72 -0
- package/dist/workspace-service.d.ts.map +1 -0
- package/dist/workspace-service.js +2 -0
- package/dist/workspace-service.js.map +1 -0
- package/package.json +54 -0
- package/src/context-keys.ts +18 -0
- package/src/dnd-service.ts +151 -0
- package/src/domain-types.ts +252 -0
- package/src/editor-service.ts +196 -0
- package/src/extension-storage.ts +25 -0
- package/src/file-service.ts +90 -0
- package/src/index.ts +151 -0
- package/src/layout-service.ts +49 -0
- package/src/permissions.ts +55 -0
- package/src/process-service.ts +143 -0
- package/src/terminal-service.ts +41 -0
- package/src/theme-service.ts +102 -0
- package/src/types.ts +513 -0
- package/src/ui-service.ts +487 -0
- package/src/use-focus-group.test.ts +168 -0
- package/src/use-focus-group.ts +382 -0
- package/src/use-service-state.ts +43 -0
- package/src/workspace-service.ts +76 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A file-type filter for the native open/save dialogs ({@link UiService.pickFile},
|
|
4
|
+
* {@link UiService.savePath}) — a human-readable group plus the extensions it
|
|
5
|
+
* matches. Mirrors the OS dialog's file-type dropdown.
|
|
6
|
+
*
|
|
7
|
+
* @category Core Types
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export interface FileFilter {
|
|
11
|
+
/** Human-readable label for the group, e.g. `"JSON"` or `"Images"`. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Extensions this group matches, **without** the leading dot, e.g. `["json"]`. */
|
|
14
|
+
extensions: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A secondary control rendered at the trailing edge of a {@link MenuItem} —
|
|
18
|
+
* e.g. a delete button on a row whose primary click does something else
|
|
19
|
+
* (reopen). Its click is isolated: it runs `onClick` and does **not** trigger
|
|
20
|
+
* the row's {@link MenuItem.run}.
|
|
21
|
+
*
|
|
22
|
+
* @category Registration
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
export interface MenuItemTrailing {
|
|
26
|
+
/** The control's glyph (e.g. a Phosphor icon element). */
|
|
27
|
+
icon: ReactNode;
|
|
28
|
+
/** Native tooltip for the control. */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Invoked when the control is clicked; the menu closes first. */
|
|
31
|
+
onClick: () => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One actionable row in a menu shown by {@link UiService.showMenu}. The host
|
|
35
|
+
* renders and themes the chrome; the extension supplies the data and an action.
|
|
36
|
+
*
|
|
37
|
+
* @category Registration
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
export interface MenuItem {
|
|
41
|
+
/** The row's text. */
|
|
42
|
+
label: string;
|
|
43
|
+
/**
|
|
44
|
+
* A pre-formatted shortcut hint shown right-aligned, e.g. `"⌘C"` or
|
|
45
|
+
* `"Ctrl+C"`. Display only — it does not bind the key. Format it for the
|
|
46
|
+
* platform yourself.
|
|
47
|
+
*/
|
|
48
|
+
accelerator?: string;
|
|
49
|
+
/** Leading glyph (e.g. a Phosphor icon element). */
|
|
50
|
+
icon?: ReactNode;
|
|
51
|
+
/** Show a check in the leading gutter — for toggle / current-selection rows. */
|
|
52
|
+
checked?: boolean;
|
|
53
|
+
/** Render the row dimmed and inert. */
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
/** Style the row as destructive (e.g. Delete). */
|
|
56
|
+
danger?: boolean;
|
|
57
|
+
/** Native tooltip for the row. */
|
|
58
|
+
title?: string;
|
|
59
|
+
/** A secondary trailing control (see {@link MenuItemTrailing}). */
|
|
60
|
+
trailing?: MenuItemTrailing;
|
|
61
|
+
/**
|
|
62
|
+
* A nested menu that cascades open to the side when this row is hovered or
|
|
63
|
+
* clicked. A row with a `submenu` is a *parent*: it shows a trailing caret and
|
|
64
|
+
* opening it reveals these {@link MenuEntry | entries} rather than running an
|
|
65
|
+
* action. Give a row a `submenu` **or** a {@link MenuItem.run | run}, not both
|
|
66
|
+
* (a `run` is ignored while the submenu is the active target).
|
|
67
|
+
*/
|
|
68
|
+
submenu?: MenuEntry[];
|
|
69
|
+
/**
|
|
70
|
+
* Invoked when the row is chosen; the menu closes first. Optional only for
|
|
71
|
+
* submenu parents (rows with a {@link MenuItem.submenu | submenu}); every leaf
|
|
72
|
+
* row must supply one.
|
|
73
|
+
*/
|
|
74
|
+
run?: () => void | Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Options for {@link UiService.confirm} — a host-rendered yes/no dialog. Always
|
|
78
|
+
* dismissible (`Escape` and backdrop-click both resolve to `false`, the safe
|
|
79
|
+
* choice). Set {@link ConfirmOptions.danger | danger} for destructive actions.
|
|
80
|
+
*
|
|
81
|
+
* @category Core Types
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
export interface ConfirmOptions {
|
|
85
|
+
/** The dialog's heading. */
|
|
86
|
+
title: string;
|
|
87
|
+
/** Optional explanatory line beneath the title. */
|
|
88
|
+
body?: string;
|
|
89
|
+
/** Label for the confirm button. Default `"OK"`. */
|
|
90
|
+
confirmLabel?: string;
|
|
91
|
+
/** Label for the cancel button. Default `"Cancel"`. */
|
|
92
|
+
cancelLabel?: string;
|
|
93
|
+
/** Style the confirm button as destructive (`.silo-button-danger`). */
|
|
94
|
+
danger?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Options for {@link UiService.prompt} — a host-rendered single-line text input
|
|
98
|
+
* dialog. Always dismissible (`Escape` and backdrop-click both resolve to
|
|
99
|
+
* `null`, i.e. cancelled).
|
|
100
|
+
*
|
|
101
|
+
* @category Core Types
|
|
102
|
+
* @public
|
|
103
|
+
*/
|
|
104
|
+
export interface PromptOptions {
|
|
105
|
+
/** The dialog's heading. */
|
|
106
|
+
title: string;
|
|
107
|
+
/** Optional label shown above the input. */
|
|
108
|
+
label?: string;
|
|
109
|
+
/** Pre-fills the input (and is selected for easy replacement). */
|
|
110
|
+
initialValue?: string;
|
|
111
|
+
/** Placeholder shown when the input is empty. */
|
|
112
|
+
placeholder?: string;
|
|
113
|
+
/** Label for the confirm button. Default `"OK"`. */
|
|
114
|
+
confirmLabel?: string;
|
|
115
|
+
/** Label for the cancel button. Default `"Cancel"`. */
|
|
116
|
+
cancelLabel?: string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* One action button rendered in a toast — see {@link NotifyOptions.actions}.
|
|
120
|
+
* The host themes the button; the extension supplies the label and what to do.
|
|
121
|
+
*
|
|
122
|
+
* @category Core Types
|
|
123
|
+
* @public
|
|
124
|
+
*/
|
|
125
|
+
export interface NotifyAction {
|
|
126
|
+
/** The button's text. */
|
|
127
|
+
label: string;
|
|
128
|
+
/**
|
|
129
|
+
* Invoked when the button is clicked. The toast then dismisses unless
|
|
130
|
+
* {@link NotifyAction.keepOpen} is set — so a "View details" action that opens
|
|
131
|
+
* a modal can close the toast behind it.
|
|
132
|
+
*/
|
|
133
|
+
run: () => void | Promise<void>;
|
|
134
|
+
/** Keep the toast open after {@link NotifyAction.run} (default: dismiss it). */
|
|
135
|
+
keepOpen?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Options for {@link UiService.notify} — an optional title, action buttons, and
|
|
139
|
+
* auto-dismiss control layered on top of the toast's `level` + `message`.
|
|
140
|
+
*
|
|
141
|
+
* @category Core Types
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
export interface NotifyOptions {
|
|
145
|
+
/** A short bold heading rendered above the `message`. */
|
|
146
|
+
title?: string;
|
|
147
|
+
/** Action buttons rendered in the toast's footer (see {@link NotifyAction}). */
|
|
148
|
+
actions?: NotifyAction[];
|
|
149
|
+
/**
|
|
150
|
+
* Auto-dismiss delay in milliseconds. Omit for the default behavior: `error`
|
|
151
|
+
* toasts and any toast with {@link NotifyOptions.actions | actions} stay until
|
|
152
|
+
* the user dismisses them, while `info` / `warn` auto-dismiss after ~4s. Pass
|
|
153
|
+
* `0` to force "stay until dismissed"; a positive number sets an explicit delay.
|
|
154
|
+
*/
|
|
155
|
+
durationMs?: number;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Options for {@link UiService.showModal} — the host-owned chrome around your
|
|
159
|
+
* custom modal content. The host owns the backdrop, z-order (stacking above all
|
|
160
|
+
* host chrome, arbitrated centrally), focus trap, and restore-focus-on-close;
|
|
161
|
+
* you supply the content and these presentation options.
|
|
162
|
+
*
|
|
163
|
+
* Unlike {@link ConfirmOptions} / {@link PromptOptions}, a `showModal` dialog is
|
|
164
|
+
* **not dismissible by default** — set {@link ModalOptions.dismissible} to wire
|
|
165
|
+
* `Escape` + backdrop-click to close (guarding staged edits otherwise).
|
|
166
|
+
*
|
|
167
|
+
* @category Core Types
|
|
168
|
+
* @public
|
|
169
|
+
*/
|
|
170
|
+
export interface ModalOptions {
|
|
171
|
+
/** Optional header rendered at the top of the card; omit for bare layouts. */
|
|
172
|
+
title?: ReactNode;
|
|
173
|
+
/**
|
|
174
|
+
* Allow `Escape` and backdrop-click to close the modal (resolving the
|
|
175
|
+
* {@link UiService.showModal} promise with `undefined`). Defaults to
|
|
176
|
+
* **`false`** — the modal stays open until your content calls `close`,
|
|
177
|
+
* guarding against accidental loss of staged edits.
|
|
178
|
+
*/
|
|
179
|
+
dismissible?: boolean;
|
|
180
|
+
/** Width preset for the card. Default `"md"`. Ignored when `bare`. */
|
|
181
|
+
size?: "sm" | "md" | "lg";
|
|
182
|
+
/**
|
|
183
|
+
* Skip the card chrome — your content *is* the card (it supplies its own
|
|
184
|
+
* background/size). The host still owns the backdrop, stacking, and focus
|
|
185
|
+
* trap. Used by full-bleed layouts.
|
|
186
|
+
*/
|
|
187
|
+
bare?: boolean;
|
|
188
|
+
/** Extra class on the card, for special-case layouts. */
|
|
189
|
+
className?: string;
|
|
190
|
+
/** Accessible name for dialogs without a visible {@link ModalOptions.title}. */
|
|
191
|
+
ariaLabel?: string;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* A horizontal rule between groups of menu items.
|
|
195
|
+
*
|
|
196
|
+
* @category Registration
|
|
197
|
+
* @public
|
|
198
|
+
*/
|
|
199
|
+
export interface MenuSeparator {
|
|
200
|
+
type: "separator";
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* A non-interactive group label within a menu.
|
|
204
|
+
*
|
|
205
|
+
* @category Registration
|
|
206
|
+
* @public
|
|
207
|
+
*/
|
|
208
|
+
export interface MenuHeader {
|
|
209
|
+
type: "header";
|
|
210
|
+
/** The label text (rendered uppercase). */
|
|
211
|
+
label: string;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* One entry in a menu — an actionable {@link MenuItem}, a {@link MenuSeparator},
|
|
215
|
+
* or a {@link MenuHeader}.
|
|
216
|
+
*
|
|
217
|
+
* @category Registration
|
|
218
|
+
* @public
|
|
219
|
+
*/
|
|
220
|
+
export type MenuEntry = MenuItem | MenuSeparator | MenuHeader;
|
|
221
|
+
/**
|
|
222
|
+
* Options for {@link UiService.showMenu}. Position resolves in the order
|
|
223
|
+
* `anchor` → `at` → the current cursor (so `showMenu({ items })` with no
|
|
224
|
+
* position opens at the mouse, which is what a right-click handler wants).
|
|
225
|
+
*
|
|
226
|
+
* @category Registration
|
|
227
|
+
* @public
|
|
228
|
+
*/
|
|
229
|
+
export interface ShowMenuOptions {
|
|
230
|
+
/** The rows to show, top to bottom. */
|
|
231
|
+
items: MenuEntry[];
|
|
232
|
+
/** Explicit viewport point — e.g. a right-click's `clientX`/`clientY`. */
|
|
233
|
+
at?: {
|
|
234
|
+
x: number;
|
|
235
|
+
y: number;
|
|
236
|
+
};
|
|
237
|
+
/** Anchor element to hang the menu off (for a button dropdown). */
|
|
238
|
+
anchor?: HTMLElement | null;
|
|
239
|
+
/** Align the menu to the anchor's left (`"start"`, default) or right (`"end"`). */
|
|
240
|
+
align?: "start" | "end";
|
|
241
|
+
/**
|
|
242
|
+
* Toggle an anchored dropdown. When `true` (the default), calling `showMenu`
|
|
243
|
+
* again with the **same `anchor`** while that menu is still open closes it
|
|
244
|
+
* instead of reopening — so a second click on the button dismisses its
|
|
245
|
+
* dropdown. Set `false` to keep the legacy always-(re)open behaviour. Has no
|
|
246
|
+
* effect without an `anchor` (cursor / `at` menus always open).
|
|
247
|
+
*/
|
|
248
|
+
toggle?: boolean;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The user-interaction domain, exposed as {@link ExtensionContext.ui}. The host
|
|
252
|
+
* renders the chrome; an extension only asks. Interactions today:
|
|
253
|
+
*
|
|
254
|
+
* - **Native OS dialogs** — {@link UiService.pickFolder | pickFolder},
|
|
255
|
+
* {@link UiService.pickFile | pickFile}, {@link UiService.savePath | savePath}.
|
|
256
|
+
* Thin wrappers over the platform dialogs the host owns; each resolves to an
|
|
257
|
+
* absolute path or `null` when the user cancels.
|
|
258
|
+
* - **Notifications** — {@link UiService.notify | notify} shows a transient
|
|
259
|
+
* toast, optionally with a title and action buttons (see {@link NotifyOptions}).
|
|
260
|
+
* The only way an extension can proactively message the user.
|
|
261
|
+
* - **Menus** — {@link UiService.showMenu | showMenu} pops a context menu or
|
|
262
|
+
* button dropdown, themed to match the rest of the app.
|
|
263
|
+
* - **Modal dialogs** — {@link UiService.confirm | confirm} and
|
|
264
|
+
* {@link UiService.prompt | prompt} pop a host-owned modal and resolve on the
|
|
265
|
+
* user's choice; {@link UiService.showModal | showModal} pops one around your
|
|
266
|
+
* own custom content (a form or bespoke layout).
|
|
267
|
+
* - **External links** — {@link UiService.openExternal | openExternal} hands a
|
|
268
|
+
* URL to the OS (browser / mail client), the host's gateway to the world
|
|
269
|
+
* outside the app.
|
|
270
|
+
*
|
|
271
|
+
* Mirrors VS Code's `window.show*`. More host-rendered chrome (quick-pick,
|
|
272
|
+
* progress) is planned — see the roadmap.
|
|
273
|
+
*
|
|
274
|
+
* @category Consumer Services
|
|
275
|
+
* @public
|
|
276
|
+
*/
|
|
277
|
+
export interface UiService {
|
|
278
|
+
/**
|
|
279
|
+
* Show the native folder picker. Resolves to the chosen absolute path, or
|
|
280
|
+
* `null` if the user cancelled.
|
|
281
|
+
*
|
|
282
|
+
* @param opts.defaultPath - Absolute path to open the dialog at.
|
|
283
|
+
*/
|
|
284
|
+
pickFolder(opts?: {
|
|
285
|
+
defaultPath?: string;
|
|
286
|
+
}): Promise<string | null>;
|
|
287
|
+
/**
|
|
288
|
+
* Show the native open-file picker (single selection). Resolves to the chosen
|
|
289
|
+
* absolute path, or `null` if the user cancelled.
|
|
290
|
+
*
|
|
291
|
+
* @param opts.defaultPath - Absolute path to open the dialog at.
|
|
292
|
+
* @param opts.filters - Restrict the selectable file types (see {@link FileFilter}).
|
|
293
|
+
*/
|
|
294
|
+
pickFile(opts?: {
|
|
295
|
+
defaultPath?: string;
|
|
296
|
+
filters?: FileFilter[];
|
|
297
|
+
}): Promise<string | null>;
|
|
298
|
+
/**
|
|
299
|
+
* Show the native save dialog. Resolves to the chosen destination's absolute
|
|
300
|
+
* path, or `null` if the user cancelled.
|
|
301
|
+
*
|
|
302
|
+
* @param opts.defaultPath - Seeds the dialog's location and suggested filename.
|
|
303
|
+
* @param opts.filters - Restrict the file-type dropdown (see {@link FileFilter}).
|
|
304
|
+
*/
|
|
305
|
+
savePath(opts?: {
|
|
306
|
+
defaultPath?: string;
|
|
307
|
+
filters?: FileFilter[];
|
|
308
|
+
}): Promise<string | null>;
|
|
309
|
+
/**
|
|
310
|
+
* Show a transient toast notification to the user. Fire-and-forget — the host
|
|
311
|
+
* renders it (and, for `info` / `warn` without actions, auto-dismisses it).
|
|
312
|
+
* `level` drives the icon and accent.
|
|
313
|
+
*
|
|
314
|
+
* Pass {@link NotifyOptions} for a bold `title`, footer `actions`, or an
|
|
315
|
+
* explicit `durationMs`. Errors and toasts with actions stay until dismissed
|
|
316
|
+
* (so a "View details" action isn't lost to the timer); everything else
|
|
317
|
+
* auto-dismisses after ~4s.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* // a plain info toast (auto-dismisses)
|
|
322
|
+
* ctx.ui.notify("info", "Theme exported.");
|
|
323
|
+
*
|
|
324
|
+
* // an error with a title and an action that opens the full detail in a modal
|
|
325
|
+
* ctx.ui.notify("error", String(err), {
|
|
326
|
+
* title: "Commit failed",
|
|
327
|
+
* actions: [
|
|
328
|
+
* {
|
|
329
|
+
* label: "View details",
|
|
330
|
+
* run: () =>
|
|
331
|
+
* ctx.ui.showModal((close) => <pre>{String(err)}</pre>, {
|
|
332
|
+
* title: "Commit failed",
|
|
333
|
+
* dismissible: true,
|
|
334
|
+
* }),
|
|
335
|
+
* },
|
|
336
|
+
* ],
|
|
337
|
+
* });
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
notify(level: "info" | "warn" | "error", message: string, options?: NotifyOptions): void;
|
|
341
|
+
/**
|
|
342
|
+
* Pop a menu — the same themed primitive behind every context menu and
|
|
343
|
+
* dropdown in Silo. Supply the {@link MenuEntry | rows} and where to place it
|
|
344
|
+
* (see {@link ShowMenuOptions}); the host renders it, runs the chosen item's
|
|
345
|
+
* {@link MenuItem.run | run}, and dismisses on outside-click or Escape.
|
|
346
|
+
*
|
|
347
|
+
* Only one menu is open at a time — calling `showMenu` again replaces it,
|
|
348
|
+
* except that re-opening with the same {@link ShowMenuOptions.anchor | anchor}
|
|
349
|
+
* toggles it closed (a second click on a dropdown button dismisses it); opt
|
|
350
|
+
* out with {@link ShowMenuOptions.toggle | toggle: false}. Resolves once an
|
|
351
|
+
* item runs or the menu is dismissed.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```ts
|
|
355
|
+
* // A right-click context menu at the cursor.
|
|
356
|
+
* element.addEventListener("contextmenu", (e) => {
|
|
357
|
+
* e.preventDefault();
|
|
358
|
+
* ctx.ui.showMenu({
|
|
359
|
+
* items: [
|
|
360
|
+
* { label: "Rename", run: rename },
|
|
361
|
+
* { type: "separator" },
|
|
362
|
+
* { label: "Delete", danger: true, run: del },
|
|
363
|
+
* ],
|
|
364
|
+
* });
|
|
365
|
+
* });
|
|
366
|
+
*
|
|
367
|
+
* // A dropdown anchored under a button.
|
|
368
|
+
* ctx.ui.showMenu({ items, anchor: buttonEl });
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
showMenu(opts: ShowMenuOptions): Promise<void>;
|
|
372
|
+
/**
|
|
373
|
+
* Pop a host-rendered confirm dialog and resolve to the user's choice —
|
|
374
|
+
* `true` for confirm, `false` for cancel. Always dismissible: `Escape` and
|
|
375
|
+
* backdrop-click both resolve `false`. The dialog stacks above all host
|
|
376
|
+
* chrome via the modal manager, so it works from anywhere.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```ts
|
|
380
|
+
* if (await ctx.ui.confirm({
|
|
381
|
+
* title: "Delete workspace?",
|
|
382
|
+
* body: `"${name}" and its saved terminals will be permanently removed.`,
|
|
383
|
+
* confirmLabel: "Delete",
|
|
384
|
+
* danger: true,
|
|
385
|
+
* })) {
|
|
386
|
+
* service.delete(id);
|
|
387
|
+
* }
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
confirm(opts: ConfirmOptions): Promise<boolean>;
|
|
391
|
+
/**
|
|
392
|
+
* Pop a host-rendered single-line input dialog and resolve to the entered
|
|
393
|
+
* string, or `null` if the user cancelled (`Escape` / backdrop / Cancel).
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```ts
|
|
397
|
+
* const name = await ctx.ui.prompt({ title: "Rename", initialValue: current });
|
|
398
|
+
* if (name !== null) rename(name);
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
prompt(opts: PromptOptions): Promise<string | null>;
|
|
402
|
+
/**
|
|
403
|
+
* Pop a host-rendered modal around your **own custom content** — the escape
|
|
404
|
+
* hatch beyond {@link UiService.confirm | confirm} / {@link UiService.prompt |
|
|
405
|
+
* prompt} when you need a form or bespoke layout. The host owns the hard parts
|
|
406
|
+
* (backdrop, central z-stacking above all chrome, focus trap,
|
|
407
|
+
* restore-focus-on-close); you own the content.
|
|
408
|
+
*
|
|
409
|
+
* Supply a `render` callback that receives a `close` function and returns the
|
|
410
|
+
* modal's content; wire your own buttons to `close(result)` (or `close()` to
|
|
411
|
+
* cancel). The returned promise resolves with the value passed to `close`, or
|
|
412
|
+
* `undefined` if the modal was dismissed (only possible when
|
|
413
|
+
* {@link ModalOptions.dismissible} is set) or `close()` was called with no
|
|
414
|
+
* argument — paralleling `confirm`→`false` / `prompt`→`null`. If you must tell
|
|
415
|
+
* "dismissed" from "closed with no result" apart, pass a distinct sentinel.
|
|
416
|
+
*
|
|
417
|
+
* **Not dismissible by default:** unless you set
|
|
418
|
+
* {@link ModalOptions.dismissible}, `Escape` and backdrop-click do nothing and
|
|
419
|
+
* the modal stays open until your content calls `close`. A non-dismissible
|
|
420
|
+
* modal whose content never calls `close` leaves the promise pending forever —
|
|
421
|
+
* by design, so staged edits can't be lost to an accidental click-away.
|
|
422
|
+
*
|
|
423
|
+
* @typeParam T - The result type your content resolves with via `close`.
|
|
424
|
+
* @param render - Returns the modal content; receives `close` to settle it.
|
|
425
|
+
* @param options - Presentation options (see {@link ModalOptions}).
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```tsx
|
|
429
|
+
* const changes = await ctx.ui.showModal<Changes>(
|
|
430
|
+
* (close) => (
|
|
431
|
+
* <MyForm onCancel={() => close()} onSave={(c) => close(c)} />
|
|
432
|
+
* ),
|
|
433
|
+
* { title: "Properties", size: "md" },
|
|
434
|
+
* );
|
|
435
|
+
* if (changes) apply(changes);
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
showModal<T = void>(render: (close: (result?: T) => void) => ReactNode, options?: ModalOptions): Promise<T | undefined>;
|
|
439
|
+
/**
|
|
440
|
+
* Hand a URL to the operating system — open an `http`/`https` link in the
|
|
441
|
+
* user's default browser, or a `mailto:` link in their mail client. The host
|
|
442
|
+
* owns the privileged platform access; this is an extension's only sanctioned
|
|
443
|
+
* way to send the user out of the app.
|
|
444
|
+
*
|
|
445
|
+
* **Scheme-guarded.** Only `http:`, `https:`, and `mailto:` URLs are opened;
|
|
446
|
+
* any other scheme (notably `file:` and `javascript:`) is rejected — the
|
|
447
|
+
* returned promise rejects, nothing is opened. This makes it safe to pass
|
|
448
|
+
* untrusted URLs (e.g. links inside a rendered Markdown document) straight
|
|
449
|
+
* through without first vetting the scheme yourself.
|
|
450
|
+
*
|
|
451
|
+
* @param url - The URL to open. Must be `http:`, `https:`, or `mailto:`.
|
|
452
|
+
* @throws If `url` has any other scheme (or is unparseable).
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```ts
|
|
456
|
+
* // open a docs link in the browser
|
|
457
|
+
* await ctx.ui.openExternal("https://silo.dev/docs");
|
|
458
|
+
*
|
|
459
|
+
* // route a clicked Markdown link safely — bad schemes just reject
|
|
460
|
+
* try {
|
|
461
|
+
* await ctx.ui.openExternal(href);
|
|
462
|
+
* } catch {
|
|
463
|
+
* ctx.ui.notify("warn", "That link can't be opened.");
|
|
464
|
+
* }
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
openExternal(url: string): Promise<void>;
|
|
468
|
+
}
|
|
469
|
+
//# sourceMappingURL=ui-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui-service.d.ts","sourceRoot":"","sources":["../src/ui-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui-service.js","sourceRoot":"","sources":["../src/ui-service.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { type FocusEvent, type KeyboardEvent, type PointerEvent } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The arrow-key axis a {@link useFocusGroup} navigates.
|
|
4
|
+
*
|
|
5
|
+
* - `"vertical"` — ↑/↓ move between items (lists, menus, listboxes).
|
|
6
|
+
* - `"horizontal"` — ←/→ move between items (toolbars, tablists, button groups).
|
|
7
|
+
* - `"grid"` — all four arrows step linearly through the items (a flat picker);
|
|
8
|
+
* `Home`/`End` still jump to the ends regardless of orientation.
|
|
9
|
+
*
|
|
10
|
+
* @category Core Types
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export type FocusGroupOrientation = "vertical" | "horizontal" | "grid";
|
|
14
|
+
/**
|
|
15
|
+
* Options for {@link useFocusGroup}. Describe the set of peer items and what
|
|
16
|
+
* `Enter`/the context-menu key should do; the hook owns the rest (single tab
|
|
17
|
+
* stop, arrow/Home/End movement, the keyboard-only ring).
|
|
18
|
+
*
|
|
19
|
+
* @category Core Types
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export interface FocusGroupOptions {
|
|
23
|
+
/** Number of items in the group. */
|
|
24
|
+
count: number;
|
|
25
|
+
/**
|
|
26
|
+
* The index focus enters on (the host's "first tabbable" lands here, e.g. the
|
|
27
|
+
* selected row). Re-parked here whenever the group doesn't hold focus, so a
|
|
28
|
+
* fresh entry always starts on it. Default `0`. Skipped to the nearest
|
|
29
|
+
* navigable index if {@link FocusGroupOptions.isNavigable | isNavigable} rejects it.
|
|
30
|
+
*/
|
|
31
|
+
start?: number;
|
|
32
|
+
/** Arrow-key axis. Default `"vertical"`. */
|
|
33
|
+
orientation?: FocusGroupOrientation;
|
|
34
|
+
/** Wrap past the ends (default `true`) vs. stop at the first/last item. */
|
|
35
|
+
wrap?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Which indices accept focus; others (separators, headers, disabled rows) are
|
|
38
|
+
* skipped by the arrows, Home/End, and the entry point. Default: all navigable.
|
|
39
|
+
*/
|
|
40
|
+
isNavigable?: (index: number) => boolean;
|
|
41
|
+
/** `Enter`/`Space` on an item. */
|
|
42
|
+
onActivate?: (index: number) => void;
|
|
43
|
+
/**
|
|
44
|
+
* The context-menu key / `Shift`+`F10` on an item; `anchor` is the item's DOM
|
|
45
|
+
* element, so a menu can be positioned against the row.
|
|
46
|
+
*/
|
|
47
|
+
onMenu?: (index: number, anchor: HTMLElement) => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Props {@link useFocusGroup} returns for the group container — spread onto the
|
|
51
|
+
* element wrapping the items.
|
|
52
|
+
*
|
|
53
|
+
* @category Core Types
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export interface FocusGroupContainerProps {
|
|
57
|
+
onBlur: (e: FocusEvent) => void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Props {@link useFocusGroup} returns per item — spread onto item `index`. They
|
|
61
|
+
* carry the single-tab-stop `tabIndex`, the key/focus handlers, and the
|
|
62
|
+
* `data-focus-*` markers the host's CSS styles into the keyboard ring. Your own
|
|
63
|
+
* `role`/`aria-*`/`onClick`/`className` sit alongside them.
|
|
64
|
+
*
|
|
65
|
+
* @category Core Types
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export interface FocusGroupItemProps {
|
|
69
|
+
tabIndex: number;
|
|
70
|
+
ref: (el: HTMLElement | null) => void;
|
|
71
|
+
onKeyDown: (e: KeyboardEvent) => void;
|
|
72
|
+
onFocus: () => void;
|
|
73
|
+
onPointerDown: (e: PointerEvent) => void;
|
|
74
|
+
/** Marks every group item, so the host can reset the native focus outline. */
|
|
75
|
+
"data-focus-item": "";
|
|
76
|
+
/**
|
|
77
|
+
* Present on the active item only while focus is keyboard-driven — the host's
|
|
78
|
+
* CSS keys the ring on it. Absent for pointer focus, matching `:focus-visible`.
|
|
79
|
+
*/
|
|
80
|
+
"data-focus-visible"?: "";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The headless focus-group controller {@link useFocusGroup} returns.
|
|
84
|
+
*
|
|
85
|
+
* @category Core Types
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export interface FocusGroup {
|
|
89
|
+
/** Spread on the group container. */
|
|
90
|
+
containerProps: FocusGroupContainerProps;
|
|
91
|
+
/** Spread on item `index`. */
|
|
92
|
+
getItemProps: (index: number) => FocusGroupItemProps;
|
|
93
|
+
/** The item that currently holds (or, when unfocused, would receive) focus. */
|
|
94
|
+
activeIndex: number;
|
|
95
|
+
/** Imperatively move focus to an item (e.g. `↓` from a search box into a list). */
|
|
96
|
+
focusItem: (index: number) => void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A roving-navigation query for {@link focusGroupNextIndex}.
|
|
100
|
+
*
|
|
101
|
+
* @category Core Types
|
|
102
|
+
* @public
|
|
103
|
+
*/
|
|
104
|
+
export interface FocusGroupNavQuery {
|
|
105
|
+
/** The index focus is on now. */
|
|
106
|
+
current: number;
|
|
107
|
+
/** Number of items. */
|
|
108
|
+
count: number;
|
|
109
|
+
/** The pressed key (`ArrowDown`/`ArrowUp`/`ArrowLeft`/`ArrowRight`/`Home`/`End`). */
|
|
110
|
+
key: string;
|
|
111
|
+
/** Which arrows navigate. */
|
|
112
|
+
orientation: FocusGroupOrientation;
|
|
113
|
+
/** Wrap past the ends vs. stop. */
|
|
114
|
+
wrap: boolean;
|
|
115
|
+
/** Which indices accept focus; others are skipped. */
|
|
116
|
+
isNavigable: (index: number) => boolean;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The index a navigation key moves focus to within a focus group, or `null` when
|
|
120
|
+
* the key isn't a navigation key for this orientation, the list is empty, or no
|
|
121
|
+
* other navigable item exists. Steps over non-navigable items; wraps at the ends
|
|
122
|
+
* when `wrap`, otherwise stops (returns `null`). `Home`/`End` jump to the
|
|
123
|
+
* first/last navigable index regardless of orientation.
|
|
124
|
+
*
|
|
125
|
+
* This is the pure roving-index core that {@link useFocusGroup} runs internally.
|
|
126
|
+
* Reach for it directly only when you **can't** use the hook — e.g. a widget that
|
|
127
|
+
* drives keys from a document-level listener and a state-driven highlight rather
|
|
128
|
+
* than DOM focus (Silo's menus work this way). For an ordinary list/toolbar,
|
|
129
|
+
* prefer {@link useFocusGroup}, which calls this for you.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* const next = focusGroupNextIndex({
|
|
134
|
+
* current: activeIndex, count: items.length, key: e.key,
|
|
135
|
+
* orientation: "vertical", wrap: true,
|
|
136
|
+
* isNavigable: (i) => !items[i].disabled,
|
|
137
|
+
* });
|
|
138
|
+
* if (next !== null) setActiveIndex(next);
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* @category Consumer Services
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
export declare function focusGroupNextIndex(params: FocusGroupNavQuery): number | null;
|
|
145
|
+
/**
|
|
146
|
+
* Whether a keydown should open an item's context menu — the dedicated
|
|
147
|
+
* ContextMenu (Menu/Application) key, or `Shift`+`F10` for keyboards without it.
|
|
148
|
+
*
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
export declare function isContextMenuKey(e: {
|
|
152
|
+
key: string;
|
|
153
|
+
shiftKey: boolean;
|
|
154
|
+
}): boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Headless keyboard navigation for a **focus group** — a set of peer items that
|
|
157
|
+
* share a single tab stop and move with the arrow keys (a list, listbox, menu,
|
|
158
|
+
* toolbar, tablist, radio group, or flat grid). It owns, once and correctly, the
|
|
159
|
+
* mechanics every such widget needs:
|
|
160
|
+
*
|
|
161
|
+
* - a single-tab-stop `tabIndex` (one item tabbable, the rest `-1`), so the group
|
|
162
|
+
* is one Tab stop and the host's "focus the first tabbable" entry lands on
|
|
163
|
+
* {@link FocusGroupOptions.start | start};
|
|
164
|
+
* - Arrow / Home / End movement (per
|
|
165
|
+
* {@link FocusGroupOptions.orientation | orientation}, wrapping or stopping per
|
|
166
|
+
* {@link FocusGroupOptions.wrap | wrap}, skipping non-navigable items);
|
|
167
|
+
* - `Enter`/`Space` → {@link FocusGroupOptions.onActivate | onActivate}, the
|
|
168
|
+
* context-menu key / `Shift`+`F10` → {@link FocusGroupOptions.onMenu | onMenu};
|
|
169
|
+
* - a **WebKit-safe, keyboard-only focus ring**: it flags the active item with a
|
|
170
|
+
* `data-focus-visible` attribute (state-driven, because WebKit won't repaint
|
|
171
|
+
* `:focus` for the programmatic focus the host's region cycle performs), and
|
|
172
|
+
* the host ships the ring CSS keyed on that attribute — so every group's ring
|
|
173
|
+
* is identical and correct without the author touching it.
|
|
174
|
+
*
|
|
175
|
+
* You keep the markup and semantics (`role`, `aria-*`, `onClick`, styling); the
|
|
176
|
+
* hook supplies behavior. Spread {@link FocusGroup.containerProps | containerProps}
|
|
177
|
+
* on the wrapper and {@link FocusGroup.getItemProps | getItemProps(i)} on each
|
|
178
|
+
* item. The index is clamped when {@link FocusGroupOptions.count | count} changes,
|
|
179
|
+
* so live-filtering a list is safe.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```tsx
|
|
183
|
+
* const group = useFocusGroup({
|
|
184
|
+
* count: items.length,
|
|
185
|
+
* start: activeIndex,
|
|
186
|
+
* onActivate: (i) => select(items[i].id),
|
|
187
|
+
* onMenu: (i, anchor) => showMenu(items[i], anchor),
|
|
188
|
+
* });
|
|
189
|
+
* return (
|
|
190
|
+
* <ul {...group.containerProps}>
|
|
191
|
+
* {items.map((it, i) => (
|
|
192
|
+
* <li key={it.id} {...group.getItemProps(i)}>{it.label}</li>
|
|
193
|
+
* ))}
|
|
194
|
+
* </ul>
|
|
195
|
+
* );
|
|
196
|
+
* ```
|
|
197
|
+
*
|
|
198
|
+
* @category Consumer Services
|
|
199
|
+
* @public
|
|
200
|
+
*/
|
|
201
|
+
export declare function useFocusGroup(options: FocusGroupOptions): FocusGroup;
|
|
202
|
+
//# sourceMappingURL=use-focus-group.d.ts.map
|