@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/README.md +15 -0
- package/dist/index.d.ts +752 -0
- package/dist/index.js +2021 -0
- package/package.json +50 -0
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
|
+
};
|