@ensera/plugin-frontend 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -15
- package/dist/index.d.ts +40 -1
- package/dist/index.js +333 -0
- package/package.json +50 -50
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# @ensera/plugin-frontend
|
|
2
|
-
|
|
3
|
-
Frontend runtime SDK for Ensera plugins.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @ensera/plugin-frontend
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Purpose
|
|
12
|
-
|
|
13
|
-
- mounts frontend features inside Ensera Core
|
|
14
|
-
- provides the runtime APIs used by the published templates
|
|
15
|
-
- stays separate from the scaffold template packages
|
|
1
|
+
# @ensera/plugin-frontend
|
|
2
|
+
|
|
3
|
+
Frontend runtime SDK for Ensera plugins.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ensera/plugin-frontend
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
- mounts frontend features inside Ensera Core
|
|
14
|
+
- provides the runtime APIs used by the published templates
|
|
15
|
+
- stays separate from the scaffold template packages
|
package/dist/index.d.ts
CHANGED
|
@@ -71,6 +71,23 @@ type PluginCtx = {
|
|
|
71
71
|
* Backward compatible: older Core can omit.
|
|
72
72
|
*/
|
|
73
73
|
launch?: PluginLaunch;
|
|
74
|
+
/**
|
|
75
|
+
* Optional: current Core theme snapshot.
|
|
76
|
+
* Included on INIT so the feature can render with the correct theme on first paint.
|
|
77
|
+
*/
|
|
78
|
+
theme?: PluginThemeSnapshot;
|
|
79
|
+
};
|
|
80
|
+
type PluginThemeMode = "light" | "dark";
|
|
81
|
+
type PluginThemeSnapshot = {
|
|
82
|
+
mode: PluginThemeMode;
|
|
83
|
+
};
|
|
84
|
+
type PluginThemeColorRole = "solid" | "surface" | "border" | "text";
|
|
85
|
+
type PluginThemeMessage = {
|
|
86
|
+
type: "ENSERA_THEME";
|
|
87
|
+
instanceId?: string;
|
|
88
|
+
payload: {
|
|
89
|
+
theme: PluginThemeSnapshot;
|
|
90
|
+
};
|
|
74
91
|
};
|
|
75
92
|
/**
|
|
76
93
|
* Launch modes for plugin instances
|
|
@@ -548,6 +565,28 @@ declare function useSyncedState<T extends JsonValue>(args: {
|
|
|
548
565
|
initialState: T;
|
|
549
566
|
}): [T, (state: T | ((prev: T) => T)) => void];
|
|
550
567
|
|
|
568
|
+
declare function normalizePluginThemeMode(value: unknown): PluginThemeMode;
|
|
569
|
+
declare function normalizePluginThemeSnapshot(value?: Partial<PluginThemeSnapshot> | PluginThemeMessage | null): PluginThemeSnapshot;
|
|
570
|
+
declare function applyPluginTheme(value?: Partial<PluginThemeSnapshot> | PluginThemeMessage | null, root?: HTMLElement): PluginThemeSnapshot;
|
|
571
|
+
declare function dispatchPluginThemeChange(value?: Partial<PluginThemeSnapshot> | PluginThemeMessage | null): PluginThemeSnapshot;
|
|
572
|
+
declare function initializePluginTheme(value?: Partial<PluginThemeSnapshot> | PluginThemeMessage | null): PluginThemeSnapshot;
|
|
573
|
+
declare function getPluginThemeSnapshot(): PluginThemeSnapshot;
|
|
574
|
+
declare function subscribePluginTheme(listener: () => void): () => boolean;
|
|
575
|
+
declare function usePluginTheme(): PluginThemeSnapshot;
|
|
576
|
+
declare function adaptFeatureColor(hex: string, args: {
|
|
577
|
+
mode: PluginThemeMode;
|
|
578
|
+
role?: PluginThemeColorRole;
|
|
579
|
+
}): string;
|
|
580
|
+
declare function getReadableFeatureTextColor(backgroundHex: string): "#FFFFFF" | "#111111";
|
|
581
|
+
declare function getFeatureColorTokens(hex: string, mode: PluginThemeMode): {
|
|
582
|
+
solid: string;
|
|
583
|
+
surface: string;
|
|
584
|
+
border: string;
|
|
585
|
+
text: string;
|
|
586
|
+
contrastText: string;
|
|
587
|
+
};
|
|
588
|
+
declare function syncAdaptiveFeatureColors(root: ParentNode | null | undefined, mode: PluginThemeMode): void;
|
|
589
|
+
|
|
551
590
|
declare const tokens: {
|
|
552
591
|
readonly spacing: {
|
|
553
592
|
readonly xs: 4;
|
|
@@ -749,4 +788,4 @@ declare function setupContextMenuRelay(ctx: {
|
|
|
749
788
|
instanceId: string;
|
|
750
789
|
}): () => void;
|
|
751
790
|
|
|
752
|
-
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant, ContextMenuShell, ContextRow, type ContextRowProps, IconButton, type IconButtonProps, type IconButtonSize, type IconButtonVariant, Input, type InputProps, type InputSize, type InputVariant, type JsonObject, type JsonValue, type NotificationAction, type NotificationCapabilities, type NotificationTypeConfig, type PluginActionHandler, type PluginActionHandlerArgs, type PluginActionId, type PluginActionsMap, PluginAuthError, type PluginCtx, type PluginDispatchFn, type PluginDispatchRequest, type PluginDispatchResult, type PluginErrorPayload, PluginFetchError, type PluginFetchExpect, PluginFetchInputError, type PluginFetchOptions, type PluginFetchResponse, type PluginFetchRetry, PluginForbiddenError, type PluginLaunch, type PluginLogLevel, type PluginLogger, PluginNetworkError, PluginNotFoundError, type PluginNotification, type PluginNotificationBulkResponse, type PluginNotificationOptions, type PluginNotificationRequest, type PluginNotificationResponse, type PluginNotificationResultMessage, type PluginNotify, type PluginOpenOverlayMessage, PluginRateLimitError, PluginResponseError, type PluginRuntime, PluginServerError, type PluginStorage, type PluginStorageBackend, PluginStorageError, type PluginStorageIndexedDB, PluginStorageQuotaError, PluginUnknownActionError, PluginValidationError, Row, type RowAlign, type RowHeight, type RowJustify, type RowProps, type SyncCallback, type SyncEventType, type SyncMessage, type SyncPayload, type SyncedState, type Tokens, attachActionDispatcher, broadcast, createPluginFetch, createPluginLogger, createPluginNotify, createPluginRuntime, createPluginStorage, createPluginStorageIndexedDB, createSyncedState, defineActions, initBroadcast, makeStorageNamespace, onBroadcast, openOverlay, runActionSafe, setupContextMenuRelay, tokens, useBroadcastListener, useSyncedState };
|
|
791
|
+
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant, ContextMenuShell, ContextRow, type ContextRowProps, IconButton, type IconButtonProps, type IconButtonSize, type IconButtonVariant, Input, type InputProps, type InputSize, type InputVariant, type JsonObject, type JsonValue, type NotificationAction, type NotificationCapabilities, type NotificationTypeConfig, type PluginActionHandler, type PluginActionHandlerArgs, type PluginActionId, type PluginActionsMap, PluginAuthError, type PluginCtx, type PluginDispatchFn, type PluginDispatchRequest, type PluginDispatchResult, type PluginErrorPayload, PluginFetchError, type PluginFetchExpect, PluginFetchInputError, type PluginFetchOptions, type PluginFetchResponse, type PluginFetchRetry, PluginForbiddenError, type PluginLaunch, type PluginLogLevel, type PluginLogger, PluginNetworkError, PluginNotFoundError, type PluginNotification, type PluginNotificationBulkResponse, type PluginNotificationOptions, type PluginNotificationRequest, type PluginNotificationResponse, type PluginNotificationResultMessage, type PluginNotify, type PluginOpenOverlayMessage, PluginRateLimitError, PluginResponseError, type PluginRuntime, PluginServerError, type PluginStorage, type PluginStorageBackend, PluginStorageError, type PluginStorageIndexedDB, PluginStorageQuotaError, type PluginThemeColorRole, type PluginThemeMessage, type PluginThemeMode, type PluginThemeSnapshot, PluginUnknownActionError, PluginValidationError, Row, type RowAlign, type RowHeight, type RowJustify, type RowProps, type SyncCallback, type SyncEventType, type SyncMessage, type SyncPayload, type SyncedState, type Tokens, adaptFeatureColor, applyPluginTheme, attachActionDispatcher, broadcast, createPluginFetch, createPluginLogger, createPluginNotify, createPluginRuntime, createPluginStorage, createPluginStorageIndexedDB, createSyncedState, defineActions, dispatchPluginThemeChange, getFeatureColorTokens, getPluginThemeSnapshot, getReadableFeatureTextColor, initBroadcast, initializePluginTheme, makeStorageNamespace, normalizePluginThemeMode, normalizePluginThemeSnapshot, onBroadcast, openOverlay, runActionSafe, setupContextMenuRelay, subscribePluginTheme, syncAdaptiveFeatureColors, tokens, useBroadcastListener, usePluginTheme, useSyncedState };
|
package/dist/index.js
CHANGED
|
@@ -1260,6 +1260,327 @@ function useSyncedState(args) {
|
|
|
1260
1260
|
return [sync.getState(), sync.setState];
|
|
1261
1261
|
}
|
|
1262
1262
|
|
|
1263
|
+
// src/theme.ts
|
|
1264
|
+
import { useSyncExternalStore } from "react";
|
|
1265
|
+
var THEME_EVENT = "ENSERA_THEME_CHANGE";
|
|
1266
|
+
var THEME_ATTR = "data-ensera-theme";
|
|
1267
|
+
var DARK_ROOT_CLASS = "dark";
|
|
1268
|
+
var LIGHT_SURFACE = "#FFFFFF";
|
|
1269
|
+
var DARK_SURFACE = "#111827";
|
|
1270
|
+
var currentTheme = readThemeFromDom();
|
|
1271
|
+
var boundWindowListeners = false;
|
|
1272
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
1273
|
+
function clamp(value, min, max) {
|
|
1274
|
+
return Math.min(max, Math.max(min, value));
|
|
1275
|
+
}
|
|
1276
|
+
function normalizeChannel(value) {
|
|
1277
|
+
return parseInt(value, 16);
|
|
1278
|
+
}
|
|
1279
|
+
function normalizePluginThemeMode(value) {
|
|
1280
|
+
return value === "dark" ? "dark" : "light";
|
|
1281
|
+
}
|
|
1282
|
+
function normalizePluginThemeSnapshot(value) {
|
|
1283
|
+
const maybeTheme = value && "payload" in value ? value.payload?.theme : value ?? void 0;
|
|
1284
|
+
return {
|
|
1285
|
+
mode: normalizePluginThemeMode(maybeTheme?.mode)
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function readThemeFromDom() {
|
|
1289
|
+
if (typeof document === "undefined") {
|
|
1290
|
+
return { mode: "light" };
|
|
1291
|
+
}
|
|
1292
|
+
const mode = document.documentElement.getAttribute(THEME_ATTR);
|
|
1293
|
+
return { mode: normalizePluginThemeMode(mode) };
|
|
1294
|
+
}
|
|
1295
|
+
function notifyThemeListeners() {
|
|
1296
|
+
for (const listener of listeners) {
|
|
1297
|
+
listener();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function applyThemeToRoot(root, theme) {
|
|
1301
|
+
root.setAttribute(THEME_ATTR, theme.mode);
|
|
1302
|
+
root.classList.toggle(DARK_ROOT_CLASS, theme.mode === "dark");
|
|
1303
|
+
root.style.colorScheme = theme.mode;
|
|
1304
|
+
}
|
|
1305
|
+
function bindWindowThemeListeners() {
|
|
1306
|
+
if (boundWindowListeners || typeof window === "undefined") {
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
window.addEventListener(THEME_EVENT, (event) => {
|
|
1310
|
+
const detail = event.detail;
|
|
1311
|
+
currentTheme = normalizePluginThemeSnapshot(detail);
|
|
1312
|
+
notifyThemeListeners();
|
|
1313
|
+
});
|
|
1314
|
+
boundWindowListeners = true;
|
|
1315
|
+
}
|
|
1316
|
+
function applyPluginTheme(value, root) {
|
|
1317
|
+
const theme = normalizePluginThemeSnapshot(value);
|
|
1318
|
+
currentTheme = theme;
|
|
1319
|
+
if (typeof document !== "undefined") {
|
|
1320
|
+
applyThemeToRoot(root ?? document.documentElement, theme);
|
|
1321
|
+
}
|
|
1322
|
+
notifyThemeListeners();
|
|
1323
|
+
return theme;
|
|
1324
|
+
}
|
|
1325
|
+
function dispatchPluginThemeChange(value) {
|
|
1326
|
+
const theme = normalizePluginThemeSnapshot(value);
|
|
1327
|
+
currentTheme = theme;
|
|
1328
|
+
if (typeof window !== "undefined") {
|
|
1329
|
+
window.dispatchEvent(
|
|
1330
|
+
new CustomEvent(THEME_EVENT, {
|
|
1331
|
+
detail: theme
|
|
1332
|
+
})
|
|
1333
|
+
);
|
|
1334
|
+
} else {
|
|
1335
|
+
notifyThemeListeners();
|
|
1336
|
+
}
|
|
1337
|
+
return theme;
|
|
1338
|
+
}
|
|
1339
|
+
function initializePluginTheme(value) {
|
|
1340
|
+
bindWindowThemeListeners();
|
|
1341
|
+
if (value) {
|
|
1342
|
+
return applyPluginTheme(value);
|
|
1343
|
+
}
|
|
1344
|
+
currentTheme = readThemeFromDom();
|
|
1345
|
+
notifyThemeListeners();
|
|
1346
|
+
return currentTheme;
|
|
1347
|
+
}
|
|
1348
|
+
function getPluginThemeSnapshot() {
|
|
1349
|
+
bindWindowThemeListeners();
|
|
1350
|
+
return currentTheme;
|
|
1351
|
+
}
|
|
1352
|
+
function subscribePluginTheme(listener) {
|
|
1353
|
+
bindWindowThemeListeners();
|
|
1354
|
+
listeners.add(listener);
|
|
1355
|
+
return () => listeners.delete(listener);
|
|
1356
|
+
}
|
|
1357
|
+
function usePluginTheme() {
|
|
1358
|
+
bindWindowThemeListeners();
|
|
1359
|
+
return useSyncExternalStore(
|
|
1360
|
+
subscribePluginTheme,
|
|
1361
|
+
getPluginThemeSnapshot,
|
|
1362
|
+
getPluginThemeSnapshot
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
function normalizeHexColor(value) {
|
|
1366
|
+
const raw = String(value ?? "").trim().replace(/^#/, "");
|
|
1367
|
+
if (raw.length === 3) {
|
|
1368
|
+
return `#${raw.split("").map((part) => `${part}${part}`).join("").toUpperCase()}`;
|
|
1369
|
+
}
|
|
1370
|
+
if (raw.length !== 6 || /[^0-9A-Fa-f]/.test(raw)) {
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
return `#${raw.toUpperCase()}`;
|
|
1374
|
+
}
|
|
1375
|
+
function parseCssColorToHex(value) {
|
|
1376
|
+
const normalizedHex = normalizeHexColor(value);
|
|
1377
|
+
if (normalizedHex) return normalizedHex;
|
|
1378
|
+
const match = String(value ?? "").trim().match(/^rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
|
|
1379
|
+
if (!match) return null;
|
|
1380
|
+
return rgbToHex(Number(match[1]), Number(match[2]), Number(match[3]));
|
|
1381
|
+
}
|
|
1382
|
+
function hexToRgb(hex) {
|
|
1383
|
+
const normalized = normalizeHexColor(hex);
|
|
1384
|
+
if (!normalized) return null;
|
|
1385
|
+
return {
|
|
1386
|
+
r: normalizeChannel(normalized.slice(1, 3)),
|
|
1387
|
+
g: normalizeChannel(normalized.slice(3, 5)),
|
|
1388
|
+
b: normalizeChannel(normalized.slice(5, 7))
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function rgbToHex(r, g, b) {
|
|
1392
|
+
const toHex = (value) => clamp(Math.round(value), 0, 255).toString(16).padStart(2, "0");
|
|
1393
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
1394
|
+
}
|
|
1395
|
+
function rgbToHsl(r, g, b) {
|
|
1396
|
+
const rn = r / 255;
|
|
1397
|
+
const gn = g / 255;
|
|
1398
|
+
const bn = b / 255;
|
|
1399
|
+
const max = Math.max(rn, gn, bn);
|
|
1400
|
+
const min = Math.min(rn, gn, bn);
|
|
1401
|
+
const delta = max - min;
|
|
1402
|
+
const lightness = (max + min) / 2;
|
|
1403
|
+
if (delta === 0) {
|
|
1404
|
+
return { h: 0, s: 0, l: lightness };
|
|
1405
|
+
}
|
|
1406
|
+
const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
|
1407
|
+
let hue = 0;
|
|
1408
|
+
switch (max) {
|
|
1409
|
+
case rn:
|
|
1410
|
+
hue = (gn - bn) / delta + (gn < bn ? 6 : 0);
|
|
1411
|
+
break;
|
|
1412
|
+
case gn:
|
|
1413
|
+
hue = (bn - rn) / delta + 2;
|
|
1414
|
+
break;
|
|
1415
|
+
default:
|
|
1416
|
+
hue = (rn - gn) / delta + 4;
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
hue /= 6;
|
|
1420
|
+
return { h: hue, s: saturation, l: lightness };
|
|
1421
|
+
}
|
|
1422
|
+
function hslToRgb(h, s, l) {
|
|
1423
|
+
if (s === 0) {
|
|
1424
|
+
const value = l * 255;
|
|
1425
|
+
return { r: value, g: value, b: value };
|
|
1426
|
+
}
|
|
1427
|
+
const hueToRgb = (p2, q2, t) => {
|
|
1428
|
+
let next = t;
|
|
1429
|
+
if (next < 0) next += 1;
|
|
1430
|
+
if (next > 1) next -= 1;
|
|
1431
|
+
if (next < 1 / 6) return p2 + (q2 - p2) * 6 * next;
|
|
1432
|
+
if (next < 1 / 2) return q2;
|
|
1433
|
+
if (next < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - next) * 6;
|
|
1434
|
+
return p2;
|
|
1435
|
+
};
|
|
1436
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
1437
|
+
const p = 2 * l - q;
|
|
1438
|
+
return {
|
|
1439
|
+
r: hueToRgb(p, q, h + 1 / 3) * 255,
|
|
1440
|
+
g: hueToRgb(p, q, h) * 255,
|
|
1441
|
+
b: hueToRgb(p, q, h - 1 / 3) * 255
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
function mixColors(baseHex, mixHex, ratio) {
|
|
1445
|
+
const base = hexToRgb(baseHex);
|
|
1446
|
+
const mix = hexToRgb(mixHex);
|
|
1447
|
+
if (!base || !mix) return normalizeHexColor(baseHex) ?? baseHex;
|
|
1448
|
+
const clampedRatio = clamp(ratio, 0, 1);
|
|
1449
|
+
const inverse = 1 - clampedRatio;
|
|
1450
|
+
return rgbToHex(
|
|
1451
|
+
base.r * inverse + mix.r * clampedRatio,
|
|
1452
|
+
base.g * inverse + mix.g * clampedRatio,
|
|
1453
|
+
base.b * inverse + mix.b * clampedRatio
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
function toLinear(channel) {
|
|
1457
|
+
const normalized = channel / 255;
|
|
1458
|
+
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
|
|
1459
|
+
}
|
|
1460
|
+
function relativeLuminance(hex) {
|
|
1461
|
+
const rgb = hexToRgb(hex);
|
|
1462
|
+
if (!rgb) return 0;
|
|
1463
|
+
const r = toLinear(rgb.r);
|
|
1464
|
+
const g = toLinear(rgb.g);
|
|
1465
|
+
const b = toLinear(rgb.b);
|
|
1466
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1467
|
+
}
|
|
1468
|
+
function contrastRatio(foregroundHex, backgroundHex) {
|
|
1469
|
+
const foreground = relativeLuminance(foregroundHex);
|
|
1470
|
+
const background = relativeLuminance(backgroundHex);
|
|
1471
|
+
const lighter = Math.max(foreground, background);
|
|
1472
|
+
const darker = Math.min(foreground, background);
|
|
1473
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
1474
|
+
}
|
|
1475
|
+
function ensureContrast(foregroundHex, backgroundHex, minRatio) {
|
|
1476
|
+
const normalizedForeground = normalizeHexColor(foregroundHex) ?? normalizeHexColor(backgroundHex) ?? "#000000";
|
|
1477
|
+
const normalizedBackground = normalizeHexColor(backgroundHex) ?? DARK_SURFACE;
|
|
1478
|
+
if (contrastRatio(normalizedForeground, normalizedBackground) >= minRatio) {
|
|
1479
|
+
return normalizedForeground;
|
|
1480
|
+
}
|
|
1481
|
+
const backgroundIsDark = relativeLuminance(normalizedBackground) < relativeLuminance(normalizedForeground);
|
|
1482
|
+
const targetMix = backgroundIsDark ? LIGHT_SURFACE : "#111111";
|
|
1483
|
+
let next = normalizedForeground;
|
|
1484
|
+
for (let index = 1; index <= 12; index += 1) {
|
|
1485
|
+
next = mixColors(normalizedForeground, targetMix, index * 0.06);
|
|
1486
|
+
if (contrastRatio(next, normalizedBackground) >= minRatio) {
|
|
1487
|
+
return next;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return next;
|
|
1491
|
+
}
|
|
1492
|
+
function adaptSolidColor(hex, mode) {
|
|
1493
|
+
const normalized = normalizeHexColor(hex);
|
|
1494
|
+
if (!normalized) return hex;
|
|
1495
|
+
const rgb = hexToRgb(normalized);
|
|
1496
|
+
if (!rgb) return normalized;
|
|
1497
|
+
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
1498
|
+
let nextLightness = hsl.l;
|
|
1499
|
+
let nextSaturation = hsl.s;
|
|
1500
|
+
if (mode === "light" && hsl.l < 0.42) {
|
|
1501
|
+
nextLightness = 0.56;
|
|
1502
|
+
nextSaturation = clamp(hsl.s * 0.96, 0, 1);
|
|
1503
|
+
} else if (mode === "dark" && hsl.l > 0.72) {
|
|
1504
|
+
nextLightness = 0.56;
|
|
1505
|
+
nextSaturation = clamp(hsl.s * 0.94, 0, 1);
|
|
1506
|
+
}
|
|
1507
|
+
const nextRgb = hslToRgb(hsl.h, nextSaturation, nextLightness);
|
|
1508
|
+
return rgbToHex(nextRgb.r, nextRgb.g, nextRgb.b);
|
|
1509
|
+
}
|
|
1510
|
+
function adaptFeatureColor(hex, args) {
|
|
1511
|
+
const role = args.role ?? "solid";
|
|
1512
|
+
const solid = adaptSolidColor(hex, args.mode);
|
|
1513
|
+
const surfaceBase = args.mode === "dark" ? DARK_SURFACE : LIGHT_SURFACE;
|
|
1514
|
+
switch (role) {
|
|
1515
|
+
case "surface":
|
|
1516
|
+
return mixColors(solid, surfaceBase, args.mode === "dark" ? 0.78 : 0.86);
|
|
1517
|
+
case "border":
|
|
1518
|
+
return mixColors(solid, surfaceBase, args.mode === "dark" ? 0.52 : 0.6);
|
|
1519
|
+
case "text":
|
|
1520
|
+
return ensureContrast(
|
|
1521
|
+
solid,
|
|
1522
|
+
surfaceBase,
|
|
1523
|
+
args.mode === "dark" ? 4 : 4.5
|
|
1524
|
+
);
|
|
1525
|
+
case "solid":
|
|
1526
|
+
default:
|
|
1527
|
+
return solid;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
function getReadableFeatureTextColor(backgroundHex) {
|
|
1531
|
+
const lightContrast = contrastRatio(LIGHT_SURFACE, backgroundHex);
|
|
1532
|
+
const darkContrast = contrastRatio("#111111", backgroundHex);
|
|
1533
|
+
return lightContrast >= darkContrast ? LIGHT_SURFACE : "#111111";
|
|
1534
|
+
}
|
|
1535
|
+
function getFeatureColorTokens(hex, mode) {
|
|
1536
|
+
const solid = adaptFeatureColor(hex, { mode, role: "solid" });
|
|
1537
|
+
const surface = adaptFeatureColor(hex, { mode, role: "surface" });
|
|
1538
|
+
const border = adaptFeatureColor(hex, { mode, role: "border" });
|
|
1539
|
+
const text = adaptFeatureColor(hex, { mode, role: "text" });
|
|
1540
|
+
return {
|
|
1541
|
+
solid,
|
|
1542
|
+
surface,
|
|
1543
|
+
border,
|
|
1544
|
+
text,
|
|
1545
|
+
contrastText: getReadableFeatureTextColor(solid)
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function syncAdaptiveFeatureColors(root, mode) {
|
|
1549
|
+
if (!root || typeof root.querySelectorAll !== "function") {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const elements = root.querySelectorAll("[style]");
|
|
1553
|
+
for (const element of elements) {
|
|
1554
|
+
const originalStyle = element.getAttribute("style");
|
|
1555
|
+
if (!originalStyle) continue;
|
|
1556
|
+
const colorMatch = originalStyle.match(/(?:^|;)\s*color:\s*([^;]+)/i);
|
|
1557
|
+
const backgroundMatch = originalStyle.match(
|
|
1558
|
+
/(?:^|;)\s*background-color:\s*([^;]+)/i
|
|
1559
|
+
);
|
|
1560
|
+
if (colorMatch) {
|
|
1561
|
+
const raw = element.dataset.enseraThemeColor ?? parseCssColorToHex(colorMatch[1]) ?? void 0;
|
|
1562
|
+
if (raw) {
|
|
1563
|
+
element.dataset.enseraThemeColor = raw;
|
|
1564
|
+
element.style.color = adaptFeatureColor(raw, {
|
|
1565
|
+
mode,
|
|
1566
|
+
role: "text"
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (backgroundMatch) {
|
|
1571
|
+
const raw = element.dataset.enseraThemeBackground ?? parseCssColorToHex(backgroundMatch[1]) ?? void 0;
|
|
1572
|
+
if (raw) {
|
|
1573
|
+
element.dataset.enseraThemeBackground = raw;
|
|
1574
|
+
element.style.backgroundColor = adaptFeatureColor(raw, {
|
|
1575
|
+
mode,
|
|
1576
|
+
role: "surface"
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
bindWindowThemeListeners();
|
|
1583
|
+
|
|
1263
1584
|
// src/ui/tokens.ts
|
|
1264
1585
|
var tokens = {
|
|
1265
1586
|
// Spacing scale (in pixels)
|
|
@@ -1999,6 +2320,8 @@ export {
|
|
|
1999
2320
|
PluginUnknownActionError,
|
|
2000
2321
|
PluginValidationError,
|
|
2001
2322
|
Row,
|
|
2323
|
+
adaptFeatureColor,
|
|
2324
|
+
applyPluginTheme,
|
|
2002
2325
|
attachActionDispatcher,
|
|
2003
2326
|
broadcast,
|
|
2004
2327
|
createPluginFetch,
|
|
@@ -2009,13 +2332,23 @@ export {
|
|
|
2009
2332
|
createPluginStorageIndexedDB,
|
|
2010
2333
|
createSyncedState,
|
|
2011
2334
|
defineActions,
|
|
2335
|
+
dispatchPluginThemeChange,
|
|
2336
|
+
getFeatureColorTokens,
|
|
2337
|
+
getPluginThemeSnapshot,
|
|
2338
|
+
getReadableFeatureTextColor,
|
|
2012
2339
|
initBroadcast,
|
|
2340
|
+
initializePluginTheme,
|
|
2013
2341
|
makeStorageNamespace,
|
|
2342
|
+
normalizePluginThemeMode,
|
|
2343
|
+
normalizePluginThemeSnapshot,
|
|
2014
2344
|
onBroadcast,
|
|
2015
2345
|
openOverlay,
|
|
2016
2346
|
runActionSafe,
|
|
2017
2347
|
setupContextMenuRelay,
|
|
2348
|
+
subscribePluginTheme,
|
|
2349
|
+
syncAdaptiveFeatureColors,
|
|
2018
2350
|
tokens,
|
|
2019
2351
|
useBroadcastListener,
|
|
2352
|
+
usePluginTheme,
|
|
2020
2353
|
useSyncedState
|
|
2021
2354
|
};
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ensera/plugin-frontend",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Runtime frontend SDK for Ensera plugins.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"module": "./dist/index.js",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.js"
|
|
13
|
-
},
|
|
14
|
-
"./package.json": "./package.json"
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist",
|
|
18
|
-
"README.md"
|
|
19
|
-
],
|
|
20
|
-
"sideEffects": false,
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsup src/index.ts --format esm --dts --clean --target es2022 --outDir dist",
|
|
23
|
-
"dev": "tsup src/index.ts --format esm --dts --watch --target es2022 --outDir dist",
|
|
24
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
-
"prepublishOnly": "npm run build && npm run typecheck"
|
|
26
|
-
},
|
|
27
|
-
"keywords": [
|
|
28
|
-
"ensera",
|
|
29
|
-
"frontend",
|
|
30
|
-
"sdk",
|
|
31
|
-
"plugin",
|
|
32
|
-
"react"
|
|
33
|
-
],
|
|
34
|
-
"publishConfig": {
|
|
35
|
-
"access": "public"
|
|
36
|
-
},
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=18"
|
|
39
|
-
},
|
|
40
|
-
"peerDependencies": {
|
|
41
|
-
"react": "^18.0.0",
|
|
42
|
-
"react-dom": "^18.0.0"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@types/react": "^18.3.28",
|
|
46
|
-
"@types/react-dom": "^18.3.7",
|
|
47
|
-
"react": "^18.3.1",
|
|
48
|
-
"react-dom": "^18.3.1"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ensera/plugin-frontend",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Runtime frontend SDK for Ensera plugins.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm --dts --clean --target es2022 --outDir dist",
|
|
23
|
+
"dev": "tsup src/index.ts --format esm --dts --watch --target es2022 --outDir dist",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"prepublishOnly": "npm run build && npm run typecheck"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"ensera",
|
|
29
|
+
"frontend",
|
|
30
|
+
"sdk",
|
|
31
|
+
"plugin",
|
|
32
|
+
"react"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": "^18.0.0",
|
|
42
|
+
"react-dom": "^18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^18.3.28",
|
|
46
|
+
"@types/react-dom": "^18.3.7",
|
|
47
|
+
"react": "^18.3.1",
|
|
48
|
+
"react-dom": "^18.3.1"
|
|
49
|
+
}
|
|
50
|
+
}
|