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