@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/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
+ };