@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 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
- useSyncedState
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.0.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.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
+ }