@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 @@
|
|
|
1
|
+
{"version":3,"file":"use-focus-group.d.ts","sourceRoot":"","sources":["../src/use-focus-group.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,OAAO,CAAC;AAEf;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,YAAY,GAAG,MAAM,CAAC;AAEvE;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,2EAA2E;IAC3E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,kCAAkC;IAClC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CACvD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IACtC,SAAS,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IACtC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,aAAa,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,8EAA8E;IAC9E,iBAAiB,EAAE,EAAE,CAAC;IACtB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,EAAE,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,qCAAqC;IACrC,cAAc,EAAE,wBAAwB,CAAC;IACzC,8BAA8B;IAC9B,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,mBAAmB,CAAC;IACrD,+EAA+E;IAC/E,WAAW,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAsBD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,WAAW,EAAE,qBAAqB,CAAC;IACnC,mCAAmC;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,sDAAsD;IACtD,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI,CA6B7E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAEV;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAiHpE"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, } from "react";
|
|
2
|
+
const clamp = (n, max) => Math.max(0, Math.min(n, max));
|
|
3
|
+
/** First navigable index at or after scanning the whole list, or `null`. */
|
|
4
|
+
function firstNavigable(count, isNavigable) {
|
|
5
|
+
for (let i = 0; i < count; i++)
|
|
6
|
+
if (isNavigable(i))
|
|
7
|
+
return i;
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
/** Last navigable index, or `null`. */
|
|
11
|
+
function lastNavigable(count, isNavigable) {
|
|
12
|
+
for (let i = count - 1; i >= 0; i--)
|
|
13
|
+
if (isNavigable(i))
|
|
14
|
+
return i;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The index a navigation key moves focus to within a focus group, or `null` when
|
|
19
|
+
* the key isn't a navigation key for this orientation, the list is empty, or no
|
|
20
|
+
* other navigable item exists. Steps over non-navigable items; wraps at the ends
|
|
21
|
+
* when `wrap`, otherwise stops (returns `null`). `Home`/`End` jump to the
|
|
22
|
+
* first/last navigable index regardless of orientation.
|
|
23
|
+
*
|
|
24
|
+
* This is the pure roving-index core that {@link useFocusGroup} runs internally.
|
|
25
|
+
* Reach for it directly only when you **can't** use the hook — e.g. a widget that
|
|
26
|
+
* drives keys from a document-level listener and a state-driven highlight rather
|
|
27
|
+
* than DOM focus (Silo's menus work this way). For an ordinary list/toolbar,
|
|
28
|
+
* prefer {@link useFocusGroup}, which calls this for you.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const next = focusGroupNextIndex({
|
|
33
|
+
* current: activeIndex, count: items.length, key: e.key,
|
|
34
|
+
* orientation: "vertical", wrap: true,
|
|
35
|
+
* isNavigable: (i) => !items[i].disabled,
|
|
36
|
+
* });
|
|
37
|
+
* if (next !== null) setActiveIndex(next);
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @category Consumer Services
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export function focusGroupNextIndex(params) {
|
|
44
|
+
const { current, count, key, orientation, wrap, isNavigable } = params;
|
|
45
|
+
if (count <= 0)
|
|
46
|
+
return null;
|
|
47
|
+
if (key === "Home")
|
|
48
|
+
return firstNavigable(count, isNavigable);
|
|
49
|
+
if (key === "End")
|
|
50
|
+
return lastNavigable(count, isNavigable);
|
|
51
|
+
const vertical = orientation === "vertical" || orientation === "grid";
|
|
52
|
+
const horizontal = orientation === "horizontal" || orientation === "grid";
|
|
53
|
+
let dir = 0;
|
|
54
|
+
if ((vertical && key === "ArrowDown") || (horizontal && key === "ArrowRight"))
|
|
55
|
+
dir = 1;
|
|
56
|
+
else if ((vertical && key === "ArrowUp") ||
|
|
57
|
+
(horizontal && key === "ArrowLeft"))
|
|
58
|
+
dir = -1;
|
|
59
|
+
if (dir === 0)
|
|
60
|
+
return null;
|
|
61
|
+
let i = current;
|
|
62
|
+
for (let n = 0; n < count; n++) {
|
|
63
|
+
i += dir;
|
|
64
|
+
if (i < 0 || i >= count) {
|
|
65
|
+
if (!wrap)
|
|
66
|
+
return null;
|
|
67
|
+
i = (i + count) % count;
|
|
68
|
+
}
|
|
69
|
+
if (i === current)
|
|
70
|
+
return null; // came all the way around — nothing else navigable
|
|
71
|
+
if (isNavigable(i))
|
|
72
|
+
return i;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether a keydown should open an item's context menu — the dedicated
|
|
78
|
+
* ContextMenu (Menu/Application) key, or `Shift`+`F10` for keyboards without it.
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export function isContextMenuKey(e) {
|
|
83
|
+
return e.key === "ContextMenu" || (e.shiftKey && e.key === "F10");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Headless keyboard navigation for a **focus group** — a set of peer items that
|
|
87
|
+
* share a single tab stop and move with the arrow keys (a list, listbox, menu,
|
|
88
|
+
* toolbar, tablist, radio group, or flat grid). It owns, once and correctly, the
|
|
89
|
+
* mechanics every such widget needs:
|
|
90
|
+
*
|
|
91
|
+
* - a single-tab-stop `tabIndex` (one item tabbable, the rest `-1`), so the group
|
|
92
|
+
* is one Tab stop and the host's "focus the first tabbable" entry lands on
|
|
93
|
+
* {@link FocusGroupOptions.start | start};
|
|
94
|
+
* - Arrow / Home / End movement (per
|
|
95
|
+
* {@link FocusGroupOptions.orientation | orientation}, wrapping or stopping per
|
|
96
|
+
* {@link FocusGroupOptions.wrap | wrap}, skipping non-navigable items);
|
|
97
|
+
* - `Enter`/`Space` → {@link FocusGroupOptions.onActivate | onActivate}, the
|
|
98
|
+
* context-menu key / `Shift`+`F10` → {@link FocusGroupOptions.onMenu | onMenu};
|
|
99
|
+
* - a **WebKit-safe, keyboard-only focus ring**: it flags the active item with a
|
|
100
|
+
* `data-focus-visible` attribute (state-driven, because WebKit won't repaint
|
|
101
|
+
* `:focus` for the programmatic focus the host's region cycle performs), and
|
|
102
|
+
* the host ships the ring CSS keyed on that attribute — so every group's ring
|
|
103
|
+
* is identical and correct without the author touching it.
|
|
104
|
+
*
|
|
105
|
+
* You keep the markup and semantics (`role`, `aria-*`, `onClick`, styling); the
|
|
106
|
+
* hook supplies behavior. Spread {@link FocusGroup.containerProps | containerProps}
|
|
107
|
+
* on the wrapper and {@link FocusGroup.getItemProps | getItemProps(i)} on each
|
|
108
|
+
* item. The index is clamped when {@link FocusGroupOptions.count | count} changes,
|
|
109
|
+
* so live-filtering a list is safe.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```tsx
|
|
113
|
+
* const group = useFocusGroup({
|
|
114
|
+
* count: items.length,
|
|
115
|
+
* start: activeIndex,
|
|
116
|
+
* onActivate: (i) => select(items[i].id),
|
|
117
|
+
* onMenu: (i, anchor) => showMenu(items[i], anchor),
|
|
118
|
+
* });
|
|
119
|
+
* return (
|
|
120
|
+
* <ul {...group.containerProps}>
|
|
121
|
+
* {items.map((it, i) => (
|
|
122
|
+
* <li key={it.id} {...group.getItemProps(i)}>{it.label}</li>
|
|
123
|
+
* ))}
|
|
124
|
+
* </ul>
|
|
125
|
+
* );
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @category Consumer Services
|
|
129
|
+
* @public
|
|
130
|
+
*/
|
|
131
|
+
export function useFocusGroup(options) {
|
|
132
|
+
const { count, start = 0, orientation = "vertical", wrap = true, isNavigable = () => true, onActivate, onMenu, } = options;
|
|
133
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
134
|
+
// Focus is somewhere inside the group (drives re-parking on the active row).
|
|
135
|
+
const [focused, setFocused] = useState(false);
|
|
136
|
+
// Focus arrived/moved via the keyboard (drives the ring), à la `:focus-visible`.
|
|
137
|
+
const [visible, setVisible] = useState(false);
|
|
138
|
+
// The most recent focus was preceded by a pointer-down on an item — so it's a
|
|
139
|
+
// mouse focus and must NOT show the ring. Read+reset on the following onFocus.
|
|
140
|
+
const pointerOrigin = useRef(false);
|
|
141
|
+
const itemEls = useRef(new Map());
|
|
142
|
+
// Read latest options in stable handlers without re-creating them each render.
|
|
143
|
+
const opts = useRef({
|
|
144
|
+
count,
|
|
145
|
+
orientation,
|
|
146
|
+
wrap,
|
|
147
|
+
isNavigable,
|
|
148
|
+
onActivate,
|
|
149
|
+
onMenu,
|
|
150
|
+
});
|
|
151
|
+
opts.current = { count, orientation, wrap, isNavigable, onActivate, onMenu };
|
|
152
|
+
// Park the active index on `start` (clamped, and nudged to a navigable item)
|
|
153
|
+
// whenever the group isn't focused, so the host's entry lands there; while
|
|
154
|
+
// focused, just keep the index in range as items come and go.
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
setActiveIndex((i) => {
|
|
157
|
+
if (count <= 0)
|
|
158
|
+
return 0;
|
|
159
|
+
if (focused)
|
|
160
|
+
return Math.min(i, count - 1);
|
|
161
|
+
const at = clamp(start, count - 1);
|
|
162
|
+
return isNavigable(at) ? at : (firstNavigable(count, isNavigable) ?? at);
|
|
163
|
+
// isNavigable identity changes per render; `start`/`count`/`focused` are the
|
|
164
|
+
// real inputs — re-running on isNavigable alone would thrash focus.
|
|
165
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
166
|
+
});
|
|
167
|
+
}, [start, focused, count]);
|
|
168
|
+
const focusItem = useCallback((index) => {
|
|
169
|
+
itemEls.current.get(index)?.focus();
|
|
170
|
+
}, []);
|
|
171
|
+
const containerProps = {
|
|
172
|
+
onBlur: (e) => {
|
|
173
|
+
// Drop focus state only when focus leaves the whole group (not when it
|
|
174
|
+
// moves between two items).
|
|
175
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
176
|
+
setFocused(false);
|
|
177
|
+
setVisible(false);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
const getItemProps = (index) => ({
|
|
182
|
+
tabIndex: index === activeIndex ? 0 : -1,
|
|
183
|
+
ref: (el) => {
|
|
184
|
+
if (el)
|
|
185
|
+
itemEls.current.set(index, el);
|
|
186
|
+
else
|
|
187
|
+
itemEls.current.delete(index);
|
|
188
|
+
},
|
|
189
|
+
onPointerDown: () => {
|
|
190
|
+
pointerOrigin.current = true;
|
|
191
|
+
},
|
|
192
|
+
onFocus: () => {
|
|
193
|
+
setFocused(true);
|
|
194
|
+
setActiveIndex(index);
|
|
195
|
+
// Keyboard/programmatic focus shows the ring; a pointer-driven focus doesn't.
|
|
196
|
+
setVisible(!pointerOrigin.current);
|
|
197
|
+
pointerOrigin.current = false;
|
|
198
|
+
},
|
|
199
|
+
onKeyDown: (e) => {
|
|
200
|
+
const o = opts.current;
|
|
201
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
202
|
+
if (!o.onActivate)
|
|
203
|
+
return;
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
setVisible(true);
|
|
206
|
+
o.onActivate(index);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (isContextMenuKey(e)) {
|
|
210
|
+
if (!o.onMenu)
|
|
211
|
+
return;
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
setVisible(true);
|
|
214
|
+
o.onMenu(index, e.currentTarget);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const next = focusGroupNextIndex({
|
|
218
|
+
current: index,
|
|
219
|
+
count: o.count,
|
|
220
|
+
key: e.key,
|
|
221
|
+
orientation: o.orientation,
|
|
222
|
+
wrap: o.wrap,
|
|
223
|
+
isNavigable: o.isNavigable,
|
|
224
|
+
});
|
|
225
|
+
if (next === null)
|
|
226
|
+
return;
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
setVisible(true);
|
|
229
|
+
focusItem(next);
|
|
230
|
+
},
|
|
231
|
+
"data-focus-item": "",
|
|
232
|
+
...(visible && index === activeIndex ? { "data-focus-visible": "" } : {}),
|
|
233
|
+
});
|
|
234
|
+
return { containerProps, getItemProps, activeIndex, focusItem };
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=use-focus-group.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-focus-group.js","sourceRoot":"","sources":["../src/use-focus-group.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Disposable } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* The minimal reactive contract every stateful `ctx` service satisfies: a
|
|
4
|
+
* `getState` returning a stable, frozen value and a `subscribe` that fires
|
|
5
|
+
* on change. This is exactly what React's `useSyncExternalStore` needs — and
|
|
6
|
+
* the only shape {@link useServiceState} requires.
|
|
7
|
+
*
|
|
8
|
+
* @category Consumer Services
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export interface ReactiveService<T> {
|
|
12
|
+
getState(): T;
|
|
13
|
+
subscribe(listener: (s: T) => void): Disposable;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Subscribe a React component to a `ctx` service's reactive state. Returns the
|
|
17
|
+
* service's current state and re-renders when it changes — the one blessed
|
|
18
|
+
* way to read service state in an extension. Use it for every domain
|
|
19
|
+
* ({@link ExtensionContext.workspaces | workspaces},
|
|
20
|
+
* {@link ExtensionContext.layout | layout},
|
|
21
|
+
* {@link ExtensionContext.theme | theme}, …) rather than re-implementing the
|
|
22
|
+
* `useSyncExternalStore` boilerplate per call site.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* function Panel({ ctx }: { ctx: ExtensionContext }) {
|
|
27
|
+
* const ws = useServiceState(ctx.workspaces);
|
|
28
|
+
* return <span>{ws.open.length} open workspaces</span>;
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @category Consumer Services
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export declare function useServiceState<T>(service: ReactiveService<T>): T;
|
|
36
|
+
//# sourceMappingURL=use-service-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-service-state.d.ts","sourceRoot":"","sources":["../src/use-service-state.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,QAAQ,IAAI,CAAC,CAAC;IACd,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,GAAG,UAAU,CAAC;CACjD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,CAKjE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Subscribe a React component to a `ctx` service's reactive state. Returns the
|
|
4
|
+
* service's current state and re-renders when it changes — the one blessed
|
|
5
|
+
* way to read service state in an extension. Use it for every domain
|
|
6
|
+
* ({@link ExtensionContext.workspaces | workspaces},
|
|
7
|
+
* {@link ExtensionContext.layout | layout},
|
|
8
|
+
* {@link ExtensionContext.theme | theme}, …) rather than re-implementing the
|
|
9
|
+
* `useSyncExternalStore` boilerplate per call site.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* function Panel({ ctx }: { ctx: ExtensionContext }) {
|
|
14
|
+
* const ws = useServiceState(ctx.workspaces);
|
|
15
|
+
* return <span>{ws.open.length} open workspaces</span>;
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @category Consumer Services
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export function useServiceState(service) {
|
|
23
|
+
return useSyncExternalStore(useCallback((cb) => service.subscribe(cb).dispose, [service]), service.getState);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=use-service-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-service-state.js","sourceRoot":"","sources":["../src/use-service-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAiB1D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,eAAe,CAAI,OAA2B;IAC5D,OAAO,oBAAoB,CACzB,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,EAC7D,OAAO,CAAC,QAAQ,CACjB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Disposable } from "./types";
|
|
2
|
+
import type { Workspace } from "./domain-types";
|
|
3
|
+
export type { Workspace, TerminalRecord } from "./domain-types";
|
|
4
|
+
/**
|
|
5
|
+
* An immutable, frozen view of workspace state, returned by
|
|
6
|
+
* {@link WorkspaceService.getState} and delivered to subscribers — read
|
|
7
|
+
* access without a Valtio dependency.
|
|
8
|
+
*
|
|
9
|
+
* @category Consumer Services
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export interface WorkspaceState {
|
|
13
|
+
/** All workspaces, in user-defined order. */
|
|
14
|
+
all: readonly Workspace[];
|
|
15
|
+
/** Workspaces where closedAt is null/undefined, in user-defined order. */
|
|
16
|
+
open: readonly Workspace[];
|
|
17
|
+
/** Workspaces where closedAt is set, sorted by closedAt descending. */
|
|
18
|
+
closed: readonly Workspace[];
|
|
19
|
+
activeId: string | null;
|
|
20
|
+
/** True once the persisted state has been loaded into the store. */
|
|
21
|
+
hydrated: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Input for {@link WorkspaceService.create}.
|
|
25
|
+
*
|
|
26
|
+
* @category Consumer Services
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export interface CreateWorkspaceInput {
|
|
30
|
+
/** Absolute path of the workspace's primary folder. */
|
|
31
|
+
folder: string;
|
|
32
|
+
/** Display name for the workspace. */
|
|
33
|
+
name: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Consumer API for workspace state, exposed as {@link ExtensionContext.workspaces}.
|
|
37
|
+
* Read via {@link WorkspaceService.getState | getState} /
|
|
38
|
+
* {@link WorkspaceService.subscribe | subscribe}; drive via the create/rename/
|
|
39
|
+
* close methods. Opening editor tabs lives on {@link ExtensionContext.editors},
|
|
40
|
+
* not here.
|
|
41
|
+
*
|
|
42
|
+
* @category Consumer Services
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export interface WorkspaceService {
|
|
46
|
+
/** Current frozen view of workspace state. */
|
|
47
|
+
getState(): WorkspaceState;
|
|
48
|
+
subscribe(listener: (s: WorkspaceState) => void): Disposable;
|
|
49
|
+
/**
|
|
50
|
+
* The workspace with this id, or `undefined`. A one-shot lookup for event
|
|
51
|
+
* handlers; for reactive reads use {@link useServiceState} over the state.
|
|
52
|
+
*/
|
|
53
|
+
get(id: string): Workspace | undefined;
|
|
54
|
+
/** Show a folder picker and create a workspace from the chosen folder. */
|
|
55
|
+
createFromFolderPicker(): Promise<Workspace | null>;
|
|
56
|
+
create(input: CreateWorkspaceInput): Workspace;
|
|
57
|
+
rename(id: string, name: string): void;
|
|
58
|
+
reorder(from: string, to: string, position: "before" | "after"): void;
|
|
59
|
+
/** Activate (and reopen if closed). */
|
|
60
|
+
activate(id: string): void;
|
|
61
|
+
/** Soft close — workspace stays saved but is hidden from the active list. */
|
|
62
|
+
close(id: string): void;
|
|
63
|
+
/** Reverse of close. */
|
|
64
|
+
reopen(id: string): void;
|
|
65
|
+
/** Add an extra folder to a workspace (no-op if already present or is the primary). */
|
|
66
|
+
addFolder(id: string, folder: string): void;
|
|
67
|
+
/** Remove an extra folder from a workspace. */
|
|
68
|
+
removeFolder(id: string, folder: string): void;
|
|
69
|
+
/** Hard delete — permanent removal. */
|
|
70
|
+
delete(id: string): void;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=workspace-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-service.d.ts","sourceRoot":"","sources":["../src/workspace-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhE;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,GAAG,EAAE,SAAS,SAAS,EAAE,CAAC;IAC1B,0EAA0E;IAC1E,IAAI,EAAE,SAAS,SAAS,EAAE,CAAC;IAC3B,uEAAuE;IACvE,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,8CAA8C;IAC9C,QAAQ,IAAI,cAAc,CAAC;IAC3B,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,GAAG,UAAU,CAAC;IAC7D;;;OAGG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;IACvC,0EAA0E;IAC1E,sBAAsB,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACpD,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAC/C,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IACtE,uCAAuC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,wBAAwB;IACxB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uFAAuF;IACvF,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,+CAA+C;IAC/C,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,uCAAuC;IACvC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-service.js","sourceRoot":"","sources":["../src/workspace-service.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@silo-code/sdk",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "The public, types-first SDK for building Silo extensions — the only surface third-party extensions import.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Dave Weaver",
|
|
7
|
+
"homepage": "https://github.com/silo-code/silo/tree/main/packages/sdk#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/silo-code/silo.git",
|
|
11
|
+
"directory": "packages/sdk"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"silo",
|
|
15
|
+
"silo-code",
|
|
16
|
+
"extension",
|
|
17
|
+
"sdk",
|
|
18
|
+
"editor"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": "^19.1.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"dockview": "~6.3.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/react": "^19.1.8",
|
|
43
|
+
"typescript": "~5.8.3",
|
|
44
|
+
"vitest": "^4.1.7"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -p tsconfig.build.json",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest"
|
|
50
|
+
},
|
|
51
|
+
"main": "./dist/index.js",
|
|
52
|
+
"module": "./dist/index.js",
|
|
53
|
+
"types": "./dist/index.d.ts"
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal context-keys store. Menu items, keybindings, and (later) view
|
|
3
|
+
* contributions can declare `when` clauses that read these values; the host
|
|
4
|
+
* re-evaluates the clauses whenever a key changes and updates UI state
|
|
5
|
+
* (enabled/disabled menu items, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Keys are added as needed. Start narrow: only the keys we have a real use
|
|
8
|
+
* for. Avoid the temptation to mirror every piece of app state into here.
|
|
9
|
+
*
|
|
10
|
+
* @category Core Types
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export interface ContextKeys {
|
|
14
|
+
/** id of the viewer rendered by the dock's active panel, or null. */
|
|
15
|
+
activeViewerId: string | null;
|
|
16
|
+
/** editorId of the active dock panel if it's an editor panel, or null. */
|
|
17
|
+
activeEditorId: string | null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { Disposable } from "./types";
|
|
3
|
+
|
|
4
|
+
// `ctx.dnd` — first-class drag-and-drop for extensions (public contract). The
|
|
5
|
+
// host owns the affordance (the floating drag chip + paste-mode overlay) and
|
|
6
|
+
// the modifier-mode resolution; the implementation lives in the extension host.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Well-known MIME types for Silo drag payloads. Use these constants (rather than
|
|
10
|
+
* raw strings) so drags interoperate across extensions and built-ins.
|
|
11
|
+
*
|
|
12
|
+
* @category Core Types
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export const DND_MIME = {
|
|
16
|
+
/** Absolute filesystem path of a file or directory being dragged. */
|
|
17
|
+
filePath: "application/x-silo-file-path",
|
|
18
|
+
/** Plain-text payload (mirrors the path today). */
|
|
19
|
+
text: "text/plain",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A MIME type from the {@link DND_MIME} vocabulary.
|
|
24
|
+
*
|
|
25
|
+
* @category Core Types
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
export type DndMime = (typeof DND_MIME)[keyof typeof DND_MIME];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One typed item carried by a drag — a MIME type plus its string payload.
|
|
32
|
+
*
|
|
33
|
+
* @category Core Types
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
export interface DndItem {
|
|
37
|
+
/** The item's MIME type; use a {@link DND_MIME} constant for interop. */
|
|
38
|
+
mime: string;
|
|
39
|
+
/** The string payload (e.g. an absolute path for {@link DND_MIME.filePath}). */
|
|
40
|
+
value: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What a drag carries and how its chip should read, passed to
|
|
45
|
+
* {@link DndService.beginDrag}.
|
|
46
|
+
*
|
|
47
|
+
* @category Core Types
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export interface DragInit {
|
|
51
|
+
/** Typed payload items written onto the native `dataTransfer`. */
|
|
52
|
+
items: DndItem[];
|
|
53
|
+
/** Label shown in the floating drag chip (e.g. the file name). */
|
|
54
|
+
label: string;
|
|
55
|
+
/** `dataTransfer.effectAllowed`; defaults to `"copyMove"`. */
|
|
56
|
+
effect?: "copy" | "move" | "copyMove";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The interaction mode resolved from held modifiers at hover/drop time:
|
|
61
|
+
* `"copy"` is the default (e.g. open / split); `"paste"` is Shift-held (e.g.
|
|
62
|
+
* insert path at caret / paste into the terminal). Resolved robustly even when
|
|
63
|
+
* WebKit suppresses key events mid-drag.
|
|
64
|
+
*
|
|
65
|
+
* @category Core Types
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export type DndMode = "copy" | "paste";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Context delivered to a {@link DropTargetHandlers} callback for a drag over or
|
|
72
|
+
* drop on a registered target.
|
|
73
|
+
*
|
|
74
|
+
* @category Core Types
|
|
75
|
+
* @public
|
|
76
|
+
*/
|
|
77
|
+
export interface DropContext {
|
|
78
|
+
/**
|
|
79
|
+
* Typed items read from the native `dataTransfer`. Populated on `drop`;
|
|
80
|
+
* during `dragover` the browser exposes only MIME *types* (not values), so
|
|
81
|
+
* this may be empty there — branch on {@link DropContext.mode} / the target's
|
|
82
|
+
* `accepts` instead.
|
|
83
|
+
*/
|
|
84
|
+
items: DndItem[];
|
|
85
|
+
/** The resolved modifier mode (Shift ⇒ `"paste"`). */
|
|
86
|
+
mode: DndMode;
|
|
87
|
+
/** Pointer X in client coordinates. */
|
|
88
|
+
clientX: number;
|
|
89
|
+
/** Pointer Y in client coordinates. */
|
|
90
|
+
clientY: number;
|
|
91
|
+
/** The underlying native event (escape hatch for advanced callers). */
|
|
92
|
+
nativeEvent: DragEvent;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handlers for a target registered via {@link DndService.registerDropTarget}.
|
|
97
|
+
*
|
|
98
|
+
* @category Core Types
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
export interface DropTargetHandlers {
|
|
102
|
+
/** MIME types this target consumes; a drag is accepted if it carries one. */
|
|
103
|
+
accepts: string[];
|
|
104
|
+
/**
|
|
105
|
+
* Called on a matching drop. Return `true` if you handled it — the host then
|
|
106
|
+
* calls `preventDefault()` + `stopPropagation()` so the drop does **not**
|
|
107
|
+
* fall through to other targets (e.g. the center dock opening a new pane).
|
|
108
|
+
*/
|
|
109
|
+
onDrop(ctx: DropContext): boolean | void;
|
|
110
|
+
/**
|
|
111
|
+
* Optional: called on each accepted drag-over (drive hover styling). Return a
|
|
112
|
+
* drop effect to set the cursor, or nothing to leave it. The host already
|
|
113
|
+
* calls `preventDefault()` so the drop is allowed.
|
|
114
|
+
*/
|
|
115
|
+
onDragOver?(ctx: DropContext): "copy" | "move" | "none" | void;
|
|
116
|
+
/**
|
|
117
|
+
* Optional: the pointer left the target (clear hover styling). Receives the
|
|
118
|
+
* native event so callers can ignore leaves into a descendant
|
|
119
|
+
* (`el.contains(e.relatedTarget)`).
|
|
120
|
+
*/
|
|
121
|
+
onDragLeave?(e: DragEvent): void;
|
|
122
|
+
/**
|
|
123
|
+
* Attach listeners in the capture phase so this target intercepts before
|
|
124
|
+
* bubble-phase handlers (e.g. the center dock). Defaults to `false`.
|
|
125
|
+
*/
|
|
126
|
+
capture?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The drag-and-drop domain, exposed as {@link ExtensionContext.dnd}. Be a drag
|
|
131
|
+
* source with {@link DndService.beginDrag} and a drop target with
|
|
132
|
+
* {@link DndService.registerDropTarget}; payloads are typed via {@link DND_MIME}.
|
|
133
|
+
*
|
|
134
|
+
* @category Consumer Services
|
|
135
|
+
* @public
|
|
136
|
+
*/
|
|
137
|
+
export interface DndService {
|
|
138
|
+
/**
|
|
139
|
+
* Begin a drag from inside a `dragstart` handler: writes the typed
|
|
140
|
+
* {@link DragInit.items} onto the native `dataTransfer`, hides the native
|
|
141
|
+
* drag preview, and starts the floating chip + paste-mode overlay affordance.
|
|
142
|
+
* Must be called synchronously within the `dragstart` event.
|
|
143
|
+
*/
|
|
144
|
+
beginDrag(event: DragEvent | React.DragEvent, init: DragInit): void;
|
|
145
|
+
/**
|
|
146
|
+
* Register `el` as a drop target. The host attaches the drag listeners,
|
|
147
|
+
* resolves the modifier {@link DndMode}, and delivers a {@link DropContext}.
|
|
148
|
+
* Returns a {@link Disposable} that removes the listeners.
|
|
149
|
+
*/
|
|
150
|
+
registerDropTarget(el: HTMLElement, handlers: DropTargetHandlers): Disposable;
|
|
151
|
+
}
|