@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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/context-keys.d.ts +19 -0
  4. package/dist/context-keys.d.ts.map +1 -0
  5. package/dist/context-keys.js +2 -0
  6. package/dist/context-keys.js.map +1 -0
  7. package/dist/dnd-service.d.ts +140 -0
  8. package/dist/dnd-service.d.ts.map +1 -0
  9. package/dist/dnd-service.js +17 -0
  10. package/dist/dnd-service.js.map +1 -0
  11. package/dist/domain-types.d.ts +237 -0
  12. package/dist/domain-types.d.ts.map +1 -0
  13. package/dist/domain-types.js +11 -0
  14. package/dist/domain-types.js.map +1 -0
  15. package/dist/editor-service.d.ts +175 -0
  16. package/dist/editor-service.d.ts.map +1 -0
  17. package/dist/editor-service.js +2 -0
  18. package/dist/editor-service.js.map +1 -0
  19. package/dist/extension-storage.d.ts +26 -0
  20. package/dist/extension-storage.d.ts.map +1 -0
  21. package/dist/extension-storage.js +2 -0
  22. package/dist/extension-storage.js.map +1 -0
  23. package/dist/file-service.d.ts +84 -0
  24. package/dist/file-service.d.ts.map +1 -0
  25. package/dist/file-service.js +2 -0
  26. package/dist/file-service.js.map +1 -0
  27. package/dist/index.d.ts +32 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +22 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/layout-service.d.ts +46 -0
  32. package/dist/layout-service.d.ts.map +1 -0
  33. package/dist/layout-service.js +2 -0
  34. package/dist/layout-service.js.map +1 -0
  35. package/dist/permissions.d.ts +41 -0
  36. package/dist/permissions.d.ts.map +1 -0
  37. package/dist/permissions.js +40 -0
  38. package/dist/permissions.js.map +1 -0
  39. package/dist/process-service.d.ts +132 -0
  40. package/dist/process-service.d.ts.map +1 -0
  41. package/dist/process-service.js +2 -0
  42. package/dist/process-service.js.map +1 -0
  43. package/dist/terminal-service.d.ts +38 -0
  44. package/dist/terminal-service.d.ts.map +1 -0
  45. package/dist/terminal-service.js +2 -0
  46. package/dist/terminal-service.js.map +1 -0
  47. package/dist/theme-service.d.ts +87 -0
  48. package/dist/theme-service.d.ts.map +1 -0
  49. package/dist/theme-service.js +2 -0
  50. package/dist/theme-service.js.map +1 -0
  51. package/dist/types.d.ts +495 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +2 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/ui-service.d.ts +469 -0
  56. package/dist/ui-service.d.ts.map +1 -0
  57. package/dist/ui-service.js +2 -0
  58. package/dist/ui-service.js.map +1 -0
  59. package/dist/use-focus-group.d.ts +202 -0
  60. package/dist/use-focus-group.d.ts.map +1 -0
  61. package/dist/use-focus-group.js +236 -0
  62. package/dist/use-focus-group.js.map +1 -0
  63. package/dist/use-service-state.d.ts +36 -0
  64. package/dist/use-service-state.d.ts.map +1 -0
  65. package/dist/use-service-state.js +25 -0
  66. package/dist/use-service-state.js.map +1 -0
  67. package/dist/workspace-service.d.ts +72 -0
  68. package/dist/workspace-service.d.ts.map +1 -0
  69. package/dist/workspace-service.js +2 -0
  70. package/dist/workspace-service.js.map +1 -0
  71. package/package.json +54 -0
  72. package/src/context-keys.ts +18 -0
  73. package/src/dnd-service.ts +151 -0
  74. package/src/domain-types.ts +252 -0
  75. package/src/editor-service.ts +196 -0
  76. package/src/extension-storage.ts +25 -0
  77. package/src/file-service.ts +90 -0
  78. package/src/index.ts +151 -0
  79. package/src/layout-service.ts +49 -0
  80. package/src/permissions.ts +55 -0
  81. package/src/process-service.ts +143 -0
  82. package/src/terminal-service.ts +41 -0
  83. package/src/theme-service.ts +102 -0
  84. package/src/types.ts +513 -0
  85. package/src/ui-service.ts +487 -0
  86. package/src/use-focus-group.test.ts +168 -0
  87. package/src/use-focus-group.ts +382 -0
  88. package/src/use-service-state.ts +43 -0
  89. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ui-service.js.map
@@ -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