@ensera/plugin-frontend 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2021 @@
1
+ // src/errors.ts
2
+ var PluginFetchError = class extends Error {
3
+ status;
4
+ url;
5
+ requestId;
6
+ payload;
7
+ code;
8
+ constructor(args) {
9
+ super(args.message);
10
+ Object.setPrototypeOf(this, new.target.prototype);
11
+ this.name = args.name ?? "PluginFetchError";
12
+ this.url = args.url;
13
+ this.requestId = args.requestId;
14
+ this.status = args.status;
15
+ this.payload = args.payload;
16
+ this.code = args.code;
17
+ }
18
+ };
19
+ var PluginAuthError = class extends PluginFetchError {
20
+ constructor(args) {
21
+ super({ ...args, name: "PluginAuthError" });
22
+ }
23
+ };
24
+ var PluginForbiddenError = class extends PluginFetchError {
25
+ constructor(args) {
26
+ super({ ...args, name: "PluginForbiddenError" });
27
+ }
28
+ };
29
+ var PluginNotFoundError = class extends PluginFetchError {
30
+ constructor(args) {
31
+ super({ ...args, name: "PluginNotFoundError" });
32
+ }
33
+ };
34
+ var PluginValidationError = class extends PluginFetchError {
35
+ constructor(args) {
36
+ super({ ...args, name: "PluginValidationError" });
37
+ }
38
+ };
39
+ var PluginRateLimitError = class extends PluginFetchError {
40
+ constructor(args) {
41
+ super({ ...args, name: "PluginRateLimitError" });
42
+ }
43
+ };
44
+ var PluginServerError = class extends PluginFetchError {
45
+ constructor(args) {
46
+ super({ ...args, name: "PluginServerError" });
47
+ }
48
+ };
49
+ var PluginNetworkError = class extends PluginFetchError {
50
+ constructor(args) {
51
+ super({ ...args, name: "PluginNetworkError" });
52
+ }
53
+ };
54
+ var PluginResponseError = class extends PluginFetchError {
55
+ constructor(args) {
56
+ super({ ...args, name: "PluginResponseError" });
57
+ }
58
+ };
59
+ var PluginStorageError = class extends Error {
60
+ constructor(message) {
61
+ super(message);
62
+ Object.setPrototypeOf(this, new.target.prototype);
63
+ this.name = "PluginStorageError";
64
+ }
65
+ };
66
+ var PluginStorageQuotaError = class extends PluginStorageError {
67
+ constructor(message = "plugin.storage: quota exceeded for this instance") {
68
+ super(message);
69
+ Object.setPrototypeOf(this, new.target.prototype);
70
+ this.name = "PluginStorageQuotaError";
71
+ }
72
+ };
73
+ var PluginFetchInputError = class extends Error {
74
+ constructor(message) {
75
+ super(message);
76
+ Object.setPrototypeOf(this, new.target.prototype);
77
+ this.name = "PluginFetchInputError";
78
+ }
79
+ };
80
+
81
+ // src/fetch.ts
82
+ var SDK_VERSION = "0.1.0";
83
+ function sleep(ms) {
84
+ return new Promise((r) => setTimeout(r, ms));
85
+ }
86
+ function genRequestId() {
87
+ const c = globalThis.crypto;
88
+ if (c?.randomUUID) return c.randomUUID();
89
+ return `req_${Math.random().toString(16).slice(2)}_${Date.now()}`;
90
+ }
91
+ function assertRelativePath(path) {
92
+ if (!path || typeof path !== "string") {
93
+ throw new PluginFetchInputError(
94
+ "plugin.fetch: path must be a non-empty string"
95
+ );
96
+ }
97
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(path) || path.startsWith("//")) {
98
+ throw new PluginFetchInputError(
99
+ "plugin.fetch: absolute URLs are not allowed; use a relative path like '/tasks'"
100
+ );
101
+ }
102
+ if (!path.startsWith("/")) {
103
+ throw new PluginFetchInputError("plugin.fetch: path must start with '/'");
104
+ }
105
+ if (path.includes("\\")) {
106
+ throw new PluginFetchInputError(
107
+ "plugin.fetch: backslashes are not allowed in paths"
108
+ );
109
+ }
110
+ if (path.includes("/../") || path.endsWith("/..") || path.startsWith("/..")) {
111
+ throw new PluginFetchInputError(
112
+ "plugin.fetch: path traversal ('..') is not allowed"
113
+ );
114
+ }
115
+ }
116
+ function joinUrl(apiBase, path) {
117
+ const base = apiBase.replace(/\/+$/, "");
118
+ return `${base}${path}`;
119
+ }
120
+ function isJsonContentType(ct) {
121
+ if (!ct) return false;
122
+ return ct.toLowerCase().includes("application/json");
123
+ }
124
+ async function safeParseJson(res) {
125
+ try {
126
+ return await res.json();
127
+ } catch {
128
+ return void 0;
129
+ }
130
+ }
131
+ function extractMessageAndCode(payload) {
132
+ if (!payload || typeof payload !== "object") return {};
133
+ const p = payload;
134
+ const message = typeof p.message === "string" ? p.message : void 0;
135
+ const code = typeof p.code === "string" ? p.code : void 0;
136
+ return { message, code };
137
+ }
138
+ function makeTypedError(args) {
139
+ const { status, url, requestId, payload } = args;
140
+ const { message, code } = extractMessageAndCode(payload);
141
+ const msg = message ?? `Request failed with status ${status}`;
142
+ if (status === 401)
143
+ return new PluginAuthError({
144
+ message: msg,
145
+ url,
146
+ requestId,
147
+ status,
148
+ payload,
149
+ code
150
+ });
151
+ if (status === 403)
152
+ return new PluginForbiddenError({
153
+ message: msg,
154
+ url,
155
+ requestId,
156
+ status,
157
+ payload,
158
+ code
159
+ });
160
+ if (status === 404)
161
+ return new PluginNotFoundError({
162
+ message: msg,
163
+ url,
164
+ requestId,
165
+ status,
166
+ payload,
167
+ code
168
+ });
169
+ if (status === 429)
170
+ return new PluginRateLimitError({
171
+ message: msg,
172
+ url,
173
+ requestId,
174
+ status,
175
+ payload,
176
+ code
177
+ });
178
+ if (status === 400 || status === 422) {
179
+ return new PluginValidationError({
180
+ message: msg,
181
+ url,
182
+ requestId,
183
+ status,
184
+ payload,
185
+ code
186
+ });
187
+ }
188
+ if (status >= 500)
189
+ return new PluginServerError({
190
+ message: msg,
191
+ url,
192
+ requestId,
193
+ status,
194
+ payload,
195
+ code
196
+ });
197
+ return new PluginResponseError({
198
+ message: msg,
199
+ url,
200
+ requestId,
201
+ status,
202
+ payload,
203
+ code
204
+ });
205
+ }
206
+ function shouldRetry(args) {
207
+ const { attempt, max, method, status, networkError, allowNonIdempotent } = args;
208
+ if (attempt >= max) return false;
209
+ const m = method.toUpperCase();
210
+ const idempotent = m === "GET" || m === "HEAD" || m === "PUT" || m === "DELETE" || m === "OPTIONS";
211
+ if (!idempotent && !allowNonIdempotent) return false;
212
+ if (networkError) return true;
213
+ if (status === 502 || status === 503 || status === 504) return true;
214
+ return false;
215
+ }
216
+ async function parseResponse(res, expect) {
217
+ if (expect === "none") return void 0;
218
+ if (res.status === 204) return null;
219
+ if (expect === "blob") return await res.blob();
220
+ if (expect === "text") return await res.text();
221
+ const ct = res.headers.get("content-type");
222
+ if (isJsonContentType(ct)) {
223
+ return await res.json();
224
+ }
225
+ throw new Error("NON_JSON");
226
+ }
227
+ function getLogger(ctx) {
228
+ const anyCtx = ctx;
229
+ const logger = anyCtx?.logger;
230
+ const error = typeof logger?.error === "function" ? logger.error.bind(logger) : console.error.bind(console);
231
+ const warn = typeof logger?.warn === "function" ? logger.warn.bind(logger) : console.warn.bind(console);
232
+ const info = typeof logger?.info === "function" ? logger.info.bind(logger) : console.info.bind(console);
233
+ const debug = typeof logger?.debug === "function" ? logger.debug.bind(logger) : console.debug.bind(console);
234
+ return { error, warn, info, debug };
235
+ }
236
+ function classifyNetworkFailure(err) {
237
+ const msg = String(err?.message ?? err ?? "").toLowerCase();
238
+ if (msg.includes("cors"))
239
+ return {
240
+ code: "CORS",
241
+ hint: "Browser blocked the response due to CORS policy/preflight."
242
+ };
243
+ if (msg.includes("mixed content"))
244
+ return {
245
+ code: "DNS_OR_TLS",
246
+ hint: "Mixed content (HTTPS page calling HTTP API) or blocked by browser."
247
+ };
248
+ if (msg.includes("ssl") || msg.includes("tls") || msg.includes("certificate")) {
249
+ return {
250
+ code: "DNS_OR_TLS",
251
+ hint: "TLS/SSL handshake or certificate issue."
252
+ };
253
+ }
254
+ if (msg.includes("dns") || msg.includes("name not resolved") || msg.includes("not known")) {
255
+ return {
256
+ code: "DNS_OR_TLS",
257
+ hint: "DNS resolution failure or invalid hostname."
258
+ };
259
+ }
260
+ return {
261
+ code: "UNREACHABLE",
262
+ hint: "Server unreachable (network down, CORS, DNS, firewall, or connection issue)."
263
+ };
264
+ }
265
+ function createPluginFetch(ctx) {
266
+ const log = getLogger(ctx);
267
+ return async function pluginFetch(path, options = {}) {
268
+ assertRelativePath(path);
269
+ const url = joinUrl(ctx.apiBase, path);
270
+ const requestId = options.requestId ?? genRequestId();
271
+ if (options.json !== void 0 && options.body !== void 0) {
272
+ throw new PluginFetchInputError(
273
+ "plugin.fetch: provide either 'json' or 'body', not both"
274
+ );
275
+ }
276
+ const method = (options.method ?? "GET").toUpperCase();
277
+ const expect = options.expect ?? "json";
278
+ const timeoutMs = options.timeoutMs ?? 3e4;
279
+ const retryCount = options.retry?.count ?? 0;
280
+ const backoffMs = options.retry?.backoffMs ?? 250;
281
+ if (typeof navigator !== "undefined" && navigator && navigator.onLine === false) {
282
+ const payload = { navigatorOnLine: false };
283
+ log.error("[plugin.fetch] OFFLINE", { url, requestId, method, payload });
284
+ throw new PluginNetworkError({
285
+ message: "Client appears to be offline (navigator.onLine=false)",
286
+ url,
287
+ requestId,
288
+ payload,
289
+ code: "OFFLINE"
290
+ });
291
+ }
292
+ const headers = new Headers(options.headers ?? {});
293
+ if (!headers.has("Authorization") && ctx.token) {
294
+ headers.set("Authorization", `Bearer ${ctx.token}`);
295
+ }
296
+ headers.set("X-Ensera-Plugin", ctx.featureSlug);
297
+ headers.set("X-Ensera-Instance", ctx.instanceId);
298
+ headers.set("X-Ensera-SDK", SDK_VERSION);
299
+ headers.set("X-Request-Id", requestId);
300
+ let body = options.body;
301
+ if (options.json !== void 0) {
302
+ body = JSON.stringify(options.json);
303
+ if (!headers.has("Content-Type"))
304
+ headers.set("Content-Type", "application/json");
305
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
306
+ } else {
307
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
308
+ }
309
+ const controller = new AbortController();
310
+ let timedOut = false;
311
+ const timeout = setTimeout(() => {
312
+ timedOut = true;
313
+ controller.abort();
314
+ }, timeoutMs);
315
+ const callerSignal = options.signal;
316
+ const onCallerAbort = () => controller.abort();
317
+ if (callerSignal) {
318
+ if (callerSignal.aborted) controller.abort();
319
+ else
320
+ callerSignal.addEventListener("abort", onCallerAbort, { once: true });
321
+ }
322
+ try {
323
+ let lastStatus;
324
+ let lastNetworkError = false;
325
+ for (let attempt = 0; attempt <= retryCount; attempt++) {
326
+ lastNetworkError = false;
327
+ try {
328
+ const res = await fetch(url, {
329
+ ...options,
330
+ method,
331
+ headers,
332
+ body,
333
+ signal: controller.signal
334
+ });
335
+ lastStatus = res.status;
336
+ if (!res.ok) {
337
+ const ct = res.headers.get("content-type");
338
+ const payload2 = isJsonContentType(ct) ? await safeParseJson(res) : await res.text().catch(() => void 0);
339
+ const okRetry = shouldRetry({
340
+ attempt,
341
+ max: retryCount,
342
+ method,
343
+ status: res.status,
344
+ networkError: false,
345
+ allowNonIdempotent: false
346
+ // keep conservative; caller can override by using retry only on GET
347
+ });
348
+ if (okRetry) {
349
+ log.warn("[plugin.fetch] retrying http error", {
350
+ url,
351
+ requestId,
352
+ method,
353
+ attempt,
354
+ status: res.status
355
+ });
356
+ await sleep(backoffMs * (attempt + 1));
357
+ continue;
358
+ }
359
+ const err = makeTypedError({
360
+ status: res.status,
361
+ url,
362
+ requestId,
363
+ payload: payload2
364
+ });
365
+ log.error("[plugin.fetch] http error", {
366
+ url,
367
+ requestId,
368
+ method,
369
+ status: res.status,
370
+ payload: payload2,
371
+ errorName: err?.name
372
+ });
373
+ throw err;
374
+ }
375
+ let data;
376
+ try {
377
+ data = await parseResponse(res, expect);
378
+ } catch (e) {
379
+ if (e?.message === "NON_JSON") {
380
+ const payloadText = await res.text().catch(() => void 0);
381
+ const err = new PluginResponseError({
382
+ message: "Expected JSON response but received non-JSON content",
383
+ url,
384
+ requestId,
385
+ status: res.status,
386
+ payload: payloadText,
387
+ code: "NON_JSON_RESPONSE"
388
+ });
389
+ log.error("[plugin.fetch] non-json response", {
390
+ url,
391
+ requestId,
392
+ method,
393
+ status: res.status,
394
+ payload: payloadText
395
+ });
396
+ throw err;
397
+ }
398
+ throw e;
399
+ }
400
+ log.debug?.("[plugin.fetch] success", {
401
+ url,
402
+ requestId,
403
+ method,
404
+ status: res.status
405
+ });
406
+ return {
407
+ data,
408
+ status: res.status,
409
+ headers: res.headers,
410
+ url,
411
+ requestId
412
+ };
413
+ } catch (err) {
414
+ if (controller.signal.aborted) {
415
+ const code = timedOut ? "TIMEOUT" : "ABORTED";
416
+ const message = timedOut ? "Request timed out" : "Request aborted";
417
+ log.error("[plugin.fetch] aborted", {
418
+ url,
419
+ requestId,
420
+ method,
421
+ attempt,
422
+ code,
423
+ timeoutMs,
424
+ originalError: String(err?.message ?? err)
425
+ });
426
+ throw new PluginNetworkError({
427
+ message,
428
+ url,
429
+ requestId,
430
+ code,
431
+ payload: {
432
+ timeoutMs,
433
+ timedOut,
434
+ originalError: String(err?.message ?? err)
435
+ }
436
+ });
437
+ }
438
+ lastNetworkError = true;
439
+ const okRetry = shouldRetry({
440
+ attempt,
441
+ max: retryCount,
442
+ method,
443
+ status: lastStatus,
444
+ networkError: true,
445
+ allowNonIdempotent: false
446
+ });
447
+ if (okRetry) {
448
+ log.warn("[plugin.fetch] retrying network error", {
449
+ url,
450
+ requestId,
451
+ method,
452
+ attempt,
453
+ lastStatus,
454
+ originalError: String(err?.message ?? err)
455
+ });
456
+ await sleep(backoffMs * (attempt + 1));
457
+ continue;
458
+ }
459
+ let detected = {
460
+ code: "NETWORK_ERROR",
461
+ hint: "Network error."
462
+ };
463
+ if (typeof navigator !== "undefined" && navigator && navigator.onLine === false) {
464
+ detected = {
465
+ code: "OFFLINE",
466
+ hint: "Client appears offline (navigator.onLine=false)."
467
+ };
468
+ } else {
469
+ detected = classifyNetworkFailure(err);
470
+ }
471
+ const payload2 = {
472
+ originalError: String(err?.message ?? err),
473
+ detectedCode: detected.code,
474
+ hint: detected.hint,
475
+ lastStatus
476
+ };
477
+ log.error("[plugin.fetch] network error", {
478
+ url,
479
+ requestId,
480
+ method,
481
+ payload: payload2
482
+ });
483
+ throw new PluginNetworkError({
484
+ message: detected.hint,
485
+ url,
486
+ requestId,
487
+ payload: payload2,
488
+ code: detected.code
489
+ });
490
+ }
491
+ }
492
+ const payload = { status: lastStatus, networkError: lastNetworkError };
493
+ log.error("[plugin.fetch] retry exhausted", {
494
+ url,
495
+ requestId,
496
+ method,
497
+ payload
498
+ });
499
+ throw new PluginNetworkError({
500
+ message: "Request failed after retries",
501
+ url,
502
+ requestId,
503
+ payload,
504
+ code: "NETWORK_ERROR"
505
+ });
506
+ } finally {
507
+ clearTimeout(timeout);
508
+ if (callerSignal)
509
+ callerSignal.removeEventListener("abort", onCallerAbort);
510
+ }
511
+ };
512
+ }
513
+
514
+ // src/storage.ts
515
+ function assertKey(key) {
516
+ if (!key || typeof key !== "string") {
517
+ throw new PluginStorageError(
518
+ "plugin.storage: key must be a non-empty string"
519
+ );
520
+ }
521
+ if (key.includes(":")) {
522
+ throw new PluginStorageError("plugin.storage: key must not include ':'");
523
+ }
524
+ if (key.length > 200) {
525
+ throw new PluginStorageError("plugin.storage: key too long");
526
+ }
527
+ }
528
+ function enc(part) {
529
+ return encodeURIComponent(part);
530
+ }
531
+ function makeStorageNamespace(ctx, version = 1) {
532
+ return `ens:v${version}:${enc(ctx.featureSlug)}:${enc(ctx.workspaceId)}:${enc(
533
+ ctx.spaceId
534
+ )}:${enc(ctx.instanceId)}:${enc(ctx.userId)}`;
535
+ }
536
+ function createPluginStorage(ctx) {
537
+ const namespace = makeStorageNamespace(ctx, 1);
538
+ const prefix = `${namespace}:`;
539
+ const backend = "localStorage";
540
+ function fullKey(key) {
541
+ assertKey(key);
542
+ return `${prefix}${key}`;
543
+ }
544
+ function isOurs(k) {
545
+ return k.startsWith(prefix);
546
+ }
547
+ function estimateBytes(s) {
548
+ return s.length * 2;
549
+ }
550
+ function approxNamespaceBytesUsed() {
551
+ let bytes = 0;
552
+ for (let i = 0; i < localStorage.length; i++) {
553
+ const k = localStorage.key(i);
554
+ if (!k || !isOurs(k)) continue;
555
+ const v = localStorage.getItem(k) ?? "";
556
+ bytes += estimateBytes(k) + estimateBytes(v);
557
+ }
558
+ return bytes;
559
+ }
560
+ function enforceQuota(nextValue) {
561
+ const MAX = 1e6;
562
+ const current = approxNamespaceBytesUsed();
563
+ const next = current + estimateBytes(nextValue);
564
+ if (next > MAX) {
565
+ throw new PluginStorageQuotaError();
566
+ }
567
+ }
568
+ const api = {
569
+ getItem(key) {
570
+ const k = fullKey(key);
571
+ try {
572
+ return localStorage.getItem(k);
573
+ } catch (e) {
574
+ const msg = String(e?.message ?? e);
575
+ throw new PluginStorageError(`plugin.storage.getItem failed: ${msg}`);
576
+ }
577
+ },
578
+ setItem(key, value) {
579
+ const k = fullKey(key);
580
+ if (typeof value !== "string") {
581
+ throw new PluginStorageError("plugin.storage: value must be a string");
582
+ }
583
+ try {
584
+ enforceQuota(value);
585
+ localStorage.setItem(k, value);
586
+ } catch (e) {
587
+ const msg = String(e?.message ?? e);
588
+ const name = String(e?.name ?? "");
589
+ if (name.toLowerCase().includes("quota") || msg.toLowerCase().includes("quota")) {
590
+ throw new PluginStorageQuotaError(
591
+ "plugin.storage: browser quota exceeded"
592
+ );
593
+ }
594
+ if (e instanceof PluginStorageQuotaError) throw e;
595
+ throw new PluginStorageError(`plugin.storage.setItem failed: ${msg}`);
596
+ }
597
+ },
598
+ removeItem(key) {
599
+ const k = fullKey(key);
600
+ try {
601
+ localStorage.removeItem(k);
602
+ } catch (e) {
603
+ const msg = String(e?.message ?? e);
604
+ throw new PluginStorageError(
605
+ `plugin.storage.removeItem failed: ${msg}`
606
+ );
607
+ }
608
+ },
609
+ clear() {
610
+ try {
611
+ const keysToRemove = [];
612
+ for (let i = 0; i < localStorage.length; i++) {
613
+ const k = localStorage.key(i);
614
+ if (k && isOurs(k)) keysToRemove.push(k);
615
+ }
616
+ for (const k of keysToRemove) localStorage.removeItem(k);
617
+ } catch (e) {
618
+ const msg = String(e?.message ?? e);
619
+ throw new PluginStorageError(`plugin.storage.clear failed: ${msg}`);
620
+ }
621
+ },
622
+ keys() {
623
+ try {
624
+ const out = [];
625
+ for (let i = 0; i < localStorage.length; i++) {
626
+ const k = localStorage.key(i);
627
+ if (!k || !isOurs(k)) continue;
628
+ out.push(k.slice(prefix.length));
629
+ }
630
+ return out.sort();
631
+ } catch (e) {
632
+ const msg = String(e?.message ?? e);
633
+ throw new PluginStorageError(`plugin.storage.keys failed: ${msg}`);
634
+ }
635
+ },
636
+ getJSON(key) {
637
+ const raw = api.getItem(key);
638
+ if (raw == null) return null;
639
+ try {
640
+ return JSON.parse(raw);
641
+ } catch {
642
+ throw new PluginStorageError(
643
+ `plugin.storage.getJSON: invalid JSON stored at key '${key}'`
644
+ );
645
+ }
646
+ },
647
+ setJSON(key, value) {
648
+ const raw = JSON.stringify(value);
649
+ api.setItem(key, raw);
650
+ },
651
+ info() {
652
+ let bytes;
653
+ try {
654
+ bytes = approxNamespaceBytesUsed();
655
+ } catch {
656
+ bytes = void 0;
657
+ }
658
+ return { namespace, backend, approxBytesUsed: bytes };
659
+ }
660
+ };
661
+ return api;
662
+ }
663
+
664
+ // src/log.ts
665
+ function redact(meta) {
666
+ if (!meta) return meta;
667
+ const out = {};
668
+ for (const [k, v] of Object.entries(meta)) {
669
+ const lk = k.toLowerCase();
670
+ if (["password", "token", "authorization", "cookie", "secret", "key"].some((x) => lk.includes(x))) {
671
+ out[k] = "[REDACTED]";
672
+ } else {
673
+ out[k] = v;
674
+ }
675
+ }
676
+ return out;
677
+ }
678
+ function createPluginLogger(ctx) {
679
+ function emit(level, message, meta) {
680
+ const event = {
681
+ level,
682
+ message,
683
+ meta: redact(meta),
684
+ tags: {
685
+ featureSlug: ctx.featureSlug,
686
+ instanceId: ctx.instanceId,
687
+ workspaceId: ctx.workspaceId,
688
+ spaceId: ctx.spaceId,
689
+ userId: ctx.userId
690
+ },
691
+ ts: Date.now()
692
+ };
693
+ const fn = level === "debug" ? console.debug : level === "info" ? console.info : level === "warn" ? console.warn : console.error;
694
+ fn(`[${ctx.featureSlug}] ${message}`, event);
695
+ }
696
+ return {
697
+ debug(message, meta) {
698
+ emit("debug", message, meta);
699
+ },
700
+ info(message, meta) {
701
+ emit("info", message, meta);
702
+ },
703
+ warn(message, meta) {
704
+ emit("warn", message, meta);
705
+ },
706
+ error(message, meta) {
707
+ emit("error", message, meta);
708
+ }
709
+ };
710
+ }
711
+
712
+ // src/actions.ts
713
+ function defineActions(actions) {
714
+ return actions;
715
+ }
716
+ var PluginUnknownActionError = class extends Error {
717
+ actionId;
718
+ constructor(actionId) {
719
+ super(`Unknown actionId: ${actionId}`);
720
+ Object.setPrototypeOf(this, new.target.prototype);
721
+ this.name = "PluginUnknownActionError";
722
+ this.actionId = actionId;
723
+ }
724
+ };
725
+ async function runActionSafe(args) {
726
+ const { actions, actionId, call } = args;
727
+ if (!actions[actionId]) throw new PluginUnknownActionError(actionId);
728
+ return await call();
729
+ }
730
+
731
+ // src/notification/notify.ts
732
+ var NOTIFICATION_TIMEOUT = 5e3;
733
+ function genRequestId2() {
734
+ const c = globalThis.crypto;
735
+ if (c?.randomUUID) return c.randomUUID();
736
+ return `notif_${Math.random().toString(16).slice(2)}_${Date.now()}`;
737
+ }
738
+ function getCoreOrigin(ctx) {
739
+ if (ctx.coreOrigin) return ctx.coreOrigin;
740
+ try {
741
+ const url = new URL(ctx.apiBase);
742
+ return url.origin;
743
+ } catch {
744
+ return window.location.origin;
745
+ }
746
+ }
747
+ function createPluginNotify(ctx) {
748
+ const coreOrigin = getCoreOrigin(ctx);
749
+ function sendViaPostMessage(notification, options) {
750
+ return new Promise((resolve, reject) => {
751
+ const requestId = genRequestId2();
752
+ let resolved = false;
753
+ const timeout = setTimeout(() => {
754
+ if (resolved) return;
755
+ resolved = true;
756
+ cleanup();
757
+ reject(
758
+ new Error(
759
+ `Notification request timeout after ${NOTIFICATION_TIMEOUT}ms`
760
+ )
761
+ );
762
+ }, NOTIFICATION_TIMEOUT);
763
+ const handleMessage = (event) => {
764
+ if (resolved) return;
765
+ if (event.origin !== coreOrigin) return;
766
+ const msg = event.data;
767
+ if (msg?.type !== "ENSERA_NOTIFICATION_RESPONSE") return;
768
+ if (msg?.requestId !== requestId) return;
769
+ resolved = true;
770
+ cleanup();
771
+ if (msg.ok) {
772
+ resolve({
773
+ success: true,
774
+ id: msg.id,
775
+ sent: msg.sent,
776
+ emailSent: msg.emailSent,
777
+ pushSent: msg.pushSent
778
+ });
779
+ } else {
780
+ resolve({
781
+ success: false,
782
+ error: msg.error
783
+ });
784
+ }
785
+ };
786
+ const cleanup = () => {
787
+ clearTimeout(timeout);
788
+ window.removeEventListener("message", handleMessage);
789
+ };
790
+ window.addEventListener("message", handleMessage);
791
+ const request = {
792
+ type: "ENSERA_SEND_NOTIFICATION",
793
+ requestId,
794
+ payload: {
795
+ ...notification,
796
+ featureSlug: ctx.featureSlug,
797
+ options
798
+ }
799
+ };
800
+ window.parent.postMessage(request, coreOrigin);
801
+ });
802
+ }
803
+ async function send(notification, options) {
804
+ if (!notification.userId?.trim()) {
805
+ return {
806
+ success: false,
807
+ error: "userId is required"
808
+ };
809
+ }
810
+ if (!notification.type?.trim()) {
811
+ return {
812
+ success: false,
813
+ error: "type is required"
814
+ };
815
+ }
816
+ if (!notification.title?.trim()) {
817
+ return {
818
+ success: false,
819
+ error: "title is required"
820
+ };
821
+ }
822
+ if (!notification.message?.trim()) {
823
+ return {
824
+ success: false,
825
+ error: "message is required"
826
+ };
827
+ }
828
+ return sendViaPostMessage(notification, options);
829
+ }
830
+ async function sendBulk(notifications, options) {
831
+ if (!notifications?.length) {
832
+ return {
833
+ success: false,
834
+ count: 0,
835
+ errors: [
836
+ { index: 0, userId: "", error: "notifications array is empty" }
837
+ ]
838
+ };
839
+ }
840
+ const results = await Promise.allSettled(
841
+ notifications.map((n) => send(n, options))
842
+ );
843
+ let count = 0;
844
+ const errors = [];
845
+ results.forEach((result, index) => {
846
+ if (result.status === "fulfilled" && result.value.success) {
847
+ count++;
848
+ } else {
849
+ const error = result.status === "rejected" ? result.reason?.message || "Unknown error" : result.value.error || "Failed to send";
850
+ errors.push({
851
+ index,
852
+ userId: notifications[index]?.userId || "",
853
+ error
854
+ });
855
+ }
856
+ });
857
+ return {
858
+ success: errors.length === 0,
859
+ count,
860
+ errors: errors.length > 0 ? errors : void 0
861
+ };
862
+ }
863
+ async function sendToSelf(notification, options) {
864
+ return send(
865
+ {
866
+ ...notification,
867
+ userId: ctx.userId
868
+ },
869
+ options
870
+ );
871
+ }
872
+ async function requestPermission() {
873
+ if (!("Notification" in window)) {
874
+ throw new Error("Browser notifications not supported");
875
+ }
876
+ if (Notification.permission === "granted") {
877
+ return "granted";
878
+ }
879
+ if (Notification.permission === "denied") {
880
+ return "denied";
881
+ }
882
+ return await Notification.requestPermission();
883
+ }
884
+ return {
885
+ send,
886
+ sendBulk,
887
+ sendToSelf,
888
+ requestPermission
889
+ };
890
+ }
891
+
892
+ // src/runtime.ts
893
+ function createPluginRuntime(ctx) {
894
+ const fetcher = createPluginFetch(ctx);
895
+ const fetch2 = (path, options) => fetcher(path, options);
896
+ return {
897
+ ctx,
898
+ fetch: fetch2,
899
+ get: (path, options = {}) => fetch2(path, { ...options, method: "GET" }),
900
+ post: (path, options = {}) => fetch2(path, { ...options, method: "POST" }),
901
+ put: (path, options = {}) => fetch2(path, { ...options, method: "PUT" }),
902
+ patch: (path, options = {}) => fetch2(path, { ...options, method: "PATCH" }),
903
+ delete: (path, options = {}) => fetch2(path, { ...options, method: "DELETE" }),
904
+ storage: createPluginStorage(ctx),
905
+ log: createPluginLogger(ctx),
906
+ notify: createPluginNotify(ctx)
907
+ };
908
+ }
909
+ function attachActionDispatcher(args) {
910
+ const { actions, runtime } = args;
911
+ return async ({ requestId, actionId, payload }) => {
912
+ try {
913
+ return await runActionSafe({
914
+ actions,
915
+ actionId,
916
+ payload,
917
+ call: () => actions[actionId]({ actionId, payload, ctx: runtime.ctx, runtime })
918
+ });
919
+ } catch (e) {
920
+ runtime.log.error("Action failed", {
921
+ requestId,
922
+ actionId,
923
+ name: String(e?.name ?? "Error"),
924
+ message: String(e?.message ?? e),
925
+ code: e?.code ? String(e.code) : null
926
+ });
927
+ if (e instanceof PluginUnknownActionError) throw e;
928
+ throw e;
929
+ }
930
+ };
931
+ }
932
+
933
+ // src/ui/context-menu-shell.tsx
934
+ import { jsx } from "react/jsx-runtime";
935
+ function ContextMenuShell({ children }) {
936
+ return /* @__PURE__ */ jsx(
937
+ "div",
938
+ {
939
+ style: {
940
+ width: "100%",
941
+ height: 44,
942
+ display: "flex",
943
+ alignItems: "center",
944
+ gap: 8,
945
+ padding: "0 10px",
946
+ boxSizing: "border-box",
947
+ fontFamily: "ui-sans-serif, system-ui",
948
+ overflow: "hidden"
949
+ },
950
+ children
951
+ }
952
+ );
953
+ }
954
+
955
+ // src/storage_indexeddb.ts
956
+ function assertKey2(key) {
957
+ if (!key || typeof key !== "string") {
958
+ throw new PluginStorageError(
959
+ "plugin.storage: key must be a non-empty string"
960
+ );
961
+ }
962
+ if (key.includes(":")) {
963
+ throw new PluginStorageError("plugin.storage: key must not include ':'");
964
+ }
965
+ if (key.length > 200) {
966
+ throw new PluginStorageError("plugin.storage: key too long");
967
+ }
968
+ }
969
+ function normalizeQuotaError(e) {
970
+ const msg = String(e?.message ?? e);
971
+ const name = String(e?.name ?? "");
972
+ return name.toLowerCase().includes("quota") || msg.toLowerCase().includes("quota") || msg.toLowerCase().includes("exceeded") || msg.toLowerCase().includes("storage");
973
+ }
974
+ function openDb(dbName, storeName) {
975
+ return new Promise((resolve, reject) => {
976
+ const req = indexedDB.open(dbName, 1);
977
+ req.onupgradeneeded = () => {
978
+ const db = req.result;
979
+ if (!db.objectStoreNames.contains(storeName)) {
980
+ db.createObjectStore(storeName);
981
+ }
982
+ };
983
+ req.onsuccess = () => resolve(req.result);
984
+ req.onerror = () => reject(req.error);
985
+ });
986
+ }
987
+ function withStore(db, storeName, mode, fn) {
988
+ return new Promise((resolve, reject) => {
989
+ const tx = db.transaction(storeName, mode);
990
+ const store = tx.objectStore(storeName);
991
+ const req = fn(store);
992
+ req.onsuccess = () => resolve(req.result);
993
+ req.onerror = () => reject(req.error);
994
+ tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted"));
995
+ });
996
+ }
997
+ function createPluginStorageIndexedDB(ctx) {
998
+ const namespace = makeStorageNamespace(ctx, 1);
999
+ const prefix = `${namespace}:`;
1000
+ const backend = "indexedDB";
1001
+ const DB_NAME = "ens-plugin-storage";
1002
+ const STORE_NAME = "kv";
1003
+ let dbPromise = null;
1004
+ const getDb = () => dbPromise ??= openDb(DB_NAME, STORE_NAME);
1005
+ function fullKey(key) {
1006
+ assertKey2(key);
1007
+ return `${prefix}${key}`;
1008
+ }
1009
+ function isOurs(k) {
1010
+ return k.startsWith(prefix);
1011
+ }
1012
+ let opQueue = Promise.resolve();
1013
+ function enqueue(op) {
1014
+ const run = opQueue.then(op, op);
1015
+ opQueue = run.then(
1016
+ () => void 0,
1017
+ () => void 0
1018
+ );
1019
+ return run;
1020
+ }
1021
+ const api = {
1022
+ async getItem(key) {
1023
+ const k = fullKey(key);
1024
+ try {
1025
+ const db = await getDb();
1026
+ const val = await withStore(
1027
+ db,
1028
+ STORE_NAME,
1029
+ "readonly",
1030
+ (s) => s.get(k)
1031
+ );
1032
+ return val ?? null;
1033
+ } catch (e) {
1034
+ const msg = String(e?.message ?? e);
1035
+ throw new PluginStorageError(`plugin.storage.getItem failed: ${msg}`);
1036
+ }
1037
+ },
1038
+ async setItem(key, value) {
1039
+ const k = fullKey(key);
1040
+ if (typeof value !== "string") {
1041
+ throw new PluginStorageError("plugin.storage: value must be a string");
1042
+ }
1043
+ return enqueue(async () => {
1044
+ try {
1045
+ const db = await getDb();
1046
+ await withStore(db, STORE_NAME, "readwrite", (s) => s.put(value, k));
1047
+ } catch (e) {
1048
+ if (normalizeQuotaError(e)) {
1049
+ throw new PluginStorageQuotaError(
1050
+ "plugin.storage: browser quota exceeded"
1051
+ );
1052
+ }
1053
+ const msg = String(e?.message ?? e);
1054
+ throw new PluginStorageError(`plugin.storage.setItem failed: ${msg}`);
1055
+ }
1056
+ });
1057
+ },
1058
+ async removeItem(key) {
1059
+ const k = fullKey(key);
1060
+ return enqueue(async () => {
1061
+ try {
1062
+ const db = await getDb();
1063
+ await withStore(db, STORE_NAME, "readwrite", (s) => s.delete(k));
1064
+ } catch (e) {
1065
+ const msg = String(e?.message ?? e);
1066
+ throw new PluginStorageError(
1067
+ `plugin.storage.removeItem failed: ${msg}`
1068
+ );
1069
+ }
1070
+ });
1071
+ },
1072
+ async clear() {
1073
+ return enqueue(async () => {
1074
+ try {
1075
+ const db = await getDb();
1076
+ const keys = await this.keys();
1077
+ const tx = db.transaction(STORE_NAME, "readwrite");
1078
+ const store = tx.objectStore(STORE_NAME);
1079
+ for (const key of keys) {
1080
+ store.delete(fullKey(key));
1081
+ }
1082
+ await new Promise((resolve, reject) => {
1083
+ tx.oncomplete = () => resolve();
1084
+ tx.onerror = () => reject(tx.error);
1085
+ tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted"));
1086
+ });
1087
+ } catch (e) {
1088
+ const msg = String(e?.message ?? e);
1089
+ throw new PluginStorageError(`plugin.storage.clear failed: ${msg}`);
1090
+ }
1091
+ });
1092
+ },
1093
+ async keys() {
1094
+ try {
1095
+ const db = await getDb();
1096
+ const allKeys = await withStore(
1097
+ db,
1098
+ STORE_NAME,
1099
+ "readonly",
1100
+ (s) => s.getAllKeys ? s.getAllKeys() : s.getAllKeys()
1101
+ );
1102
+ const out = [];
1103
+ for (const k of allKeys) {
1104
+ if (typeof k !== "string") continue;
1105
+ if (!isOurs(k)) continue;
1106
+ out.push(k.slice(prefix.length));
1107
+ }
1108
+ return out.sort();
1109
+ } catch (e) {
1110
+ const msg = String(e?.message ?? e);
1111
+ throw new PluginStorageError(`plugin.storage.keys failed: ${msg}`);
1112
+ }
1113
+ },
1114
+ async getJSON(key) {
1115
+ const raw = await this.getItem(key);
1116
+ if (raw == null) return null;
1117
+ try {
1118
+ return JSON.parse(raw);
1119
+ } catch {
1120
+ throw new PluginStorageError(
1121
+ `plugin.storage.getJSON: invalid JSON stored at key '${key}'`
1122
+ );
1123
+ }
1124
+ },
1125
+ async setJSON(key, value) {
1126
+ const raw = JSON.stringify(value);
1127
+ await this.setItem(key, raw);
1128
+ },
1129
+ async info() {
1130
+ return { namespace, backend };
1131
+ }
1132
+ };
1133
+ return api;
1134
+ }
1135
+
1136
+ // src/broadcast.ts
1137
+ import { useEffect, useRef } from "react";
1138
+ var _instanceId;
1139
+ var _coreOrigin;
1140
+ function initBroadcast(opts) {
1141
+ _instanceId = opts.instanceId;
1142
+ _coreOrigin = opts.coreOrigin || "*";
1143
+ }
1144
+ function broadcast(event, payload = {}) {
1145
+ if (typeof window === "undefined") return;
1146
+ if (window.parent === window) return;
1147
+ const message = {
1148
+ type: "ENSERA_SYNC_BROADCAST",
1149
+ event,
1150
+ payload,
1151
+ sourceInstanceId: _instanceId
1152
+ };
1153
+ try {
1154
+ window.parent.postMessage(message, _coreOrigin || "*");
1155
+ } catch (err) {
1156
+ console.debug("[plugin-frontend] broadcast postMessage failed:", err);
1157
+ }
1158
+ }
1159
+ function useBroadcastListener(event, callback) {
1160
+ const callbackRef = useRef(callback);
1161
+ callbackRef.current = callback;
1162
+ const events = Array.isArray(event) ? event : [event];
1163
+ useEffect(() => {
1164
+ function handleMessage(msg) {
1165
+ const data = msg.data;
1166
+ if (!data || data.type !== "ENSERA_SYNC_RELAY") return;
1167
+ if (data.sourceInstanceId && data.sourceInstanceId === _instanceId)
1168
+ return;
1169
+ if (!events.includes(data.event)) return;
1170
+ callbackRef.current(data.payload ?? {});
1171
+ }
1172
+ window.addEventListener("message", handleMessage);
1173
+ return () => window.removeEventListener("message", handleMessage);
1174
+ }, [events.join(",")]);
1175
+ }
1176
+ function onBroadcast(event, callback) {
1177
+ const events = Array.isArray(event) ? event : [event];
1178
+ function handleMessage(msg) {
1179
+ const data = msg.data;
1180
+ if (!data || data.type !== "ENSERA_SYNC_RELAY") return;
1181
+ if (data.sourceInstanceId && data.sourceInstanceId === _instanceId) return;
1182
+ if (!events.includes(data.event)) return;
1183
+ callback(data.payload ?? {});
1184
+ }
1185
+ window.addEventListener("message", handleMessage);
1186
+ return () => window.removeEventListener("message", handleMessage);
1187
+ }
1188
+
1189
+ // src/sync.ts
1190
+ function createSyncedState(args) {
1191
+ const { featureSlug, instanceId, initialState, onSync } = args;
1192
+ let currentState = initialState;
1193
+ const channelName = `ensera:sync:${featureSlug}`;
1194
+ let channel = null;
1195
+ const clientId = Math.random().toString(36).substring(2, 10);
1196
+ const handleMessage = (msg) => {
1197
+ if (msg.clientId === clientId) return;
1198
+ if (msg.type !== "STATE_SYNC") return;
1199
+ if (msg.featureSlug !== featureSlug) return;
1200
+ currentState = msg.state;
1201
+ if (onSync) {
1202
+ onSync(msg.state, msg.instanceId);
1203
+ }
1204
+ };
1205
+ if (typeof BroadcastChannel !== "undefined") {
1206
+ try {
1207
+ channel = new BroadcastChannel(channelName);
1208
+ channel.onmessage = (event) => {
1209
+ handleMessage(event.data);
1210
+ };
1211
+ } catch (e) {
1212
+ console.warn("BroadcastChannel not available:", e);
1213
+ }
1214
+ }
1215
+ function setState(newState) {
1216
+ const updatedState = typeof newState === "function" ? newState(currentState) : newState;
1217
+ currentState = updatedState;
1218
+ const msg = {
1219
+ type: "STATE_SYNC",
1220
+ featureSlug,
1221
+ instanceId,
1222
+ clientId,
1223
+ state: updatedState,
1224
+ timestamp: Date.now()
1225
+ };
1226
+ if (channel) {
1227
+ try {
1228
+ channel.postMessage(msg);
1229
+ } catch (e) {
1230
+ console.error("Failed to broadcast state:", e);
1231
+ }
1232
+ }
1233
+ return updatedState;
1234
+ }
1235
+ function getState() {
1236
+ return currentState;
1237
+ }
1238
+ function cleanup() {
1239
+ if (channel) {
1240
+ try {
1241
+ channel.close();
1242
+ } catch {
1243
+ }
1244
+ channel = null;
1245
+ }
1246
+ }
1247
+ return {
1248
+ setState,
1249
+ getState,
1250
+ cleanup
1251
+ };
1252
+ }
1253
+ function useSyncedState(args) {
1254
+ const { featureSlug, instanceId, initialState } = args;
1255
+ const sync = createSyncedState({
1256
+ featureSlug,
1257
+ instanceId,
1258
+ initialState
1259
+ });
1260
+ return [sync.getState(), sync.setState];
1261
+ }
1262
+
1263
+ // src/ui/tokens.ts
1264
+ var tokens = {
1265
+ // Spacing scale (in pixels)
1266
+ spacing: {
1267
+ xs: 4,
1268
+ sm: 8,
1269
+ md: 12,
1270
+ lg: 16,
1271
+ xl: 20,
1272
+ xxl: 24
1273
+ },
1274
+ // Row heights for context menu items
1275
+ rowHeight: {
1276
+ compact: 32,
1277
+ default: 44,
1278
+ comfortable: 56,
1279
+ large: 72
1280
+ },
1281
+ // Font sizes
1282
+ fontSize: {
1283
+ xs: 11,
1284
+ sm: 12,
1285
+ md: 14,
1286
+ lg: 16,
1287
+ xl: 18
1288
+ },
1289
+ // Font weights
1290
+ fontWeight: {
1291
+ normal: 400,
1292
+ medium: 500,
1293
+ semibold: 600,
1294
+ bold: 700
1295
+ },
1296
+ // Border radius
1297
+ radius: {
1298
+ sm: 4,
1299
+ md: 6,
1300
+ lg: 8,
1301
+ xl: 12,
1302
+ full: 9999
1303
+ },
1304
+ // Colors (Light mode - context menu specific)
1305
+ colors: {
1306
+ // Backgrounds
1307
+ bg: {
1308
+ primary: "#FFFFFF",
1309
+ secondary: "#F9FAFB",
1310
+ hover: "#F3F4F6",
1311
+ active: "#E5E7EB",
1312
+ disabled: "#F9FAFB"
1313
+ },
1314
+ // Borders
1315
+ border: {
1316
+ default: "#E5E7EB",
1317
+ hover: "#D1D5DB",
1318
+ focus: "#3B82F6"
1319
+ },
1320
+ // Text
1321
+ text: {
1322
+ primary: "#111827",
1323
+ secondary: "#6B7280",
1324
+ tertiary: "#9CA3AF",
1325
+ disabled: "#D1D5DB",
1326
+ inverse: "#FFFFFF"
1327
+ },
1328
+ // Status/Accent
1329
+ accent: {
1330
+ primary: "#3B82F6",
1331
+ primaryHover: "#2563EB",
1332
+ success: "#10B981",
1333
+ warning: "#F59E0B",
1334
+ danger: "#EF4444"
1335
+ }
1336
+ },
1337
+ // Shadows
1338
+ shadow: {
1339
+ sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
1340
+ md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
1341
+ lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
1342
+ xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)"
1343
+ },
1344
+ // Transitions
1345
+ transition: {
1346
+ fast: "150ms cubic-bezier(0.4, 0, 0.2, 1)",
1347
+ base: "200ms cubic-bezier(0.4, 0, 0.2, 1)",
1348
+ slow: "300ms cubic-bezier(0.4, 0, 0.2, 1)"
1349
+ }
1350
+ };
1351
+
1352
+ // src/ui/Button.tsx
1353
+ import React from "react";
1354
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1355
+ var getVariantStyles = (variant) => {
1356
+ const baseStyles = {
1357
+ border: "1px solid",
1358
+ cursor: "pointer",
1359
+ fontWeight: tokens.fontWeight.medium,
1360
+ transition: `all ${tokens.transition.fast}`,
1361
+ display: "inline-flex",
1362
+ alignItems: "center",
1363
+ justifyContent: "center",
1364
+ gap: tokens.spacing.sm,
1365
+ fontFamily: "ui-sans-serif, system-ui",
1366
+ outline: "none"
1367
+ };
1368
+ switch (variant) {
1369
+ case "primary":
1370
+ return {
1371
+ ...baseStyles,
1372
+ backgroundColor: tokens.colors.accent.primary,
1373
+ borderColor: tokens.colors.accent.primary,
1374
+ color: tokens.colors.text.inverse
1375
+ };
1376
+ case "secondary":
1377
+ return {
1378
+ ...baseStyles,
1379
+ backgroundColor: tokens.colors.bg.primary,
1380
+ borderColor: tokens.colors.border.default,
1381
+ color: tokens.colors.text.primary
1382
+ };
1383
+ case "ghost":
1384
+ return {
1385
+ ...baseStyles,
1386
+ backgroundColor: "transparent",
1387
+ borderColor: "transparent",
1388
+ color: tokens.colors.text.secondary
1389
+ };
1390
+ case "danger":
1391
+ return {
1392
+ ...baseStyles,
1393
+ backgroundColor: tokens.colors.accent.danger,
1394
+ borderColor: tokens.colors.accent.danger,
1395
+ color: tokens.colors.text.inverse
1396
+ };
1397
+ default:
1398
+ return baseStyles;
1399
+ }
1400
+ };
1401
+ var getHoverStyles = (variant) => {
1402
+ switch (variant) {
1403
+ case "primary":
1404
+ return {
1405
+ backgroundColor: tokens.colors.accent.primaryHover,
1406
+ borderColor: tokens.colors.accent.primaryHover
1407
+ };
1408
+ case "secondary":
1409
+ return {
1410
+ backgroundColor: tokens.colors.bg.hover,
1411
+ borderColor: tokens.colors.border.hover
1412
+ };
1413
+ case "ghost":
1414
+ return {
1415
+ backgroundColor: tokens.colors.bg.hover
1416
+ };
1417
+ case "danger":
1418
+ return {
1419
+ backgroundColor: "#DC2626"
1420
+ };
1421
+ default:
1422
+ return {};
1423
+ }
1424
+ };
1425
+ var getSizeStyles = (size) => {
1426
+ switch (size) {
1427
+ case "sm":
1428
+ return {
1429
+ height: 28,
1430
+ padding: `0 ${tokens.spacing.md}px`,
1431
+ fontSize: tokens.fontSize.sm,
1432
+ borderRadius: tokens.radius.md
1433
+ };
1434
+ case "md":
1435
+ return {
1436
+ height: 36,
1437
+ padding: `0 ${tokens.spacing.lg}px`,
1438
+ fontSize: tokens.fontSize.md,
1439
+ borderRadius: tokens.radius.lg
1440
+ };
1441
+ case "lg":
1442
+ return {
1443
+ height: 44,
1444
+ padding: `0 ${tokens.spacing.xl}px`,
1445
+ fontSize: tokens.fontSize.lg,
1446
+ borderRadius: tokens.radius.lg
1447
+ };
1448
+ default:
1449
+ return {};
1450
+ }
1451
+ };
1452
+ var Button = React.forwardRef(
1453
+ ({
1454
+ variant = "secondary",
1455
+ size = "md",
1456
+ fullWidth = false,
1457
+ icon,
1458
+ iconPosition = "left",
1459
+ disabled,
1460
+ children,
1461
+ style,
1462
+ onMouseEnter,
1463
+ onMouseLeave,
1464
+ ...props
1465
+ }, ref) => {
1466
+ const [isHovered, setIsHovered] = React.useState(false);
1467
+ const baseStyles = getVariantStyles(variant);
1468
+ const sizeStyles = getSizeStyles(size);
1469
+ const hoverStyles = isHovered ? getHoverStyles(variant) : {};
1470
+ const disabledStyles = disabled ? {
1471
+ opacity: 0.5,
1472
+ cursor: "not-allowed",
1473
+ pointerEvents: "none"
1474
+ } : {};
1475
+ const widthStyle = fullWidth ? { width: "100%" } : {};
1476
+ const combinedStyles = {
1477
+ ...baseStyles,
1478
+ ...sizeStyles,
1479
+ ...hoverStyles,
1480
+ ...disabledStyles,
1481
+ ...widthStyle,
1482
+ ...style
1483
+ };
1484
+ return /* @__PURE__ */ jsxs(
1485
+ "button",
1486
+ {
1487
+ ref,
1488
+ disabled,
1489
+ style: combinedStyles,
1490
+ onMouseEnter: (e) => {
1491
+ setIsHovered(true);
1492
+ onMouseEnter?.(e);
1493
+ },
1494
+ onMouseLeave: (e) => {
1495
+ setIsHovered(false);
1496
+ onMouseLeave?.(e);
1497
+ },
1498
+ ...props,
1499
+ children: [
1500
+ icon && iconPosition === "left" && /* @__PURE__ */ jsx2("span", { children: icon }),
1501
+ children,
1502
+ icon && iconPosition === "right" && /* @__PURE__ */ jsx2("span", { children: icon })
1503
+ ]
1504
+ }
1505
+ );
1506
+ }
1507
+ );
1508
+ Button.displayName = "Button";
1509
+
1510
+ // src/ui/Input.tsx
1511
+ import React2 from "react";
1512
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1513
+ var getSizeStyles2 = (size) => {
1514
+ switch (size) {
1515
+ case "sm":
1516
+ return {
1517
+ height: 28,
1518
+ padding: `0 ${tokens.spacing.sm}px`,
1519
+ fontSize: tokens.fontSize.sm
1520
+ };
1521
+ case "md":
1522
+ return {
1523
+ height: 36,
1524
+ padding: `0 ${tokens.spacing.md}px`,
1525
+ fontSize: tokens.fontSize.md
1526
+ };
1527
+ case "lg":
1528
+ return {
1529
+ height: 44,
1530
+ padding: `0 ${tokens.spacing.lg}px`,
1531
+ fontSize: tokens.fontSize.lg
1532
+ };
1533
+ default:
1534
+ return {};
1535
+ }
1536
+ };
1537
+ var Input = React2.forwardRef(
1538
+ ({
1539
+ size = "md",
1540
+ variant = "default",
1541
+ error = false,
1542
+ fullWidth = false,
1543
+ disabled = false,
1544
+ leftIcon,
1545
+ rightIcon,
1546
+ style,
1547
+ className,
1548
+ ...props
1549
+ }, ref) => {
1550
+ const [isFocused, setIsFocused] = React2.useState(false);
1551
+ const sizeStyles = getSizeStyles2(size);
1552
+ const baseStyles = {
1553
+ border: "1px solid",
1554
+ borderRadius: tokens.radius.lg,
1555
+ fontFamily: "ui-sans-serif, system-ui",
1556
+ transition: `all ${tokens.transition.fast}`,
1557
+ outline: "none",
1558
+ ...sizeStyles
1559
+ };
1560
+ const variantStyles = variant === "filled" ? {
1561
+ backgroundColor: tokens.colors.bg.secondary,
1562
+ borderColor: "transparent"
1563
+ } : {
1564
+ backgroundColor: tokens.colors.bg.primary,
1565
+ borderColor: tokens.colors.border.default
1566
+ };
1567
+ const stateStyles = error ? {
1568
+ borderColor: tokens.colors.accent.danger
1569
+ } : isFocused ? {
1570
+ borderColor: tokens.colors.border.focus,
1571
+ boxShadow: `0 0 0 3px rgba(59, 130, 246, 0.1)`
1572
+ } : {};
1573
+ const disabledStyles = disabled ? {
1574
+ backgroundColor: tokens.colors.bg.disabled,
1575
+ color: tokens.colors.text.disabled,
1576
+ cursor: "not-allowed"
1577
+ } : {};
1578
+ const widthStyle = fullWidth ? { width: "100%" } : {};
1579
+ const iconPaddingStyles = {};
1580
+ if (leftIcon) {
1581
+ iconPaddingStyles.paddingLeft = `${tokens.spacing.xl + 4}px`;
1582
+ }
1583
+ if (rightIcon) {
1584
+ iconPaddingStyles.paddingRight = `${tokens.spacing.xl + 4}px`;
1585
+ }
1586
+ const combinedStyles = {
1587
+ ...baseStyles,
1588
+ ...variantStyles,
1589
+ ...stateStyles,
1590
+ ...disabledStyles,
1591
+ ...widthStyle,
1592
+ ...iconPaddingStyles,
1593
+ ...style
1594
+ };
1595
+ if (leftIcon || rightIcon) {
1596
+ return /* @__PURE__ */ jsxs2(
1597
+ "div",
1598
+ {
1599
+ style: {
1600
+ position: "relative",
1601
+ display: "inline-flex",
1602
+ alignItems: "center",
1603
+ width: fullWidth ? "100%" : "auto"
1604
+ },
1605
+ children: [
1606
+ leftIcon && /* @__PURE__ */ jsx3(
1607
+ "div",
1608
+ {
1609
+ style: {
1610
+ position: "absolute",
1611
+ left: tokens.spacing.md,
1612
+ display: "flex",
1613
+ alignItems: "center",
1614
+ color: tokens.colors.text.tertiary,
1615
+ pointerEvents: "none"
1616
+ },
1617
+ children: leftIcon
1618
+ }
1619
+ ),
1620
+ /* @__PURE__ */ jsx3(
1621
+ "input",
1622
+ {
1623
+ ref,
1624
+ disabled,
1625
+ style: combinedStyles,
1626
+ className,
1627
+ onFocus: (e) => {
1628
+ setIsFocused(true);
1629
+ props.onFocus?.(e);
1630
+ },
1631
+ onBlur: (e) => {
1632
+ setIsFocused(false);
1633
+ props.onBlur?.(e);
1634
+ },
1635
+ ...props
1636
+ }
1637
+ ),
1638
+ rightIcon && /* @__PURE__ */ jsx3(
1639
+ "div",
1640
+ {
1641
+ style: {
1642
+ position: "absolute",
1643
+ right: tokens.spacing.md,
1644
+ display: "flex",
1645
+ alignItems: "center",
1646
+ color: tokens.colors.text.tertiary,
1647
+ pointerEvents: "none"
1648
+ },
1649
+ children: rightIcon
1650
+ }
1651
+ )
1652
+ ]
1653
+ }
1654
+ );
1655
+ }
1656
+ return /* @__PURE__ */ jsx3(
1657
+ "input",
1658
+ {
1659
+ ref,
1660
+ disabled,
1661
+ style: combinedStyles,
1662
+ className,
1663
+ onFocus: (e) => {
1664
+ setIsFocused(true);
1665
+ props.onFocus?.(e);
1666
+ },
1667
+ onBlur: (e) => {
1668
+ setIsFocused(false);
1669
+ props.onBlur?.(e);
1670
+ },
1671
+ ...props
1672
+ }
1673
+ );
1674
+ }
1675
+ );
1676
+ Input.displayName = "Input";
1677
+
1678
+ // src/ui/IconButton.tsx
1679
+ import React3 from "react";
1680
+ import { jsx as jsx4 } from "react/jsx-runtime";
1681
+ var getSizeStyles3 = (size) => {
1682
+ switch (size) {
1683
+ case "sm":
1684
+ return {
1685
+ width: 28,
1686
+ height: 28,
1687
+ fontSize: 14
1688
+ };
1689
+ case "md":
1690
+ return {
1691
+ width: 36,
1692
+ height: 36,
1693
+ fontSize: 18
1694
+ };
1695
+ case "lg":
1696
+ return {
1697
+ width: 44,
1698
+ height: 44,
1699
+ fontSize: 20
1700
+ };
1701
+ default:
1702
+ return {};
1703
+ }
1704
+ };
1705
+ var getVariantStyles2 = (variant) => {
1706
+ const baseStyles = {
1707
+ border: "1px solid",
1708
+ cursor: "pointer",
1709
+ transition: `all ${tokens.transition.fast}`,
1710
+ display: "inline-flex",
1711
+ alignItems: "center",
1712
+ justifyContent: "center",
1713
+ outline: "none"
1714
+ };
1715
+ switch (variant) {
1716
+ case "default":
1717
+ return {
1718
+ ...baseStyles,
1719
+ backgroundColor: tokens.colors.bg.primary,
1720
+ borderColor: tokens.colors.border.default,
1721
+ color: tokens.colors.text.primary
1722
+ };
1723
+ case "ghost":
1724
+ return {
1725
+ ...baseStyles,
1726
+ backgroundColor: "transparent",
1727
+ borderColor: "transparent",
1728
+ color: tokens.colors.text.secondary
1729
+ };
1730
+ case "primary":
1731
+ return {
1732
+ ...baseStyles,
1733
+ backgroundColor: tokens.colors.accent.primary,
1734
+ borderColor: tokens.colors.accent.primary,
1735
+ color: tokens.colors.text.inverse
1736
+ };
1737
+ case "danger":
1738
+ return {
1739
+ ...baseStyles,
1740
+ backgroundColor: "transparent",
1741
+ borderColor: "transparent",
1742
+ color: tokens.colors.accent.danger
1743
+ };
1744
+ default:
1745
+ return baseStyles;
1746
+ }
1747
+ };
1748
+ var getHoverStyles2 = (variant) => {
1749
+ switch (variant) {
1750
+ case "default":
1751
+ return {
1752
+ backgroundColor: tokens.colors.bg.hover,
1753
+ borderColor: tokens.colors.border.hover
1754
+ };
1755
+ case "ghost":
1756
+ return {
1757
+ backgroundColor: tokens.colors.bg.hover
1758
+ };
1759
+ case "primary":
1760
+ return {
1761
+ backgroundColor: tokens.colors.accent.primaryHover,
1762
+ borderColor: tokens.colors.accent.primaryHover
1763
+ };
1764
+ case "danger":
1765
+ return {
1766
+ backgroundColor: "rgba(239, 68, 68, 0.1)"
1767
+ };
1768
+ default:
1769
+ return {};
1770
+ }
1771
+ };
1772
+ var IconButton = React3.forwardRef(
1773
+ ({
1774
+ size = "md",
1775
+ variant = "ghost",
1776
+ icon,
1777
+ disabled,
1778
+ style,
1779
+ onMouseEnter,
1780
+ onMouseLeave,
1781
+ ...props
1782
+ }, ref) => {
1783
+ const [isHovered, setIsHovered] = React3.useState(false);
1784
+ const baseStyles = getVariantStyles2(variant);
1785
+ const sizeStyles = getSizeStyles3(size);
1786
+ const hoverStyles = isHovered ? getHoverStyles2(variant) : {};
1787
+ const disabledStyles = disabled ? {
1788
+ opacity: 0.5,
1789
+ cursor: "not-allowed",
1790
+ pointerEvents: "none"
1791
+ } : {};
1792
+ const combinedStyles = {
1793
+ ...baseStyles,
1794
+ ...sizeStyles,
1795
+ ...hoverStyles,
1796
+ ...disabledStyles,
1797
+ borderRadius: tokens.radius.lg,
1798
+ ...style
1799
+ };
1800
+ return /* @__PURE__ */ jsx4(
1801
+ "button",
1802
+ {
1803
+ ref,
1804
+ disabled,
1805
+ style: combinedStyles,
1806
+ onMouseEnter: (e) => {
1807
+ setIsHovered(true);
1808
+ onMouseEnter?.(e);
1809
+ },
1810
+ onMouseLeave: (e) => {
1811
+ setIsHovered(false);
1812
+ onMouseLeave?.(e);
1813
+ },
1814
+ ...props,
1815
+ children: icon
1816
+ }
1817
+ );
1818
+ }
1819
+ );
1820
+ IconButton.displayName = "IconButton";
1821
+
1822
+ // src/ui/Row.tsx
1823
+ import React4 from "react";
1824
+ import { jsx as jsx5 } from "react/jsx-runtime";
1825
+ var getAlignItems = (align) => {
1826
+ switch (align) {
1827
+ case "start":
1828
+ return "flex-start";
1829
+ case "center":
1830
+ return "center";
1831
+ case "end":
1832
+ return "flex-end";
1833
+ case "stretch":
1834
+ return "stretch";
1835
+ default:
1836
+ return "center";
1837
+ }
1838
+ };
1839
+ var getJustifyContent = (justify) => {
1840
+ switch (justify) {
1841
+ case "start":
1842
+ return "flex-start";
1843
+ case "center":
1844
+ return "center";
1845
+ case "end":
1846
+ return "flex-end";
1847
+ case "between":
1848
+ return "space-between";
1849
+ case "around":
1850
+ return "space-around";
1851
+ case "evenly":
1852
+ return "space-evenly";
1853
+ default:
1854
+ return "flex-start";
1855
+ }
1856
+ };
1857
+ var Row = React4.forwardRef(
1858
+ ({
1859
+ height = "default",
1860
+ customHeight,
1861
+ align = "center",
1862
+ justify = "start",
1863
+ gap = tokens.spacing.md,
1864
+ padding = tokens.spacing.md,
1865
+ fullWidth = true,
1866
+ divider = false,
1867
+ children,
1868
+ style,
1869
+ ...props
1870
+ }, ref) => {
1871
+ const rowHeight = customHeight ?? tokens.rowHeight[height];
1872
+ const containerStyles = {
1873
+ display: "flex",
1874
+ alignItems: getAlignItems(align),
1875
+ justifyContent: getJustifyContent(justify),
1876
+ gap,
1877
+ padding: `0 ${padding}px`,
1878
+ height: typeof rowHeight === "number" ? `${rowHeight}px` : rowHeight,
1879
+ width: fullWidth ? "100%" : "auto",
1880
+ boxSizing: "border-box",
1881
+ fontFamily: "ui-sans-serif, system-ui",
1882
+ overflow: "hidden",
1883
+ ...style
1884
+ };
1885
+ if (divider && React4.Children.count(children) > 1) {
1886
+ const childrenArray = React4.Children.toArray(children);
1887
+ const childrenWithDividers = [];
1888
+ childrenArray.forEach((child, index) => {
1889
+ childrenWithDividers.push(
1890
+ /* @__PURE__ */ jsx5(React4.Fragment, { children: child }, `child-${index}`)
1891
+ );
1892
+ if (index < childrenArray.length - 1) {
1893
+ childrenWithDividers.push(
1894
+ /* @__PURE__ */ jsx5(
1895
+ "div",
1896
+ {
1897
+ style: {
1898
+ width: 1,
1899
+ height: "60%",
1900
+ backgroundColor: tokens.colors.border.default,
1901
+ flexShrink: 0
1902
+ }
1903
+ },
1904
+ `divider-${index}`
1905
+ )
1906
+ );
1907
+ }
1908
+ });
1909
+ return /* @__PURE__ */ jsx5("div", { ref, style: containerStyles, ...props, children: childrenWithDividers });
1910
+ }
1911
+ return /* @__PURE__ */ jsx5("div", { ref, style: containerStyles, ...props, children });
1912
+ }
1913
+ );
1914
+ Row.displayName = "Row";
1915
+ var ContextRow = React4.forwardRef(
1916
+ ({
1917
+ expandable = false,
1918
+ minHeight = tokens.rowHeight.default,
1919
+ maxHeight = 120,
1920
+ children,
1921
+ style,
1922
+ ...props
1923
+ }, ref) => {
1924
+ const heightStyles = expandable ? {
1925
+ minHeight: `${minHeight}px`,
1926
+ maxHeight: `${maxHeight}px`,
1927
+ height: "auto"
1928
+ } : {
1929
+ height: `${minHeight}px`
1930
+ };
1931
+ return /* @__PURE__ */ jsx5(
1932
+ Row,
1933
+ {
1934
+ ref,
1935
+ customHeight: "auto",
1936
+ style: {
1937
+ ...heightStyles,
1938
+ ...style
1939
+ },
1940
+ ...props,
1941
+ children
1942
+ }
1943
+ );
1944
+ }
1945
+ );
1946
+ ContextRow.displayName = "ContextRow";
1947
+
1948
+ // src/overlay.ts
1949
+ function openOverlay(featureSlug) {
1950
+ const msg = {
1951
+ type: "ENSERA_OPEN_OVERLAY",
1952
+ featureSlug
1953
+ };
1954
+ window.parent.postMessage(msg, "*");
1955
+ }
1956
+
1957
+ // src/context.ts
1958
+ function setupContextMenuRelay(ctx) {
1959
+ if (typeof window === "undefined") return () => {
1960
+ };
1961
+ if (window === window.parent) return () => {
1962
+ };
1963
+ const onContextMenu = (e) => {
1964
+ if (e.defaultPrevented) return;
1965
+ const target = e.target;
1966
+ if (target?.closest("[data-no-context]")) return;
1967
+ e.preventDefault();
1968
+ window.parent.postMessage(
1969
+ {
1970
+ type: "OPEN_CONTEXT_MENU",
1971
+ x: e.clientX,
1972
+ y: e.clientY,
1973
+ fromIframe: true,
1974
+ instanceId: ctx.instanceId
1975
+ },
1976
+ "*"
1977
+ );
1978
+ };
1979
+ document.addEventListener("contextmenu", onContextMenu);
1980
+ return () => document.removeEventListener("contextmenu", onContextMenu);
1981
+ }
1982
+ export {
1983
+ Button,
1984
+ ContextMenuShell,
1985
+ ContextRow,
1986
+ IconButton,
1987
+ Input,
1988
+ PluginAuthError,
1989
+ PluginFetchError,
1990
+ PluginFetchInputError,
1991
+ PluginForbiddenError,
1992
+ PluginNetworkError,
1993
+ PluginNotFoundError,
1994
+ PluginRateLimitError,
1995
+ PluginResponseError,
1996
+ PluginServerError,
1997
+ PluginStorageError,
1998
+ PluginStorageQuotaError,
1999
+ PluginUnknownActionError,
2000
+ PluginValidationError,
2001
+ Row,
2002
+ attachActionDispatcher,
2003
+ broadcast,
2004
+ createPluginFetch,
2005
+ createPluginLogger,
2006
+ createPluginNotify,
2007
+ createPluginRuntime,
2008
+ createPluginStorage,
2009
+ createPluginStorageIndexedDB,
2010
+ createSyncedState,
2011
+ defineActions,
2012
+ initBroadcast,
2013
+ makeStorageNamespace,
2014
+ onBroadcast,
2015
+ openOverlay,
2016
+ runActionSafe,
2017
+ setupContextMenuRelay,
2018
+ tokens,
2019
+ useBroadcastListener,
2020
+ useSyncedState
2021
+ };