@ggui-ai/gadgets 0.1.0-rc.1
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 +201 -0
- package/README.md +121 -0
- package/dist/codegen.d.mts +78 -0
- package/dist/codegen.d.ts +78 -0
- package/dist/codegen.js +69 -0
- package/dist/codegen.mjs +34 -0
- package/dist/index.d.mts +663 -0
- package/dist/index.d.ts +663 -0
- package/dist/index.js +825 -0
- package/dist/index.mjs +790 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CachingGadgetCatalog: () => CachingGadgetCatalog,
|
|
24
|
+
InMemoryGadgetCatalog: () => InMemoryGadgetCatalog,
|
|
25
|
+
WrapperConformanceError: () => WrapperConformanceError,
|
|
26
|
+
createGguiGadget: () => createGguiGadget,
|
|
27
|
+
defineGadgetPackage: () => defineGadgetPackage,
|
|
28
|
+
getPublicEnv: () => getPublicEnv,
|
|
29
|
+
useCamera: () => useCamera,
|
|
30
|
+
useClipboardPaste: () => useClipboardPaste,
|
|
31
|
+
useClipboardWrite: () => useClipboardWrite,
|
|
32
|
+
useFilePicker: () => useFilePicker,
|
|
33
|
+
useGeolocation: () => useGeolocation,
|
|
34
|
+
useMicrophone: () => useMicrophone,
|
|
35
|
+
useNotifications: () => useNotifications
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// src/createGguiGadget.ts
|
|
40
|
+
var import_protocol = require("@ggui-ai/protocol");
|
|
41
|
+
var WrapperConformanceError = class extends Error {
|
|
42
|
+
code = "wrapper_conformance";
|
|
43
|
+
violations;
|
|
44
|
+
constructor(label, violations) {
|
|
45
|
+
const labelTail = label ? ` (${label})` : "";
|
|
46
|
+
const summary = violations.map((v) => ` - ${v.path.join(".") || "<root>"}: ${v.message}`).join("\n");
|
|
47
|
+
super(`gadget spec conformance failed${labelTail}
|
|
48
|
+
${summary}`);
|
|
49
|
+
this.name = "WrapperConformanceError";
|
|
50
|
+
this.violations = violations;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
function createGguiGadget(spec) {
|
|
54
|
+
const {
|
|
55
|
+
hookImpl,
|
|
56
|
+
hook,
|
|
57
|
+
description,
|
|
58
|
+
usage,
|
|
59
|
+
example,
|
|
60
|
+
gotchas,
|
|
61
|
+
permission,
|
|
62
|
+
required,
|
|
63
|
+
...packageFields
|
|
64
|
+
} = spec;
|
|
65
|
+
const violations = [];
|
|
66
|
+
if (typeof hookImpl !== "function") {
|
|
67
|
+
violations.push({
|
|
68
|
+
path: ["hookImpl"],
|
|
69
|
+
message: "`hookImpl` MUST be a React hook function"
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const hookExport = {
|
|
73
|
+
hook,
|
|
74
|
+
description,
|
|
75
|
+
usage,
|
|
76
|
+
example,
|
|
77
|
+
...gotchas !== void 0 ? { gotchas } : {},
|
|
78
|
+
...permission !== void 0 ? { permission } : {},
|
|
79
|
+
...required !== void 0 ? { required } : {}
|
|
80
|
+
};
|
|
81
|
+
const descriptorDraft = {
|
|
82
|
+
...packageFields,
|
|
83
|
+
exports: [hookExport]
|
|
84
|
+
};
|
|
85
|
+
const parsed = import_protocol.strictGadgetDescriptorSchema.safeParse(descriptorDraft);
|
|
86
|
+
if (!parsed.success) {
|
|
87
|
+
for (const issue of parsed.error.issues) {
|
|
88
|
+
violations.push({
|
|
89
|
+
// zod's path is `(string | number | symbol)[]`; we coerce
|
|
90
|
+
// symbol segments to strings for our author-facing message.
|
|
91
|
+
path: issue.path.map(
|
|
92
|
+
(segment) => typeof segment === "symbol" ? segment.toString() : segment
|
|
93
|
+
),
|
|
94
|
+
message: issue.message
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!parsed.success || violations.length > 0) {
|
|
99
|
+
throw new WrapperConformanceError(
|
|
100
|
+
typeof spec.hook === "string" ? spec.hook : void 0,
|
|
101
|
+
violations
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const descriptor = parsed.data;
|
|
105
|
+
const wrapped = ((options) => hookImpl(options));
|
|
106
|
+
return Object.assign(wrapped, {
|
|
107
|
+
descriptor: Object.freeze(descriptor)
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/defineGadgetPackage.ts
|
|
112
|
+
var import_protocol2 = require("@ggui-ai/protocol");
|
|
113
|
+
function defineGadgetPackage(spec) {
|
|
114
|
+
const { exports: exportSpecs, ...packageFields } = spec;
|
|
115
|
+
const violations = [];
|
|
116
|
+
if (exportSpecs.length === 0) {
|
|
117
|
+
violations.push({
|
|
118
|
+
path: ["exports"],
|
|
119
|
+
message: "a gadget package MUST declare at least one export"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const exports2 = exportSpecs.map((entry, index) => {
|
|
123
|
+
if (typeof entry.impl !== "function") {
|
|
124
|
+
violations.push({
|
|
125
|
+
path: ["exports", index, "impl"],
|
|
126
|
+
message: "`impl` MUST be a function (a React hook or component)"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const teaching = {
|
|
130
|
+
description: entry.description,
|
|
131
|
+
usage: entry.usage,
|
|
132
|
+
example: entry.example,
|
|
133
|
+
...entry.gotchas !== void 0 ? { gotchas: entry.gotchas } : {},
|
|
134
|
+
...entry.permission !== void 0 ? { permission: entry.permission } : {},
|
|
135
|
+
...entry.required !== void 0 ? { required: entry.required } : {}
|
|
136
|
+
};
|
|
137
|
+
return "hook" in entry ? { hook: entry.hook, ...teaching } : { component: entry.component, ...teaching };
|
|
138
|
+
});
|
|
139
|
+
const parsed = import_protocol2.strictGadgetDescriptorSchema.safeParse({
|
|
140
|
+
...packageFields,
|
|
141
|
+
exports: exports2
|
|
142
|
+
});
|
|
143
|
+
if (!parsed.success) {
|
|
144
|
+
for (const issue of parsed.error.issues) {
|
|
145
|
+
violations.push({
|
|
146
|
+
path: issue.path.map(
|
|
147
|
+
(segment) => typeof segment === "symbol" ? segment.toString() : segment
|
|
148
|
+
),
|
|
149
|
+
message: issue.message
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!parsed.success || violations.length > 0) {
|
|
154
|
+
throw new WrapperConformanceError(spec.package, violations);
|
|
155
|
+
}
|
|
156
|
+
return parsed.data;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/catalog-adapter.ts
|
|
160
|
+
var InMemoryGadgetCatalog = class {
|
|
161
|
+
#byApp;
|
|
162
|
+
constructor(byApp) {
|
|
163
|
+
this.#byApp = byApp;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Convenience factory for the common "every app gets the same
|
|
167
|
+
* catalog" case (the OSS stdlib seed pattern). Apps not explicitly
|
|
168
|
+
* keyed fall back to the supplied default.
|
|
169
|
+
*/
|
|
170
|
+
static withDefault(defaultEntries, perApp) {
|
|
171
|
+
const merged = /* @__PURE__ */ new Map();
|
|
172
|
+
if (perApp) {
|
|
173
|
+
for (const [appId, entries] of perApp) merged.set(appId, entries);
|
|
174
|
+
}
|
|
175
|
+
return new InMemoryGadgetCatalogWithFallback(merged, defaultEntries);
|
|
176
|
+
}
|
|
177
|
+
async list(appId) {
|
|
178
|
+
return this.#byApp.get(appId) ?? [];
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var InMemoryGadgetCatalogWithFallback = class extends InMemoryGadgetCatalog {
|
|
182
|
+
#fallback;
|
|
183
|
+
constructor(byApp, fallback) {
|
|
184
|
+
super(byApp);
|
|
185
|
+
this.#fallback = fallback;
|
|
186
|
+
}
|
|
187
|
+
async list(appId) {
|
|
188
|
+
const direct = await super.list(appId);
|
|
189
|
+
return direct.length > 0 ? direct : this.#fallback;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var CachingGadgetCatalog = class {
|
|
193
|
+
#inner;
|
|
194
|
+
#ttlMs;
|
|
195
|
+
#now;
|
|
196
|
+
#cache = /* @__PURE__ */ new Map();
|
|
197
|
+
#inflight = /* @__PURE__ */ new Map();
|
|
198
|
+
constructor(inner, options = {}) {
|
|
199
|
+
this.#inner = inner;
|
|
200
|
+
this.#ttlMs = options.ttlMs ?? 3e4;
|
|
201
|
+
this.#now = options.now ?? Date.now;
|
|
202
|
+
}
|
|
203
|
+
async list(appId) {
|
|
204
|
+
const now = this.#now();
|
|
205
|
+
const cached = this.#cache.get(appId);
|
|
206
|
+
if (cached !== void 0 && cached.expiresAt > now) {
|
|
207
|
+
return cached.entries;
|
|
208
|
+
}
|
|
209
|
+
const inflight = this.#inflight.get(appId);
|
|
210
|
+
if (inflight !== void 0) return inflight;
|
|
211
|
+
const fetchPromise = (async () => {
|
|
212
|
+
try {
|
|
213
|
+
const entries = await this.#inner.list(appId);
|
|
214
|
+
if (this.#ttlMs > 0) {
|
|
215
|
+
this.#cache.set(appId, {
|
|
216
|
+
entries,
|
|
217
|
+
expiresAt: this.#now() + this.#ttlMs
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return entries;
|
|
221
|
+
} finally {
|
|
222
|
+
this.#inflight.delete(appId);
|
|
223
|
+
}
|
|
224
|
+
})();
|
|
225
|
+
this.#inflight.set(appId, fetchPromise);
|
|
226
|
+
return fetchPromise;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Drop the cached entry for `appId`. Right for operator-triggered
|
|
230
|
+
* invalidation (gadget registered / unregistered / updated). When
|
|
231
|
+
* `appId` is omitted, drops every cached entry.
|
|
232
|
+
*/
|
|
233
|
+
invalidate(appId) {
|
|
234
|
+
if (appId === void 0) {
|
|
235
|
+
this.#cache.clear();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.#cache.delete(appId);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/getPublicEnv.ts
|
|
243
|
+
function getPublicEnv(key, opts, target = globalThis) {
|
|
244
|
+
const root = target.__ggui__;
|
|
245
|
+
if (!root) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`getPublicEnv('${key}'): globalThis.__ggui__ is not initialized. This function must be invoked from inside a ggui-rendered component or wrapper hook, after the iframe runtime has booted.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const env = root.publicEnv ?? {};
|
|
251
|
+
if (Object.prototype.hasOwnProperty.call(env, key)) {
|
|
252
|
+
return env[key];
|
|
253
|
+
}
|
|
254
|
+
if (opts?.optional) return void 0;
|
|
255
|
+
const available = Object.keys(env);
|
|
256
|
+
throw new Error(
|
|
257
|
+
`getPublicEnv('${key}'): not provided in App.publicEnv. Available: [${available.join(", ") || "(none)"}]. Operators set this on the App record; gadget authors declare the key in 'requires' so the push gate verifies it before mount.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/useGeolocation.ts
|
|
262
|
+
var import_react = require("react");
|
|
263
|
+
function toCoords(position) {
|
|
264
|
+
const c = position.coords;
|
|
265
|
+
const base = {
|
|
266
|
+
latitude: c.latitude,
|
|
267
|
+
longitude: c.longitude,
|
|
268
|
+
accuracy: c.accuracy,
|
|
269
|
+
timestamp: position.timestamp
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
...base,
|
|
273
|
+
...c.altitude !== null && { altitude: c.altitude },
|
|
274
|
+
...c.altitudeAccuracy !== null && {
|
|
275
|
+
altitudeAccuracy: c.altitudeAccuracy
|
|
276
|
+
},
|
|
277
|
+
...c.heading !== null && { heading: c.heading },
|
|
278
|
+
...c.speed !== null && { speed: c.speed }
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function mapGeolocationError(err) {
|
|
282
|
+
if (err.code === err.PERMISSION_DENIED) {
|
|
283
|
+
return { code: "permission_denied", message: err.message };
|
|
284
|
+
}
|
|
285
|
+
if (err.code === err.TIMEOUT) {
|
|
286
|
+
return { code: "timeout", message: err.message };
|
|
287
|
+
}
|
|
288
|
+
return { code: "unknown", message: err.message };
|
|
289
|
+
}
|
|
290
|
+
var useGeolocation = (options) => {
|
|
291
|
+
const [value, setValue] = (0, import_react.useState)(void 0);
|
|
292
|
+
const [status, setStatus] = (0, import_react.useState)("idle");
|
|
293
|
+
const [error, setError] = (0, import_react.useState)(void 0);
|
|
294
|
+
const watchIdRef = (0, import_react.useRef)(null);
|
|
295
|
+
const start = (0, import_react.useCallback)(async () => {
|
|
296
|
+
if (typeof navigator === "undefined" || !navigator.geolocation) {
|
|
297
|
+
const e = {
|
|
298
|
+
code: "not_supported",
|
|
299
|
+
message: "Geolocation API unavailable in this environment."
|
|
300
|
+
};
|
|
301
|
+
setError(e);
|
|
302
|
+
setStatus("error");
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
setStatus("prompting");
|
|
306
|
+
setError(void 0);
|
|
307
|
+
const positionOptions = {
|
|
308
|
+
enableHighAccuracy: options?.enableHighAccuracy ?? false,
|
|
309
|
+
maximumAge: options?.maximumAge ?? 0,
|
|
310
|
+
timeout: options?.timeout ?? Number.POSITIVE_INFINITY
|
|
311
|
+
};
|
|
312
|
+
if (options?.watch) {
|
|
313
|
+
return new Promise((resolve) => {
|
|
314
|
+
let firstResolved = false;
|
|
315
|
+
watchIdRef.current = navigator.geolocation.watchPosition(
|
|
316
|
+
(position) => {
|
|
317
|
+
const coords = toCoords(position);
|
|
318
|
+
setValue(coords);
|
|
319
|
+
setStatus("active");
|
|
320
|
+
if (!firstResolved) {
|
|
321
|
+
firstResolved = true;
|
|
322
|
+
resolve(coords);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
(err) => {
|
|
326
|
+
const mapped = mapGeolocationError(err);
|
|
327
|
+
setError(mapped);
|
|
328
|
+
setStatus(mapped.code === "permission_denied" ? "denied" : "error");
|
|
329
|
+
if (!firstResolved) {
|
|
330
|
+
firstResolved = true;
|
|
331
|
+
resolve(void 0);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
positionOptions
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return new Promise((resolve) => {
|
|
339
|
+
navigator.geolocation.getCurrentPosition(
|
|
340
|
+
(position) => {
|
|
341
|
+
const coords = toCoords(position);
|
|
342
|
+
setValue(coords);
|
|
343
|
+
setStatus("completed");
|
|
344
|
+
resolve(coords);
|
|
345
|
+
},
|
|
346
|
+
(err) => {
|
|
347
|
+
const mapped = mapGeolocationError(err);
|
|
348
|
+
setError(mapped);
|
|
349
|
+
setStatus(mapped.code === "permission_denied" ? "denied" : "error");
|
|
350
|
+
resolve(void 0);
|
|
351
|
+
},
|
|
352
|
+
positionOptions
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
}, [options?.enableHighAccuracy, options?.maximumAge, options?.timeout, options?.watch]);
|
|
356
|
+
const stop = (0, import_react.useCallback)(() => {
|
|
357
|
+
if (watchIdRef.current !== null && typeof navigator !== "undefined") {
|
|
358
|
+
navigator.geolocation.clearWatch(watchIdRef.current);
|
|
359
|
+
watchIdRef.current = null;
|
|
360
|
+
}
|
|
361
|
+
setStatus("completed");
|
|
362
|
+
}, []);
|
|
363
|
+
return {
|
|
364
|
+
value,
|
|
365
|
+
status,
|
|
366
|
+
...error !== void 0 ? { error } : {},
|
|
367
|
+
start,
|
|
368
|
+
stop
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/useClipboardWrite.ts
|
|
373
|
+
var import_react2 = require("react");
|
|
374
|
+
function mapWriteError(err) {
|
|
375
|
+
if (err instanceof Error) {
|
|
376
|
+
if (err.name === "NotAllowedError") {
|
|
377
|
+
return { code: "permission_denied", message: err.message };
|
|
378
|
+
}
|
|
379
|
+
return { code: "unknown", message: err.message };
|
|
380
|
+
}
|
|
381
|
+
return { code: "unknown", message: String(err) };
|
|
382
|
+
}
|
|
383
|
+
var useClipboardWrite = (options) => {
|
|
384
|
+
const [value, setValue] = (0, import_react2.useState)(void 0);
|
|
385
|
+
const [status, setStatus] = (0, import_react2.useState)("idle");
|
|
386
|
+
const [error, setError] = (0, import_react2.useState)(void 0);
|
|
387
|
+
const start = (0, import_react2.useCallback)(async () => {
|
|
388
|
+
if (typeof navigator === "undefined" || !navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
|
|
389
|
+
const e = {
|
|
390
|
+
code: "not_supported",
|
|
391
|
+
message: "Clipboard write API unavailable in this environment."
|
|
392
|
+
};
|
|
393
|
+
setError(e);
|
|
394
|
+
setStatus("error");
|
|
395
|
+
return void 0;
|
|
396
|
+
}
|
|
397
|
+
const text = options?.text;
|
|
398
|
+
if (typeof text !== "string") {
|
|
399
|
+
const e = {
|
|
400
|
+
code: "unknown",
|
|
401
|
+
message: "useClipboardWrite: `text` option is required at start() time."
|
|
402
|
+
};
|
|
403
|
+
setError(e);
|
|
404
|
+
setStatus("error");
|
|
405
|
+
return void 0;
|
|
406
|
+
}
|
|
407
|
+
setStatus("prompting");
|
|
408
|
+
setError(void 0);
|
|
409
|
+
try {
|
|
410
|
+
await navigator.clipboard.writeText(text);
|
|
411
|
+
setValue(text);
|
|
412
|
+
setStatus("completed");
|
|
413
|
+
return text;
|
|
414
|
+
} catch (err) {
|
|
415
|
+
const mapped = mapWriteError(err);
|
|
416
|
+
setError(mapped);
|
|
417
|
+
setStatus(mapped.code === "permission_denied" ? "denied" : "error");
|
|
418
|
+
return void 0;
|
|
419
|
+
}
|
|
420
|
+
}, [options?.text]);
|
|
421
|
+
return {
|
|
422
|
+
value,
|
|
423
|
+
status,
|
|
424
|
+
...error !== void 0 ? { error } : {},
|
|
425
|
+
start
|
|
426
|
+
};
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// src/useClipboardPaste.ts
|
|
430
|
+
var import_react3 = require("react");
|
|
431
|
+
function mapReadError(err) {
|
|
432
|
+
if (err instanceof Error) {
|
|
433
|
+
if (err.name === "NotAllowedError") {
|
|
434
|
+
return { code: "permission_denied", message: err.message };
|
|
435
|
+
}
|
|
436
|
+
return { code: "unknown", message: err.message };
|
|
437
|
+
}
|
|
438
|
+
return { code: "unknown", message: String(err) };
|
|
439
|
+
}
|
|
440
|
+
var useClipboardPaste = () => {
|
|
441
|
+
const [value, setValue] = (0, import_react3.useState)(void 0);
|
|
442
|
+
const [status, setStatus] = (0, import_react3.useState)("idle");
|
|
443
|
+
const [error, setError] = (0, import_react3.useState)(void 0);
|
|
444
|
+
const start = (0, import_react3.useCallback)(async () => {
|
|
445
|
+
if (typeof navigator === "undefined" || !navigator.clipboard || typeof navigator.clipboard.readText !== "function") {
|
|
446
|
+
const e = {
|
|
447
|
+
code: "not_supported",
|
|
448
|
+
message: "Clipboard read API unavailable in this environment."
|
|
449
|
+
};
|
|
450
|
+
setError(e);
|
|
451
|
+
setStatus("error");
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
setStatus("prompting");
|
|
455
|
+
setError(void 0);
|
|
456
|
+
try {
|
|
457
|
+
const text = await navigator.clipboard.readText();
|
|
458
|
+
setValue(text);
|
|
459
|
+
setStatus("completed");
|
|
460
|
+
return text;
|
|
461
|
+
} catch (err) {
|
|
462
|
+
const mapped = mapReadError(err);
|
|
463
|
+
setError(mapped);
|
|
464
|
+
setStatus(mapped.code === "permission_denied" ? "denied" : "error");
|
|
465
|
+
return void 0;
|
|
466
|
+
}
|
|
467
|
+
}, []);
|
|
468
|
+
return {
|
|
469
|
+
value,
|
|
470
|
+
status,
|
|
471
|
+
...error !== void 0 ? { error } : {},
|
|
472
|
+
start
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/useNotifications.ts
|
|
477
|
+
var import_react4 = require("react");
|
|
478
|
+
var useNotifications = (options) => {
|
|
479
|
+
const [value, setValue] = (0, import_react4.useState)(
|
|
480
|
+
void 0
|
|
481
|
+
);
|
|
482
|
+
const [status, setStatus] = (0, import_react4.useState)("idle");
|
|
483
|
+
const [error, setError] = (0, import_react4.useState)(void 0);
|
|
484
|
+
const start = (0, import_react4.useCallback)(async () => {
|
|
485
|
+
if (typeof Notification === "undefined") {
|
|
486
|
+
const e = {
|
|
487
|
+
code: "not_supported",
|
|
488
|
+
message: "Notification API unavailable in this environment."
|
|
489
|
+
};
|
|
490
|
+
setError(e);
|
|
491
|
+
setStatus("error");
|
|
492
|
+
return void 0;
|
|
493
|
+
}
|
|
494
|
+
if (!options?.title) {
|
|
495
|
+
const e = {
|
|
496
|
+
code: "unknown",
|
|
497
|
+
message: "useNotifications: `title` option required at start()."
|
|
498
|
+
};
|
|
499
|
+
setError(e);
|
|
500
|
+
setStatus("error");
|
|
501
|
+
return void 0;
|
|
502
|
+
}
|
|
503
|
+
setStatus("prompting");
|
|
504
|
+
setError(void 0);
|
|
505
|
+
let permission = Notification.permission;
|
|
506
|
+
if (permission === "default") {
|
|
507
|
+
try {
|
|
508
|
+
permission = await Notification.requestPermission();
|
|
509
|
+
} catch (err) {
|
|
510
|
+
const e = {
|
|
511
|
+
code: "unknown",
|
|
512
|
+
message: err instanceof Error ? err.message : String(err)
|
|
513
|
+
};
|
|
514
|
+
setError(e);
|
|
515
|
+
setStatus("error");
|
|
516
|
+
return void 0;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (permission !== "granted") {
|
|
520
|
+
const e = {
|
|
521
|
+
code: "permission_denied",
|
|
522
|
+
message: "Notification permission not granted."
|
|
523
|
+
};
|
|
524
|
+
setError(e);
|
|
525
|
+
setStatus("denied");
|
|
526
|
+
return void 0;
|
|
527
|
+
}
|
|
528
|
+
return new Promise((resolve) => {
|
|
529
|
+
const notif = new Notification(options.title, {
|
|
530
|
+
...options.body !== void 0 ? { body: options.body } : {},
|
|
531
|
+
...options.icon !== void 0 ? { icon: options.icon } : {},
|
|
532
|
+
...options.tag !== void 0 ? { tag: options.tag } : {}
|
|
533
|
+
});
|
|
534
|
+
const finish = (outcome) => {
|
|
535
|
+
const result = {
|
|
536
|
+
outcome,
|
|
537
|
+
...options.tag !== void 0 ? { tag: options.tag } : {}
|
|
538
|
+
};
|
|
539
|
+
setValue(result);
|
|
540
|
+
setStatus("completed");
|
|
541
|
+
resolve(result);
|
|
542
|
+
};
|
|
543
|
+
notif.onclick = () => finish("clicked");
|
|
544
|
+
notif.onclose = () => finish("closed");
|
|
545
|
+
});
|
|
546
|
+
}, [options?.title, options?.body, options?.icon, options?.tag]);
|
|
547
|
+
return {
|
|
548
|
+
value,
|
|
549
|
+
status,
|
|
550
|
+
...error !== void 0 ? { error } : {},
|
|
551
|
+
start
|
|
552
|
+
};
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/useFilePicker.ts
|
|
556
|
+
var import_react5 = require("react");
|
|
557
|
+
function fileToPicked(file) {
|
|
558
|
+
return {
|
|
559
|
+
name: file.name,
|
|
560
|
+
size: file.size,
|
|
561
|
+
type: file.type,
|
|
562
|
+
_file: file
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
var useFilePicker = (options) => {
|
|
566
|
+
const [value, setValue] = (0, import_react5.useState)(void 0);
|
|
567
|
+
const [status, setStatus] = (0, import_react5.useState)("idle");
|
|
568
|
+
const [error, setError] = (0, import_react5.useState)(void 0);
|
|
569
|
+
const start = (0, import_react5.useCallback)(async () => {
|
|
570
|
+
if (typeof document === "undefined") {
|
|
571
|
+
const e = {
|
|
572
|
+
code: "not_supported",
|
|
573
|
+
message: "File picker requires a DOM environment."
|
|
574
|
+
};
|
|
575
|
+
setError(e);
|
|
576
|
+
setStatus("error");
|
|
577
|
+
return void 0;
|
|
578
|
+
}
|
|
579
|
+
setStatus("prompting");
|
|
580
|
+
setError(void 0);
|
|
581
|
+
return new Promise((resolve) => {
|
|
582
|
+
const input = document.createElement("input");
|
|
583
|
+
input.type = "file";
|
|
584
|
+
if (options?.multiple) input.multiple = true;
|
|
585
|
+
if (options?.accept) input.accept = options.accept;
|
|
586
|
+
input.onchange = () => {
|
|
587
|
+
const files = input.files ? Array.from(input.files) : [];
|
|
588
|
+
if (files.length === 0) {
|
|
589
|
+
const e = {
|
|
590
|
+
code: "aborted",
|
|
591
|
+
message: "File picker cancelled by user."
|
|
592
|
+
};
|
|
593
|
+
setError(e);
|
|
594
|
+
setStatus("denied");
|
|
595
|
+
resolve(void 0);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const result = {
|
|
599
|
+
files: files.map(fileToPicked)
|
|
600
|
+
};
|
|
601
|
+
setValue(result);
|
|
602
|
+
setStatus("completed");
|
|
603
|
+
resolve(result);
|
|
604
|
+
};
|
|
605
|
+
input.click();
|
|
606
|
+
});
|
|
607
|
+
}, [options?.multiple, options?.accept]);
|
|
608
|
+
return {
|
|
609
|
+
value,
|
|
610
|
+
status,
|
|
611
|
+
...error !== void 0 ? { error } : {},
|
|
612
|
+
start
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// src/useMicrophone.ts
|
|
617
|
+
var import_react6 = require("react");
|
|
618
|
+
function mapGetUserMediaError(err) {
|
|
619
|
+
if (err instanceof Error) {
|
|
620
|
+
if (err.name === "NotAllowedError") {
|
|
621
|
+
return { code: "permission_denied", message: err.message };
|
|
622
|
+
}
|
|
623
|
+
if (err.name === "NotFoundError" || err.name === "OverconstrainedError") {
|
|
624
|
+
return { code: "not_supported", message: err.message };
|
|
625
|
+
}
|
|
626
|
+
return { code: "unknown", message: err.message };
|
|
627
|
+
}
|
|
628
|
+
return { code: "unknown", message: String(err) };
|
|
629
|
+
}
|
|
630
|
+
var useMicrophone = (options) => {
|
|
631
|
+
const [value, setValue] = (0, import_react6.useState)(void 0);
|
|
632
|
+
const [status, setStatus] = (0, import_react6.useState)("idle");
|
|
633
|
+
const [error, setError] = (0, import_react6.useState)(void 0);
|
|
634
|
+
const recorderRef = (0, import_react6.useRef)(null);
|
|
635
|
+
const streamRef = (0, import_react6.useRef)(null);
|
|
636
|
+
const startedAtRef = (0, import_react6.useRef)(0);
|
|
637
|
+
const resolveRef = (0, import_react6.useRef)(null);
|
|
638
|
+
const start = (0, import_react6.useCallback)(async () => {
|
|
639
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== "function" || typeof MediaRecorder === "undefined") {
|
|
640
|
+
const e = {
|
|
641
|
+
code: "not_supported",
|
|
642
|
+
message: "MediaRecorder / getUserMedia unavailable."
|
|
643
|
+
};
|
|
644
|
+
setError(e);
|
|
645
|
+
setStatus("error");
|
|
646
|
+
return void 0;
|
|
647
|
+
}
|
|
648
|
+
setStatus("prompting");
|
|
649
|
+
setError(void 0);
|
|
650
|
+
let stream;
|
|
651
|
+
try {
|
|
652
|
+
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
653
|
+
} catch (err) {
|
|
654
|
+
const mapped = mapGetUserMediaError(err);
|
|
655
|
+
setError(mapped);
|
|
656
|
+
setStatus(mapped.code === "permission_denied" ? "denied" : "error");
|
|
657
|
+
return void 0;
|
|
658
|
+
}
|
|
659
|
+
streamRef.current = stream;
|
|
660
|
+
const mimeType = options?.mimeType ?? "audio/webm";
|
|
661
|
+
let recorder;
|
|
662
|
+
try {
|
|
663
|
+
recorder = new MediaRecorder(stream, { mimeType });
|
|
664
|
+
} catch {
|
|
665
|
+
recorder = new MediaRecorder(stream);
|
|
666
|
+
}
|
|
667
|
+
recorderRef.current = recorder;
|
|
668
|
+
const chunks = [];
|
|
669
|
+
recorder.ondataavailable = (event) => {
|
|
670
|
+
if (event.data && event.data.size > 0) chunks.push(event.data);
|
|
671
|
+
};
|
|
672
|
+
return new Promise((resolve) => {
|
|
673
|
+
resolveRef.current = resolve;
|
|
674
|
+
recorder.onstop = () => {
|
|
675
|
+
const blob = new Blob(chunks, { type: recorder.mimeType });
|
|
676
|
+
const durationMs = Date.now() - startedAtRef.current;
|
|
677
|
+
const result = {
|
|
678
|
+
blob,
|
|
679
|
+
durationMs,
|
|
680
|
+
mimeType: recorder.mimeType
|
|
681
|
+
};
|
|
682
|
+
setValue(result);
|
|
683
|
+
setStatus("completed");
|
|
684
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
685
|
+
streamRef.current = null;
|
|
686
|
+
recorderRef.current = null;
|
|
687
|
+
resolveRef.current = null;
|
|
688
|
+
resolve(result);
|
|
689
|
+
};
|
|
690
|
+
startedAtRef.current = Date.now();
|
|
691
|
+
recorder.start();
|
|
692
|
+
setStatus("active");
|
|
693
|
+
});
|
|
694
|
+
}, [options?.mimeType]);
|
|
695
|
+
const stop = (0, import_react6.useCallback)(() => {
|
|
696
|
+
if (recorderRef.current && recorderRef.current.state === "recording") {
|
|
697
|
+
recorderRef.current.stop();
|
|
698
|
+
} else {
|
|
699
|
+
setStatus("completed");
|
|
700
|
+
}
|
|
701
|
+
}, []);
|
|
702
|
+
return {
|
|
703
|
+
value,
|
|
704
|
+
status,
|
|
705
|
+
...error !== void 0 ? { error } : {},
|
|
706
|
+
start,
|
|
707
|
+
stop
|
|
708
|
+
};
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// src/useCamera.ts
|
|
712
|
+
var import_react7 = require("react");
|
|
713
|
+
function mapCameraError(err) {
|
|
714
|
+
if (err instanceof Error) {
|
|
715
|
+
if (err.name === "NotAllowedError") {
|
|
716
|
+
return { code: "permission_denied", message: err.message };
|
|
717
|
+
}
|
|
718
|
+
if (err.name === "NotFoundError" || err.name === "OverconstrainedError") {
|
|
719
|
+
return { code: "not_supported", message: err.message };
|
|
720
|
+
}
|
|
721
|
+
return { code: "unknown", message: err.message };
|
|
722
|
+
}
|
|
723
|
+
return { code: "unknown", message: String(err) };
|
|
724
|
+
}
|
|
725
|
+
var useCamera = (options) => {
|
|
726
|
+
const [value, setValue] = (0, import_react7.useState)(void 0);
|
|
727
|
+
const [status, setStatus] = (0, import_react7.useState)("idle");
|
|
728
|
+
const [error, setError] = (0, import_react7.useState)(void 0);
|
|
729
|
+
const streamRef = (0, import_react7.useRef)(null);
|
|
730
|
+
const stop = (0, import_react7.useCallback)(() => {
|
|
731
|
+
if (streamRef.current) {
|
|
732
|
+
streamRef.current.getTracks().forEach((t) => t.stop());
|
|
733
|
+
streamRef.current = null;
|
|
734
|
+
}
|
|
735
|
+
setStatus("completed");
|
|
736
|
+
}, []);
|
|
737
|
+
const start = (0, import_react7.useCallback)(async () => {
|
|
738
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== "function" || typeof document === "undefined") {
|
|
739
|
+
const e = {
|
|
740
|
+
code: "not_supported",
|
|
741
|
+
message: "getUserMedia / DOM unavailable."
|
|
742
|
+
};
|
|
743
|
+
setError(e);
|
|
744
|
+
setStatus("error");
|
|
745
|
+
return void 0;
|
|
746
|
+
}
|
|
747
|
+
setStatus("prompting");
|
|
748
|
+
setError(void 0);
|
|
749
|
+
let stream;
|
|
750
|
+
try {
|
|
751
|
+
stream = await navigator.mediaDevices.getUserMedia({
|
|
752
|
+
video: options?.facingMode ? { facingMode: options.facingMode } : true
|
|
753
|
+
});
|
|
754
|
+
} catch (err) {
|
|
755
|
+
const mapped = mapCameraError(err);
|
|
756
|
+
setError(mapped);
|
|
757
|
+
setStatus(mapped.code === "permission_denied" ? "denied" : "error");
|
|
758
|
+
return void 0;
|
|
759
|
+
}
|
|
760
|
+
streamRef.current = stream;
|
|
761
|
+
setStatus("active");
|
|
762
|
+
try {
|
|
763
|
+
const video = document.createElement("video");
|
|
764
|
+
video.srcObject = stream;
|
|
765
|
+
video.muted = true;
|
|
766
|
+
await video.play();
|
|
767
|
+
await new Promise(
|
|
768
|
+
(resolve) => requestAnimationFrame(() => resolve())
|
|
769
|
+
);
|
|
770
|
+
const canvas = document.createElement("canvas");
|
|
771
|
+
const width = video.videoWidth || 640;
|
|
772
|
+
const height = video.videoHeight || 480;
|
|
773
|
+
canvas.width = width;
|
|
774
|
+
canvas.height = height;
|
|
775
|
+
const ctx = canvas.getContext("2d");
|
|
776
|
+
if (!ctx) {
|
|
777
|
+
const e = {
|
|
778
|
+
code: "unknown",
|
|
779
|
+
message: "2D canvas context unavailable."
|
|
780
|
+
};
|
|
781
|
+
setError(e);
|
|
782
|
+
setStatus("error");
|
|
783
|
+
stop();
|
|
784
|
+
return void 0;
|
|
785
|
+
}
|
|
786
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
787
|
+
const mimeType = options?.mimeType ?? "image/png";
|
|
788
|
+
const dataUrl = options?.quality !== void 0 ? canvas.toDataURL(mimeType, options.quality) : canvas.toDataURL(mimeType);
|
|
789
|
+
const result = { dataUrl, width, height, mimeType };
|
|
790
|
+
setValue(result);
|
|
791
|
+
setStatus("completed");
|
|
792
|
+
stop();
|
|
793
|
+
return result;
|
|
794
|
+
} catch (err) {
|
|
795
|
+
const mapped = mapCameraError(err);
|
|
796
|
+
setError(mapped);
|
|
797
|
+
setStatus("error");
|
|
798
|
+
stop();
|
|
799
|
+
return void 0;
|
|
800
|
+
}
|
|
801
|
+
}, [options?.facingMode, options?.mimeType, options?.quality, stop]);
|
|
802
|
+
return {
|
|
803
|
+
value,
|
|
804
|
+
status,
|
|
805
|
+
...error !== void 0 ? { error } : {},
|
|
806
|
+
start,
|
|
807
|
+
stop
|
|
808
|
+
};
|
|
809
|
+
};
|
|
810
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
811
|
+
0 && (module.exports = {
|
|
812
|
+
CachingGadgetCatalog,
|
|
813
|
+
InMemoryGadgetCatalog,
|
|
814
|
+
WrapperConformanceError,
|
|
815
|
+
createGguiGadget,
|
|
816
|
+
defineGadgetPackage,
|
|
817
|
+
getPublicEnv,
|
|
818
|
+
useCamera,
|
|
819
|
+
useClipboardPaste,
|
|
820
|
+
useClipboardWrite,
|
|
821
|
+
useFilePicker,
|
|
822
|
+
useGeolocation,
|
|
823
|
+
useMicrophone,
|
|
824
|
+
useNotifications
|
|
825
|
+
});
|