@medeo-plugin/sdk 0.0.0-stage → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,424 @@
1
+ export { METHODS, LIMITS } from "./registry.js";
2
+ /** The `api_version` this SDK targets; `spec/api-v1.json` carries the same number. */
3
+ export const API_VERSION = 1;
4
+ /** Messages between this plugin's background worker and its own windows
5
+ * (64 KiB each, JSON). A worker message reaches every open window; a window
6
+ * message reaches the worker. Nothing is persisted or sent elsewhere. */
7
+ export const channel = {
8
+ post: (message) => globalThis.medeoPlugin.post(message),
9
+ onMessage: (callback) => globalThis.medeoPlugin.onMessage(m => callback(m)),
10
+ };
11
+ export const events = {
12
+ on: (callback) => globalThis.medeoPlugin.onEvent(callback),
13
+ /** Call `pull` now and again whenever the channel signals new entries. A
14
+ * failed pull (e.g. a permission withdrawn meanwhile) waits for the next signal. */
15
+ onSignal(channel, pull) {
16
+ const run = () => { void Promise.resolve().then(pull).catch(() => { }); };
17
+ const off = globalThis.medeoPlugin.onEvent(event => { if (event.type === 'signal' && event.channel === channel)
18
+ run(); });
19
+ run();
20
+ return off;
21
+ },
22
+ };
23
+ /** Resolves on the next event for `id`, or after `fallbackMs` as a safety net. */
24
+ function nextChange(id, fallbackMs) {
25
+ let off = () => { };
26
+ const promise = new Promise(resolve => {
27
+ const timer = setTimeout(resolve, fallbackMs);
28
+ const stop = globalThis.medeoPlugin.onEvent(event => { if (event.type === 'operation' && event.id === id) {
29
+ clearTimeout(timer);
30
+ resolve();
31
+ } });
32
+ off = () => { clearTimeout(timer); stop(); };
33
+ });
34
+ return { promise, off };
35
+ }
36
+ export const storage = {
37
+ get: (key) => globalThis.medeoPlugin.invoke('storage.get', { key }),
38
+ remove: (key) => globalThis.medeoPlugin.invoke('storage.remove', { key }),
39
+ set: (key, value) => globalThis.medeoPlugin.invoke('storage.set', { key, value }),
40
+ };
41
+ export const creation = { open: () => globalThis.medeoPlugin.invoke('creation.open') };
42
+ export const commands = { run: (command, args) => globalThis.medeoPlugin.run(command, args) };
43
+ export function definePlugin(definition) {
44
+ return { deactivate: () => undefined, ...definition };
45
+ }
46
+ /** Settings declared under `contributes.configuration`; the user edits them in the plugin center. */
47
+ export const config = {
48
+ get: (key) => globalThis.medeoPlugin.invoke('config.get', { key }),
49
+ all: () => globalThis.medeoPlugin.invoke('config.get'),
50
+ set: (key, value) => globalThis.medeoPlugin.invoke('config.set', { key, value }),
51
+ /** Calls `listener` with all values after each change, from the user or the plugin. */
52
+ onDidChange(listener) {
53
+ return globalThis.medeoPlugin.onEvent(event => {
54
+ if (event.type === 'signal' && event.channel === 'config')
55
+ void config.all().then(listener).catch(() => { });
56
+ });
57
+ },
58
+ };
59
+ export const providers = {
60
+ list: () => globalThis.medeoPlugin.invoke('providers.list'),
61
+ require: async (id, version = 1) => {
62
+ const provider = (await providers.list()).find(p => p.id === id && p.version === version && p.available);
63
+ if (!provider)
64
+ throw new Error('provider_unavailable');
65
+ return provider;
66
+ },
67
+ };
68
+ export const resources = {
69
+ chooseDirectories: (access = {}) => globalThis.medeoPlugin.invoke('resources.choose_directories', access.full ? { full: true } : {}),
70
+ list: () => globalThis.medeoPlugin.invoke('resources.list'),
71
+ revoke: (id) => globalThis.medeoPlugin.invoke('resources.revoke', { id }),
72
+ requestDirectory: (path, reason, access = {}) => globalThis.medeoPlugin.invoke('resources.request_directory', { path, reason, ...(access.full ? { full: true } : {}) }),
73
+ };
74
+ /** Generic registered-provider entry point; the Host remains the authority. */
75
+ export const capabilities = {
76
+ list: () => globalThis.medeoPlugin.invoke('capabilities.list'),
77
+ labels: () => globalThis.medeoPlugin.invoke('capabilities.labels'),
78
+ invoke: (method, args = {}) => globalThis.medeoPlugin.invoke(method, args),
79
+ };
80
+ export const clipboard = { read: () => globalThis.medeoPlugin.invoke('clipboard.read'), write: (text) => globalThis.medeoPlugin.invoke('clipboard.write', { text }) };
81
+ export const system = { info: () => globalThis.medeoPlugin.invoke('system.info'), running: (rules) => capabilities.invoke('system.running', { rules }) };
82
+ /** `runtime: "node"` plugins only (the `node` permission): moves up to 256 absolute
83
+ * paths to the OS Trash, the one thing Node cannot do itself. A failure does not stop the rest. */
84
+ export const trash = (paths) => globalThis.medeoPlugin.invoke('system.trash', { paths });
85
+ export const notifications = { show: (id, title, body) => globalThis.medeoPlugin.invoke('notifications.show', { id, title, body }), close: (id) => globalThis.medeoPlugin.invoke('notifications.close', { id }) };
86
+ export const power = { start: (type) => globalThis.medeoPlugin.invoke('power.start', { type }), stop: (id) => globalThis.medeoPlugin.invoke('power.stop', { id }), state: () => globalThis.medeoPlugin.invoke('power.state') };
87
+ export const shortcuts = { list: () => globalThis.medeoPlugin.invoke('shortcuts.list'), register: (id, accelerator, openUI = false) => globalThis.medeoPlugin.invoke('shortcuts.register', { id, accelerator, open_ui: openUI }), unregister: (id) => globalThis.medeoPlugin.invoke('shortcuts.unregister', { id }), events: () => globalThis.medeoPlugin.invoke('shortcuts.events') };
88
+ /** `panel: true` (macOS): a frameless window floating over every app and Space that does not
89
+ * activate Medeo — the app the user was in keeps focus — opened at the pointer and hidden (not
90
+ * closed) when it loses focus; opening it again moves it to the pointer. `hide` keeps a window
91
+ * and its page alive for the next `open`/`focus`. */
92
+ export const appWindows = { open: (options) => globalThis.medeoPlugin.invoke('windows.open', options), close: (id) => globalThis.medeoPlugin.invoke('windows.close', { id }), focus: (id) => globalThis.medeoPlugin.invoke('windows.focus', { id }), hide: (id) => globalThis.medeoPlugin.invoke('windows.hide', { id }), list: () => globalThis.medeoPlugin.invoke('windows.list') };
93
+ export const tray = { set: (title, items, options = {}) => globalThis.medeoPlugin.invoke('tray.set', { title, items, ...options }), remove: () => globalThis.medeoPlugin.invoke('tray.remove'), events: () => globalThis.medeoPlugin.invoke('tray.events') };
94
+ export const external = { open: (url) => globalThis.medeoPlugin.invoke('external.open', { url }) };
95
+ export const permissions = { request: (permission, reason) => globalThis.medeoPlugin.invoke('permissions.request', { permission, reason }) };
96
+ export const approvals = {
97
+ request: (request) => globalThis.medeoPlugin.invoke('approvals.request', { ...request }),
98
+ rules: () => globalThis.medeoPlugin.invoke('approvals.rules'),
99
+ forget: (action, pattern) => globalThis.medeoPlugin.invoke('approvals.forget', { action, pattern }),
100
+ };
101
+ export const agent = {
102
+ models: () => capabilities.invoke('agent.models', {}),
103
+ start: (messages, model) => capabilities.invoke('agent.start', { messages, ...(model ? { model } : {}) }),
104
+ state: (id) => operations.state(id),
105
+ cancel: (id) => operations.cancel(id),
106
+ release: (id) => operations.release(id),
107
+ /** Calls `onText` as the reply streams in; resolves with the final job. */
108
+ async follow(id, onText) {
109
+ for (;;) {
110
+ const change = nextChange(id, 2000);
111
+ const job = await agent.state(id);
112
+ onText(job);
113
+ if (job.status !== 'running') {
114
+ change.off();
115
+ return job;
116
+ }
117
+ await change.promise;
118
+ change.off();
119
+ }
120
+ },
121
+ };
122
+ export const applications = {
123
+ /** The whole inventory; plugins rank and filter it themselves. */
124
+ list: (refresh = false) => capabilities.invoke('applications.list', { refresh }),
125
+ launch: (id) => capabilities.invoke('applications.launch', { id }),
126
+ state: () => capabilities.invoke('applications.state', {}),
127
+ };
128
+ export const capture = {
129
+ status: (refresh = true) => capabilities.invoke('capture.status', { refresh }),
130
+ sources: (refresh = false, requestPermission = false) => capabilities.invoke('capture.sources', { refresh, request_permission: requestPermission }),
131
+ start: (source, options = {}) => capabilities.invoke('capture.start', { source, microphone: options.microphone ?? false, system_audio: options.systemAudio ?? false }),
132
+ state: () => capabilities.invoke('capture.state', {}),
133
+ stop: (id) => capabilities.invoke('capture.stop', { id }),
134
+ cancel: (id) => capabilities.invoke('capture.cancel', { id }),
135
+ recover: (id) => capabilities.invoke('capture.recover', { id }),
136
+ open: (id, mode = 'play') => capabilities.invoke('capture.open', { id, mode }),
137
+ previewURL: (id) => location.origin + '/_capture/' + encodeURIComponent(id) + '.mp4',
138
+ };
139
+ export const inputMethod = {
140
+ system: (refresh = false) => capabilities.invoke('input_method.system', { refresh }),
141
+ setup: () => capabilities.invoke('input_method.setup'),
142
+ remove: () => capabilities.invoke('input_method.remove'),
143
+ settings: () => capabilities.invoke('input_method.settings'),
144
+ status: () => capabilities.invoke('input_method.status'),
145
+ configure: (name) => capabilities.invoke('input_method.configure', { name }),
146
+ enable: () => capabilities.invoke('input_method.enable'),
147
+ disable: () => capabilities.invoke('input_method.disable'),
148
+ events: () => capabilities.invoke('input_method.events'),
149
+ respond: (id, candidates) => capabilities.invoke('input_method.respond', { id, candidates }),
150
+ };
151
+ export const network = {
152
+ origins: () => capabilities.invoke('network.origins'),
153
+ requestOrigin: (origin, reason) => capabilities.invoke('network.request_origin', { origin, reason }),
154
+ revokeOrigin: (origin) => capabilities.invoke('network.revoke_origin', { origin }),
155
+ request: (request) => capabilities.invoke('network.request', { ...request }),
156
+ state: (id) => operations.state(id),
157
+ read: (id, offset = 0, length = 32768) => capabilities.invoke('network.read', { id, offset, length }),
158
+ cancel: (id) => operations.cancel(id),
159
+ release: (id) => operations.release(id),
160
+ /** Resolves once the response has fully arrived; the job stays readable until released. */
161
+ async settled(id) { for (;;) {
162
+ const change = nextChange(id, 2000);
163
+ const state = await network.state(id);
164
+ if (state.status !== 'running') {
165
+ change.off();
166
+ return state;
167
+ }
168
+ await change.promise;
169
+ change.off();
170
+ } },
171
+ /** Single consumer for a stream:true job. Break/throw/abort cancels and releases it. */
172
+ async *bytes(id, signal) {
173
+ let offset = 0;
174
+ let abortCleanup;
175
+ const abort = () => { abortCleanup = network.dispose(id); void abortCleanup.catch(() => { }); };
176
+ signal?.addEventListener('abort', abort, { once: true });
177
+ try {
178
+ for (;;) {
179
+ let wake;
180
+ const changed = new Promise(resolve => { wake = resolve; });
181
+ const off = events.on(event => { if (event.type === 'operation' && event.id === id)
182
+ wake(); });
183
+ signal?.addEventListener('abort', wake, { once: true });
184
+ try {
185
+ if (signal?.aborted)
186
+ throw new Error('network_cancelled');
187
+ const state = await network.state(id);
188
+ if (state.status === 'failed' || state.status === 'cancelled')
189
+ throw new Error(state.error ?? state.status);
190
+ const part = await network.read(id, offset);
191
+ offset = part.next_offset;
192
+ if (signal?.aborted)
193
+ throw new Error('network_cancelled');
194
+ if (part.data_base64)
195
+ yield Uint8Array.from(atob(part.data_base64), c => c.charCodeAt(0));
196
+ if (part.eof)
197
+ return;
198
+ if (!part.data_base64)
199
+ await changed;
200
+ }
201
+ finally {
202
+ off();
203
+ signal?.removeEventListener('abort', wake);
204
+ }
205
+ }
206
+ }
207
+ finally {
208
+ signal?.removeEventListener('abort', abort);
209
+ await (abortCleanup ?? network.dispose(id));
210
+ }
211
+ },
212
+ /** Incremental UTF-8 decoder, including code points split across HTTP chunks. */
213
+ async *textStream(id, signal) {
214
+ const decoder = new TextDecoder();
215
+ for await (const bytes of network.bytes(id, signal)) {
216
+ const text = decoder.decode(bytes, { stream: true });
217
+ if (text)
218
+ yield text;
219
+ }
220
+ const tail = decoder.decode();
221
+ if (tail)
222
+ yield tail;
223
+ },
224
+ /** SSE framing only: no automatic reconnect/retry or Last-Event-ID requests. */
225
+ async *sse(id, signal) {
226
+ let pending = '', data = [], event = '', lastId = '', retry, size = 0, skipLf = false;
227
+ for await (const text of network.textStream(id, signal)) {
228
+ pending += skipLf && text.startsWith('\n') ? text.slice(1) : text;
229
+ skipLf = false;
230
+ for (;;) {
231
+ const at = pending.search(/[\r\n]/);
232
+ if (at < 0)
233
+ break;
234
+ skipLf = pending[at] === '\r' && at === pending.length - 1;
235
+ const line = pending.slice(0, at);
236
+ pending = pending.slice(at + (pending[at] === '\r' && pending[at + 1] === '\n' ? 2 : 1));
237
+ size += line.length;
238
+ if (size > 262144)
239
+ throw new Error('network_sse_event_too_large');
240
+ if (!line) {
241
+ if (data.length)
242
+ yield { data: data.join('\n'), event: event || 'message', id: lastId, ...(retry === undefined ? {} : { retry }) };
243
+ data = [];
244
+ event = '';
245
+ size = 0;
246
+ continue;
247
+ }
248
+ if (line.startsWith(':'))
249
+ continue;
250
+ const colon = line.indexOf(':');
251
+ const field = colon < 0 ? line : line.slice(0, colon);
252
+ let value = colon < 0 ? '' : line.slice(colon + 1);
253
+ if (value.startsWith(' '))
254
+ value = value.slice(1);
255
+ if (field === 'data')
256
+ data.push(value);
257
+ else if (field === 'event')
258
+ event = value;
259
+ else if (field === 'id' && !value.includes('\0'))
260
+ lastId = value;
261
+ else if (field === 'retry' && /^\d+$/.test(value) && Number.isSafeInteger(Number(value)))
262
+ retry = Number(value);
263
+ }
264
+ if (size + pending.length > 262144)
265
+ throw new Error('network_sse_event_too_large');
266
+ }
267
+ // SSE requires a blank line to dispatch; incomplete final events are discarded.
268
+ },
269
+ /** Explicitly dispose a stream that was never iterated (or has another owner). */
270
+ async dispose(id) {
271
+ const ignoreMissing = (error) => { if (!/^(operation_missing|network_job_missing|permission_denied)$/.test(error instanceof Error ? error.message : String(error)))
272
+ throw error; };
273
+ try {
274
+ await network.cancel(id);
275
+ }
276
+ catch (error) {
277
+ ignoreMissing(error);
278
+ }
279
+ try {
280
+ await network.release(id);
281
+ }
282
+ catch (error) {
283
+ ignoreMissing(error);
284
+ }
285
+ },
286
+ async text(id) {
287
+ const decoder = new TextDecoder();
288
+ let offset = 0, text = '';
289
+ for (;;) {
290
+ const part = await network.read(id, offset);
291
+ const bytes = Uint8Array.from(atob(part.data_base64), c => c.charCodeAt(0));
292
+ text += decoder.decode(bytes, { stream: !part.eof });
293
+ if (part.eof)
294
+ return text;
295
+ offset = part.next_offset;
296
+ }
297
+ },
298
+ };
299
+ export const files = {
300
+ backupPreview: (root, record) => capabilities.invoke('files.backup_preview', { root, record }),
301
+ archiveBackup: (preview) => capabilities.invoke('files.archive_backup', { preview }),
302
+ forgetPreview: (root, record) => capabilities.invoke('files.forget_preview', { root, record }),
303
+ forgetRecord: (preview) => capabilities.invoke('files.forget_record', { preview }),
304
+ historyPage: (root, cursor, limit = 50) => capabilities.invoke('files.history_page', { root, ...(cursor ? { cursor } : {}), limit }),
305
+ trashPreview: (root, path) => capabilities.invoke('files.trash_preview', { root, path }),
306
+ discardPreview: (preview) => capabilities.invoke('files.discard_preview', { preview }),
307
+ trash: (preview) => capabilities.invoke('files.trash', { preview }),
308
+ /** Breadth-first listings of a whole tree in one call. Hidden and `skip`
309
+ * directories are listed by their parent but not entered. */
310
+ listTree: (root, path = '', options = {}) => capabilities.invoke('files.list_tree', { root, path, ...options }),
311
+ /** Full-access roots only: move entries to the Trash without a Host confirmation.
312
+ * `stamp` is what `files.measure` returned; a changed entry is refused, not moved. */
313
+ trashDirect: (items) => capabilities.invoke('files.trash_direct', { items }),
314
+ watch: (root, path = '', recursive = false) => capabilities.invoke('files.watch', { root, path, recursive }),
315
+ watches: () => capabilities.invoke('files.watches'),
316
+ events: (id, after = 0) => capabilities.invoke('files.events', { id, after }),
317
+ unwatch: (id) => capabilities.invoke('files.unwatch', { id }),
318
+ roots: () => capabilities.invoke('files.roots'),
319
+ list: (root, path = '') => capabilities.invoke('files.list', { root, path }),
320
+ stat: (root, path = '') => capabilities.invoke('files.stat', { root, path }),
321
+ readText: (root, path, revision) => capabilities.invoke('files.read_text', { root, path, ...(revision ? { revision } : {}) }),
322
+ writeText: (root, path, text, revision) => capabilities.invoke('files.write_text', { root, path, text, revision }),
323
+ mkdir: (root, path) => capabilities.invoke('files.mkdir', { root, path }),
324
+ move: (root, path, destination, revision) => capabilities.invoke('files.move', { root, path, destination, revision }),
325
+ history: (root) => capabilities.invoke('files.history', { root }),
326
+ restore: (root, record) => capabilities.invoke('files.restore', { root, record }),
327
+ state: (id) => operations.state(id),
328
+ cancel: (id) => operations.cancel(id),
329
+ release: (id) => operations.release(id),
330
+ result: (job) => operations.wait(job),
331
+ };
332
+ async function secretOperation(method, args) {
333
+ const job = await capabilities.invoke('secrets.' + method, args);
334
+ const deadline = Date.now() + 30000;
335
+ for (;;) {
336
+ const change = nextChange(job.id, 1000);
337
+ const result = await capabilities.invoke('secrets.result', { id: job.id });
338
+ if (result.status !== 'running')
339
+ change.off();
340
+ if (result.status === 'completed')
341
+ return result.value ?? null;
342
+ if (result.status === 'failed')
343
+ throw Error(result.error ?? 'secrets_storage_failed');
344
+ if (Date.now() >= deadline) {
345
+ change.off();
346
+ throw Error('secrets_outcome_unknown');
347
+ }
348
+ await change.promise;
349
+ change.off();
350
+ }
351
+ }
352
+ export const secrets = {
353
+ get: (key) => secretOperation('get', { key }),
354
+ set: async (key, value) => { await secretOperation('set', { key, value }); },
355
+ remove: async (key) => { await secretOperation('remove', { key }); },
356
+ };
357
+ /** A loopback server is instance-owned and never survives Host restart. Keep the
358
+ * start token in private memory and serve requests in a worker for background use. */
359
+ export const server = {
360
+ start: (options) => capabilities.invoke('server.start', { ...options }),
361
+ state: (id) => capabilities.invoke('server.state', { id }),
362
+ list: () => capabilities.invoke('server.state'),
363
+ events: (id, limit = 8) => capabilities.invoke('server.events', { id, limit }),
364
+ respond: (id, request, response) => capabilities.invoke('server.respond', { id, request, ...response }),
365
+ stop: (id) => capabilities.invoke('server.stop', { id }),
366
+ };
367
+ /** Text is formatted in Rust from the signed package. Render with textContent,
368
+ * never innerHTML. Register listeners before initial state fetch to avoid races. */
369
+ export const i18n = {
370
+ state: () => capabilities.invoke('i18n.state'),
371
+ setLocale: (preference) => capabilities.invoke('i18n.setLocale', { preference }),
372
+ t: (key, params = {}) => capabilities.invoke('i18n.t', { key, params }),
373
+ /** Every message of the effective locale (default-locale fallback merged in). */
374
+ catalog: () => capabilities.invoke('i18n.catalog'),
375
+ onDidChangeLocale: (callback) => globalThis.medeoPlugin.onLocaleChanged(callback),
376
+ };
377
+ export const operations = {
378
+ state: (id) => capabilities.invoke('operations.state', { id }),
379
+ list: () => capabilities.invoke('operations.list'),
380
+ cancel: (id) => capabilities.invoke('operations.cancel', { id }),
381
+ release: (id) => capabilities.invoke('operations.release', { id }),
382
+ async wait(operation, signal) {
383
+ let value = operation;
384
+ while (value.status === 'running') {
385
+ if (signal?.aborted) {
386
+ await operations.cancel(value.id);
387
+ throw Error('cancelled');
388
+ }
389
+ const change = nextChange(value.id, 2000);
390
+ value = await operations.state(value.id);
391
+ if (value.status === 'running')
392
+ await change.promise;
393
+ change.off();
394
+ }
395
+ // Quick reads may come back settled and already released by the Host.
396
+ if (!value.released)
397
+ await operations.release(value.id);
398
+ if (value.status !== 'completed')
399
+ throw Error(value.error ?? value.status);
400
+ return value.result;
401
+ },
402
+ };
403
+ const nativeCall = (method, args = {}) => capabilities.invoke(method, args);
404
+ function mediaProvider(kind) {
405
+ return {
406
+ status: () => nativeCall(kind + '.status'),
407
+ start: (maxSeconds = 300) => nativeCall(kind + '.start', { max_seconds: maxSeconds }),
408
+ stop: (id) => nativeCall(kind + '.stop', { id }),
409
+ state: (id) => nativeCall(kind + '.state', id ? { id } : {}),
410
+ read: (id, offset = 0, length = 32768) => nativeCall(kind + '.read', { id, offset, length }),
411
+ /** Same-origin URL for an `<audio>` / `<video>` element; plays once the record is "completed". */
412
+ previewURL: (record) => location.origin + '/_media/' + encodeURIComponent(record.id) + (record.extension === 'm4a' ? '.m4a' : '.mp4'),
413
+ };
414
+ }
415
+ export const camera = mediaProvider('camera'), microphone = mediaProvider('microphone'), systemAudio = mediaProvider('system_audio');
416
+ export const accessibility = { status: () => nativeCall('accessibility.status'), request: () => nativeCall('accessibility.request'), windows: () => nativeCall('accessibility.windows'), focus: (id) => nativeCall('accessibility.focus', { id }), selectedText: (id) => nativeCall('accessibility.selected_text', { id }), insertText: (focus, text) => nativeCall('accessibility.insert_text', { id: focus, text }) };
417
+ export const browserView = { open: (id, url, width = 1000, height = 720) => capabilities.invoke('browser.open', { id, url, width, height }), state: (id) => capabilities.invoke('browser.state', { id }), close: (id) => capabilities.invoke('browser.close', { id }) };
418
+ export const printing = { print: (text) => capabilities.invoke('printing.print', { text }) };
419
+ export const processes = { start: (executable, args = [], timeoutMs = 60000) => nativeCall('process.start', { executable, arguments: args, timeout_ms: timeoutMs }), state: (id) => nativeCall('process.state', { id }), write: (id, text) => nativeCall('process.write', { id, text }), stop: (id) => nativeCall('process.stop', { id }) };
420
+ export const background = { schedule: (id, delayMs, payload = null, intervalMs = 0) => capabilities.invoke('background.schedule', { id, delay_ms: delayMs, interval_ms: intervalMs, payload }), list: () => capabilities.invoke('background.list'), cancel: (id) => capabilities.invoke('background.cancel', { id }), events: () => capabilities.invoke('background.events') };
421
+ export const contacts = { status: () => nativeCall('contacts.status'), request: () => nativeCall('contacts.request'), list: (query = '', limit = 50) => nativeCall('contacts.list', { query, limit }), save: (contact) => nativeCall('contacts.save', contact) };
422
+ export const calendar = { status: () => nativeCall('calendar.status'), request: () => nativeCall('calendar.request'), list: (startMs, endMs, limit = 50) => nativeCall('calendar.list', { start_ms: startMs, end_ms: endMs, limit }), save: (event) => nativeCall('calendar.save', event) };
423
+ export const deviceLocation = { status: () => nativeCall('location.status'), request: () => nativeCall('location.request'), current: () => nativeCall('location.current') };
424
+ export const devices = { choose: (transport) => nativeCall('devices.choose', { transport }), open: (id, baudRate = 115200) => nativeCall('devices.open', { id, baud_rate: baudRate }), list: () => nativeCall('devices.list'), read: (id, length = 4096) => nativeCall('devices.read', { id, length }), write: (id, dataBase64) => nativeCall('devices.write', { id, data_base64: dataBase64 }), close: (id) => nativeCall('devices.close', { id }) };
@@ -0,0 +1,46 @@
1
+ /** React is only imported by the optional @medeo-plugin/sdk/ui/react entry. */
2
+ import { type ReactNode } from 'react';
3
+ import { type CardOptions, type PillOptions, type ToggleOptions, type SegmentedOptions, type ListOptions } from './ui.js';
4
+ export { uiStyles, installStyles } from './ui/styles.js';
5
+ export type { CardOptions, PillOptions, ToggleOptions, SegmentedOptions, ListOptions, Segment } from './ui.js';
6
+ /** Call once near your root. SSR-safe; style installation is idempotent. */
7
+ export declare function useUIStyles(): void;
8
+ export declare function Card({ title, description, testid, children }: CardOptions & {
9
+ children?: ReactNode;
10
+ }): import("react").DetailedReactHTMLElement<{
11
+ className: string;
12
+ 'data-testid': string;
13
+ 'aria-label': string;
14
+ }, HTMLElement>;
15
+ export declare function Pill({ label, tone, testid }: PillOptions): import("react").DetailedReactHTMLElement<{
16
+ className: string;
17
+ 'data-testid': string;
18
+ 'data-tone': "error" | "neutral" | "info";
19
+ }, HTMLElement>;
20
+ /** Controlled: update checked in onChange. Native input supplies name and Space handling. */
21
+ export declare function Toggle({ label, checked, disabled, onChange, testid }: ToggleOptions): import("react").DetailedReactHTMLElement<{
22
+ className: string;
23
+ 'data-testid': string;
24
+ }, HTMLElement>;
25
+ /** Controlled radio group with a single tab stop and arrow/Home/End navigation. */
26
+ export declare function Segmented<T extends string>({ label, value, options, disabled, onChange, testid }: SegmentedOptions<T>): import("react").DetailedReactHTMLElement<{
27
+ className: string;
28
+ role: "radiogroup";
29
+ 'aria-label': string;
30
+ 'data-testid': string;
31
+ }, HTMLElement>;
32
+ export declare function List({ label, testid, items }: ListOptions & {
33
+ items: readonly {
34
+ key: string;
35
+ content: ReactNode;
36
+ }[];
37
+ }): import("react").DetailedReactHTMLElement<{
38
+ className: string;
39
+ 'data-testid': string;
40
+ 'aria-label': string;
41
+ }, HTMLElement>;
42
+ export declare function EmptyState({ title, description, testid }: CardOptions): import("react").DetailedReactHTMLElement<{
43
+ className: string;
44
+ 'data-testid': string;
45
+ 'aria-label': string;
46
+ }, HTMLElement>;
package/dist/react.js ADDED
@@ -0,0 +1,44 @@
1
+ /** React is only imported by the optional @medeo-plugin/sdk/ui/react entry. */
2
+ import { createElement as h, useEffect } from 'react';
3
+ import { segmentTarget, segmentTabIndex } from './ui.js';
4
+ import { installStyles } from './ui/styles.js';
5
+ export { uiStyles, installStyles } from './ui/styles.js';
6
+ /** Call once near your root. SSR-safe; style installation is idempotent. */
7
+ export function useUIStyles() { useEffect(() => { installStyles(); }, []); }
8
+ export function Card({ title, description, testid, children }) {
9
+ return h('section', { className: 'medeo-ui medeo-card', 'data-testid': testid, 'aria-label': title }, h('h2', { 'data-testid': `${testid}.title` }, title), description !== undefined && h('p', { className: 'medeo-description' }, description), children);
10
+ }
11
+ export function Pill({ label, tone = 'info', testid }) {
12
+ return h('span', { className: 'medeo-ui medeo-pill', 'data-testid': testid, 'data-tone': tone }, label);
13
+ }
14
+ /** Controlled: update checked in onChange. Native input supplies name and Space handling. */
15
+ export function Toggle({ label, checked, disabled, onChange, testid }) {
16
+ return h('label', { className: 'medeo-ui medeo-toggle', 'data-testid': `${testid}.root` }, h('input', { type: 'checkbox', role: 'switch', checked, disabled, 'data-testid': testid, onChange: (event) => onChange(event.currentTarget.checked) }), h('span', null, label));
17
+ }
18
+ /** Controlled radio group with a single tab stop and arrow/Home/End navigation. */
19
+ export function Segmented({ label, value, options, disabled, onChange, testid }) {
20
+ return h('div', { className: 'medeo-ui medeo-segmented', role: 'radiogroup', 'aria-label': label, 'data-testid': testid }, options.map((option, index) => h('button', {
21
+ key: option.value, type: 'button', role: 'radio', 'aria-checked': option.value === value,
22
+ 'data-testid': `${testid}.${index}`, disabled: disabled || option.disabled,
23
+ tabIndex: disabled ? -1 : segmentTabIndex(options, value, index),
24
+ onClick: () => { if (!disabled && !option.disabled)
25
+ onChange(option.value); },
26
+ onKeyDown: (event) => {
27
+ if (disabled)
28
+ return;
29
+ const group = event.currentTarget.parentElement;
30
+ const target = segmentTarget(options, index, event.key, getComputedStyle(group).direction === 'rtl');
31
+ if (target === undefined)
32
+ return;
33
+ event.preventDefault();
34
+ group.children[target].focus();
35
+ onChange(options[target].value);
36
+ },
37
+ }, option.label)));
38
+ }
39
+ export function List({ label, testid, items }) {
40
+ return h('ul', { className: 'medeo-ui medeo-list', 'data-testid': testid, 'aria-label': label }, items.map(item => h('li', { key: item.key, 'data-testid': `${testid}.${item.key}` }, item.content)));
41
+ }
42
+ export function EmptyState({ title, description, testid }) {
43
+ return h('section', { className: 'medeo-ui medeo-empty', 'data-testid': testid, 'aria-label': title }, h('h2', null, title), description !== undefined && h('p', { className: 'medeo-description' }, description));
44
+ }