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