@ensera/plugin-frontend 1.0.0 → 1.1.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 -15
- package/dist/index.d.ts +57 -1
- package/dist/index.js +424 -1
- 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,31 @@ 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
|
+
};
|
|
91
|
+
};
|
|
92
|
+
type WorkspaceMemberRole = "OWNER" | "ADMIN" | "MEMBER" | "GUEST";
|
|
93
|
+
type PluginWorkspaceMember = {
|
|
94
|
+
userId: string;
|
|
95
|
+
name: string | null;
|
|
96
|
+
email: string;
|
|
97
|
+
image: string | null;
|
|
98
|
+
role: WorkspaceMemberRole;
|
|
74
99
|
};
|
|
75
100
|
/**
|
|
76
101
|
* Launch modes for plugin instances
|
|
@@ -548,6 +573,37 @@ declare function useSyncedState<T extends JsonValue>(args: {
|
|
|
548
573
|
initialState: T;
|
|
549
574
|
}): [T, (state: T | ((prev: T) => T)) => void];
|
|
550
575
|
|
|
576
|
+
declare function normalizePluginThemeMode(value: unknown): PluginThemeMode;
|
|
577
|
+
declare function normalizePluginThemeSnapshot(value?: Partial<PluginThemeSnapshot> | PluginThemeMessage | null): PluginThemeSnapshot;
|
|
578
|
+
declare function applyPluginTheme(value?: Partial<PluginThemeSnapshot> | PluginThemeMessage | null, root?: HTMLElement): PluginThemeSnapshot;
|
|
579
|
+
declare function dispatchPluginThemeChange(value?: Partial<PluginThemeSnapshot> | PluginThemeMessage | null): PluginThemeSnapshot;
|
|
580
|
+
declare function initializePluginTheme(value?: Partial<PluginThemeSnapshot> | PluginThemeMessage | null): PluginThemeSnapshot;
|
|
581
|
+
declare function getPluginThemeSnapshot(): PluginThemeSnapshot;
|
|
582
|
+
declare function subscribePluginTheme(listener: () => void): () => boolean;
|
|
583
|
+
declare function usePluginTheme(): PluginThemeSnapshot;
|
|
584
|
+
declare function adaptFeatureColor(hex: string, args: {
|
|
585
|
+
mode: PluginThemeMode;
|
|
586
|
+
role?: PluginThemeColorRole;
|
|
587
|
+
}): string;
|
|
588
|
+
declare function getReadableFeatureTextColor(backgroundHex: string): "#FFFFFF" | "#111111";
|
|
589
|
+
declare function getFeatureColorTokens(hex: string, mode: PluginThemeMode): {
|
|
590
|
+
solid: string;
|
|
591
|
+
surface: string;
|
|
592
|
+
border: string;
|
|
593
|
+
text: string;
|
|
594
|
+
contrastText: string;
|
|
595
|
+
};
|
|
596
|
+
declare function syncAdaptiveFeatureColors(root: ParentNode | null | undefined, mode: PluginThemeMode): void;
|
|
597
|
+
|
|
598
|
+
declare function getWorkspaceMembers(options?: {
|
|
599
|
+
includeGuests?: boolean;
|
|
600
|
+
}): PluginWorkspaceMember[];
|
|
601
|
+
declare function getUser(userId: string): PluginWorkspaceMember | null;
|
|
602
|
+
declare function isMembersReady(): boolean;
|
|
603
|
+
declare function useWorkspaceMembers(options?: {
|
|
604
|
+
includeGuests?: boolean;
|
|
605
|
+
}): PluginWorkspaceMember[];
|
|
606
|
+
|
|
551
607
|
declare const tokens: {
|
|
552
608
|
readonly spacing: {
|
|
553
609
|
readonly xs: 4;
|
|
@@ -749,4 +805,4 @@ declare function setupContextMenuRelay(ctx: {
|
|
|
749
805
|
instanceId: string;
|
|
750
806
|
}): () => void;
|
|
751
807
|
|
|
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 };
|
|
808
|
+
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, type PluginWorkspaceMember, Row, type RowAlign, type RowHeight, type RowJustify, type RowProps, type SyncCallback, type SyncEventType, type SyncMessage, type SyncPayload, type SyncedState, type Tokens, type WorkspaceMemberRole, adaptFeatureColor, applyPluginTheme, attachActionDispatcher, broadcast, createPluginFetch, createPluginLogger, createPluginNotify, createPluginRuntime, createPluginStorage, createPluginStorageIndexedDB, createSyncedState, defineActions, dispatchPluginThemeChange, getFeatureColorTokens, getPluginThemeSnapshot, getReadableFeatureTextColor, getUser, getWorkspaceMembers, initBroadcast, initializePluginTheme, isMembersReady, makeStorageNamespace, normalizePluginThemeMode, normalizePluginThemeSnapshot, onBroadcast, openOverlay, runActionSafe, setupContextMenuRelay, subscribePluginTheme, syncAdaptiveFeatureColors, tokens, useBroadcastListener, usePluginTheme, useSyncedState, useWorkspaceMembers };
|
package/dist/index.js
CHANGED
|
@@ -1260,6 +1260,413 @@ 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
|
+
|
|
1584
|
+
// src/members.ts
|
|
1585
|
+
import { useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
1586
|
+
var membersStore = [];
|
|
1587
|
+
var membersInitialized = false;
|
|
1588
|
+
var boundWindowListeners2 = false;
|
|
1589
|
+
var listeners2 = /* @__PURE__ */ new Set();
|
|
1590
|
+
function notifyListeners() {
|
|
1591
|
+
for (const listener of listeners2) {
|
|
1592
|
+
listener();
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function getMembersSnapshot() {
|
|
1596
|
+
return membersStore;
|
|
1597
|
+
}
|
|
1598
|
+
function subscribeMembersStore(listener) {
|
|
1599
|
+
listeners2.add(listener);
|
|
1600
|
+
return () => listeners2.delete(listener);
|
|
1601
|
+
}
|
|
1602
|
+
function bindMembersMessageListeners() {
|
|
1603
|
+
if (boundWindowListeners2 || typeof window === "undefined") {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
window.addEventListener("message", (event) => {
|
|
1607
|
+
const message = event.data;
|
|
1608
|
+
if (!message || typeof message !== "object") {
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
switch (message.type) {
|
|
1612
|
+
case "ENSERA_INIT": {
|
|
1613
|
+
const members = message.payload?.members;
|
|
1614
|
+
if (Array.isArray(members)) {
|
|
1615
|
+
setWorkspaceMembers(members);
|
|
1616
|
+
} else {
|
|
1617
|
+
setWorkspaceMembers([]);
|
|
1618
|
+
}
|
|
1619
|
+
break;
|
|
1620
|
+
}
|
|
1621
|
+
case "ENSERA_MEMBERS_UPDATE": {
|
|
1622
|
+
const members = message.payload?.members;
|
|
1623
|
+
if (Array.isArray(members)) {
|
|
1624
|
+
updateWorkspaceMembers(members);
|
|
1625
|
+
}
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
boundWindowListeners2 = true;
|
|
1631
|
+
}
|
|
1632
|
+
function setWorkspaceMembers(members) {
|
|
1633
|
+
membersStore = Array.isArray(members) ? members : [];
|
|
1634
|
+
membersInitialized = true;
|
|
1635
|
+
notifyListeners();
|
|
1636
|
+
}
|
|
1637
|
+
function updateWorkspaceMembers(members) {
|
|
1638
|
+
membersStore = Array.isArray(members) ? members : [];
|
|
1639
|
+
notifyListeners();
|
|
1640
|
+
}
|
|
1641
|
+
function getWorkspaceMembers(options) {
|
|
1642
|
+
bindMembersMessageListeners();
|
|
1643
|
+
if (!options?.includeGuests) {
|
|
1644
|
+
return membersStore.filter((member) => member.role !== "GUEST");
|
|
1645
|
+
}
|
|
1646
|
+
return [...membersStore];
|
|
1647
|
+
}
|
|
1648
|
+
function getUser(userId) {
|
|
1649
|
+
bindMembersMessageListeners();
|
|
1650
|
+
return membersStore.find((member) => member.userId === userId) ?? null;
|
|
1651
|
+
}
|
|
1652
|
+
function isMembersReady() {
|
|
1653
|
+
bindMembersMessageListeners();
|
|
1654
|
+
return membersInitialized;
|
|
1655
|
+
}
|
|
1656
|
+
function useWorkspaceMembers(options) {
|
|
1657
|
+
bindMembersMessageListeners();
|
|
1658
|
+
const all = useSyncExternalStore2(
|
|
1659
|
+
subscribeMembersStore,
|
|
1660
|
+
getMembersSnapshot,
|
|
1661
|
+
getMembersSnapshot
|
|
1662
|
+
);
|
|
1663
|
+
if (!options?.includeGuests) {
|
|
1664
|
+
return all.filter((member) => member.role !== "GUEST");
|
|
1665
|
+
}
|
|
1666
|
+
return all;
|
|
1667
|
+
}
|
|
1668
|
+
bindMembersMessageListeners();
|
|
1669
|
+
|
|
1263
1670
|
// src/ui/tokens.ts
|
|
1264
1671
|
var tokens = {
|
|
1265
1672
|
// Spacing scale (in pixels)
|
|
@@ -1999,6 +2406,8 @@ export {
|
|
|
1999
2406
|
PluginUnknownActionError,
|
|
2000
2407
|
PluginValidationError,
|
|
2001
2408
|
Row,
|
|
2409
|
+
adaptFeatureColor,
|
|
2410
|
+
applyPluginTheme,
|
|
2002
2411
|
attachActionDispatcher,
|
|
2003
2412
|
broadcast,
|
|
2004
2413
|
createPluginFetch,
|
|
@@ -2009,13 +2418,27 @@ export {
|
|
|
2009
2418
|
createPluginStorageIndexedDB,
|
|
2010
2419
|
createSyncedState,
|
|
2011
2420
|
defineActions,
|
|
2421
|
+
dispatchPluginThemeChange,
|
|
2422
|
+
getFeatureColorTokens,
|
|
2423
|
+
getPluginThemeSnapshot,
|
|
2424
|
+
getReadableFeatureTextColor,
|
|
2425
|
+
getUser,
|
|
2426
|
+
getWorkspaceMembers,
|
|
2012
2427
|
initBroadcast,
|
|
2428
|
+
initializePluginTheme,
|
|
2429
|
+
isMembersReady,
|
|
2013
2430
|
makeStorageNamespace,
|
|
2431
|
+
normalizePluginThemeMode,
|
|
2432
|
+
normalizePluginThemeSnapshot,
|
|
2014
2433
|
onBroadcast,
|
|
2015
2434
|
openOverlay,
|
|
2016
2435
|
runActionSafe,
|
|
2017
2436
|
setupContextMenuRelay,
|
|
2437
|
+
subscribePluginTheme,
|
|
2438
|
+
syncAdaptiveFeatureColors,
|
|
2018
2439
|
tokens,
|
|
2019
2440
|
useBroadcastListener,
|
|
2020
|
-
|
|
2441
|
+
usePluginTheme,
|
|
2442
|
+
useSyncedState,
|
|
2443
|
+
useWorkspaceMembers
|
|
2021
2444
|
};
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ensera/plugin-frontend",
|
|
3
|
-
"version": "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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ensera/plugin-frontend",
|
|
3
|
+
"version": "1.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
|
+
}
|