@aiot-nexus/plugin-sdk 3.1.1 → 3.2.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.
@@ -8,6 +8,40 @@ const requestMessageType = "nexus-plugin:request";
8
8
  const responseMessageType = "nexus-plugin:response";
9
9
  const resizeMessageType = "nexus-plugin:resize";
10
10
  const defaultInitializationTimeoutMs = 20000;
11
+ const systemThemeMediaQuery = "(prefers-color-scheme: dark)";
12
+ const themeTokenCssVariables = [
13
+ ["background", "--nexus-color-background"],
14
+ ["surface", "--nexus-color-surface"],
15
+ ["surfaceAlt", "--nexus-color-surface-alt"],
16
+ ["surfaceRaised", "--nexus-color-surface-raised"],
17
+ ["surfaceHover", "--nexus-color-surface-hover"],
18
+ ["surfaceInset", "--nexus-color-surface-inset"],
19
+ ["border", "--nexus-color-border"],
20
+ ["borderSoft", "--nexus-color-border-soft"],
21
+ ["borderStrong", "--nexus-color-border-strong"],
22
+ ["text", "--nexus-color-text"],
23
+ ["textMuted", "--nexus-color-text-muted"],
24
+ ["textSoft", "--nexus-color-text-soft"],
25
+ ["textFaint", "--nexus-color-text-faint"],
26
+ ["primary", "--nexus-color-primary"],
27
+ ["accent", "--nexus-color-accent"],
28
+ ["onPrimary", "--nexus-color-on-primary"],
29
+ ["primarySoft", "--nexus-color-primary-soft"],
30
+ ["info", "--nexus-color-info"],
31
+ ["infoSoft", "--nexus-color-info-soft"],
32
+ ["positive", "--nexus-color-positive"],
33
+ ["positiveSoft", "--nexus-color-positive-soft"],
34
+ ["warning", "--nexus-color-warning"],
35
+ ["warningSoft", "--nexus-color-warning-soft"],
36
+ ["negative", "--nexus-color-negative"],
37
+ ["negativeSoft", "--nexus-color-negative-soft"],
38
+ ["mutedSoft", "--nexus-color-muted-soft"],
39
+ ["onInfo", "--nexus-color-on-info"],
40
+ ["onPositive", "--nexus-color-on-positive"],
41
+ ["onWarning", "--nexus-color-on-warning"],
42
+ ["onNegative", "--nexus-color-on-negative"]
43
+ ];
44
+ const hostThemeOwners = new WeakMap();
11
45
  class PluginFrameClientError extends Error {
12
46
  constructor(code, message) {
13
47
  super(message);
@@ -25,6 +59,7 @@ function createPluginFrameClient(options = {}) {
25
59
  const targetWindow = options.targetWindow || frameWindow.parent;
26
60
  const initializationTimeoutMs = options.initializationTimeoutMs === undefined ? defaultInitializationTimeoutMs : options.initializationTimeoutMs;
27
61
  const requestTimeoutMs = options.requestTimeoutMs === undefined ? 0 : options.requestTimeoutMs;
62
+ const autoApplyHostTheme = options.autoApplyHostTheme !== false;
28
63
  if (!Number.isFinite(initializationTimeoutMs) || initializationTimeoutMs <= 0) {
29
64
  throw new PluginFrameClientError("ProtocolError", "initializationTimeoutMs must be a positive finite number.");
30
65
  }
@@ -39,6 +74,16 @@ function createPluginFrameClient(options = {}) {
39
74
  let contextTimeoutId;
40
75
  let resizeTimer;
41
76
  let resizeObserver;
77
+ const hostThemeOwner = Symbol("nexus-plugin-host-theme");
78
+ let hostThemeRoot;
79
+ let hostThemeMarker;
80
+ let hostThemePreviousMarker;
81
+ let hostThemeHadPreviousMarker = false;
82
+ const hostThemeAppliedValues = new Map();
83
+ const hostThemePreviousValues = new Map();
84
+ const systemThemeMedia = autoApplyHostTheme && typeof frameWindow.matchMedia === "function"
85
+ ? frameWindow.matchMedia(systemThemeMediaQuery)
86
+ : undefined;
42
87
  const pendingRequests = new Map();
43
88
  const contextListeners = new Set();
44
89
  let resolveContext;
@@ -56,6 +101,14 @@ function createPluginFrameClient(options = {}) {
56
101
  }
57
102
  }, initializationTimeoutMs);
58
103
  frameWindow.addEventListener("message", handleMessage);
104
+ if (systemThemeMedia) {
105
+ if (typeof systemThemeMedia.addEventListener === "function") {
106
+ systemThemeMedia.addEventListener("change", handleSystemThemeChange);
107
+ }
108
+ else {
109
+ systemThemeMedia.addListener(handleSystemThemeChange);
110
+ }
111
+ }
59
112
  if (options.autoResize !== false) {
60
113
  frameWindow.addEventListener("resize", scheduleResize);
61
114
  const ResizeObserverConstructor = frameWindow.ResizeObserver;
@@ -128,6 +181,7 @@ function createPluginFrameClient(options = {}) {
128
181
  const hadContext = contextValue !== undefined;
129
182
  hostOrigin = event.origin;
130
183
  contextValue = event.data.context;
184
+ applyCurrentHostTheme();
131
185
  if (!contextSettled) {
132
186
  contextSettled = true;
133
187
  if (contextTimeoutId !== undefined) {
@@ -170,6 +224,99 @@ function createPluginFrameClient(options = {}) {
170
224
  }
171
225
  }
172
226
  }
227
+ function handleSystemThemeChange() {
228
+ if (contextValue?.themeMode === "system") {
229
+ applyCurrentHostTheme();
230
+ }
231
+ }
232
+ function applyCurrentHostTheme() {
233
+ if (!autoApplyHostTheme || !contextValue) {
234
+ return;
235
+ }
236
+ const root = frameWindow.document.documentElement;
237
+ if (!root?.dataset || !root.style) {
238
+ return;
239
+ }
240
+ initializeHostThemeOwnership(root);
241
+ const resolvedMode = contextValue.themeMode === "system"
242
+ ? (systemThemeMedia?.matches ? "dark" : "light")
243
+ : contextValue.themeMode;
244
+ root.dataset.nexusTheme = resolvedMode;
245
+ hostThemeMarker = resolvedMode;
246
+ const tokens = contextValue.theme?.[resolvedMode];
247
+ for (const [tokenName, cssVariable] of themeTokenCssVariables) {
248
+ if (tokens) {
249
+ const value = tokens[tokenName];
250
+ root.style.setProperty(cssVariable, value);
251
+ hostThemeAppliedValues.set(cssVariable, value);
252
+ }
253
+ else {
254
+ restoreHostThemeVariable(root, cssVariable);
255
+ }
256
+ }
257
+ }
258
+ function initializeHostThemeOwnership(root) {
259
+ if (hostThemeRoot === root) {
260
+ hostThemeOwners.set(root, hostThemeOwner);
261
+ return;
262
+ }
263
+ if (hostThemeRoot) {
264
+ cleanupHostTheme();
265
+ }
266
+ hostThemeRoot = root;
267
+ hostThemeOwners.set(root, hostThemeOwner);
268
+ hostThemePreviousMarker = root.dataset.nexusTheme;
269
+ hostThemeHadPreviousMarker = Object.prototype.hasOwnProperty.call(root.dataset, "nexusTheme");
270
+ hostThemePreviousValues.clear();
271
+ for (const [, cssVariable] of themeTokenCssVariables) {
272
+ hostThemePreviousValues.set(cssVariable, root.style.getPropertyValue(cssVariable));
273
+ }
274
+ }
275
+ function restoreHostThemeVariable(root, cssVariable) {
276
+ const appliedValue = hostThemeAppliedValues.get(cssVariable);
277
+ if (appliedValue === undefined || root.style.getPropertyValue(cssVariable) !== appliedValue) {
278
+ hostThemeAppliedValues.delete(cssVariable);
279
+ return;
280
+ }
281
+ const previousValue = hostThemePreviousValues.get(cssVariable) || "";
282
+ if (previousValue) {
283
+ root.style.setProperty(cssVariable, previousValue);
284
+ }
285
+ else {
286
+ root.style.removeProperty(cssVariable);
287
+ }
288
+ hostThemeAppliedValues.delete(cssVariable);
289
+ }
290
+ function cleanupHostTheme() {
291
+ const root = hostThemeRoot;
292
+ if (!root) {
293
+ return;
294
+ }
295
+ if (hostThemeOwners.get(root) !== hostThemeOwner) {
296
+ hostThemeRoot = undefined;
297
+ hostThemeAppliedValues.clear();
298
+ hostThemePreviousValues.clear();
299
+ return;
300
+ }
301
+ for (const [, cssVariable] of themeTokenCssVariables) {
302
+ restoreHostThemeVariable(root, cssVariable);
303
+ }
304
+ if (hostThemeMarker !== undefined && root.dataset.nexusTheme === hostThemeMarker) {
305
+ if (hostThemeHadPreviousMarker) {
306
+ root.dataset.nexusTheme = hostThemePreviousMarker || "";
307
+ }
308
+ else {
309
+ delete root.dataset.nexusTheme;
310
+ }
311
+ }
312
+ hostThemeOwners.delete(root);
313
+ hostThemeRoot = undefined;
314
+ hostThemeMarker = undefined;
315
+ hostThemePreviousMarker = undefined;
316
+ hostThemeHadPreviousMarker = false;
317
+ hostThemeAppliedValues.clear();
318
+ hostThemePreviousValues.clear();
319
+ }
173
320
  function scheduleResize() {
174
321
  if (disposed || !contextValue || options.autoResize === false) {
175
322
  return;
@@ -204,6 +351,19 @@ function createPluginFrameClient(options = {}) {
204
351
  disposed = true;
205
352
  frameWindow.removeEventListener("message", handleMessage);
206
353
  frameWindow.removeEventListener("resize", scheduleResize);
354
+ if (systemThemeMedia) {
355
+ if (typeof systemThemeMedia.removeEventListener === "function") {
356
+ systemThemeMedia.removeEventListener("change", handleSystemThemeChange);
357
+ }
358
+ else {
359
+ systemThemeMedia.removeListener(handleSystemThemeChange);
360
+ }
361
+ }
362
+ cleanupHostTheme();
363
+ if (contextTimeoutId !== undefined) {
364
+ frameWindow.clearTimeout(contextTimeoutId);
365
+ contextTimeoutId = undefined;
366
+ }
207
367
  resizeObserver?.disconnect();
208
368
  if (resizeTimer !== undefined) {
209
369
  frameWindow.clearTimeout(resizeTimer);
@@ -241,8 +401,45 @@ function isPluginRendererContext(value) {
241
401
  && typeof value.contributionId === "string"
242
402
  && typeof value.language === "string"
243
403
  && (value.themeMode === "light" || value.themeMode === "dark" || value.themeMode === "system")
404
+ && (value.theme === undefined || isPluginRendererTheme(value.theme))
244
405
  && typeof value.hostApiVersion === "string";
245
406
  }
407
+ function isPluginRendererTheme(value) {
408
+ if (!hasExactKeys(value, ["light", "dark"])) {
409
+ return false;
410
+ }
411
+ return isPluginThemeTokens(value.light) && isPluginThemeTokens(value.dark);
412
+ }
413
+ function isPluginThemeTokens(value) {
414
+ const tokenNames = themeTokenCssVariables.map(([tokenName]) => tokenName);
415
+ return hasExactKeys(value, tokenNames)
416
+ && tokenNames.every((tokenName) => isSafeCssColorValue(value[tokenName]));
417
+ }
418
+ function hasExactKeys(value, expectedKeys) {
419
+ if (!isRecord(value)) {
420
+ return false;
421
+ }
422
+ const keys = Object.keys(value);
423
+ return keys.length === expectedKeys.length && keys.every((key) => expectedKeys.includes(key));
424
+ }
425
+ function isSafeCssColorValue(value) {
426
+ if (typeof value !== "string" || value.length === 0 || value.length > 64 || /[;{}]/.test(value)) {
427
+ return false;
428
+ }
429
+ if (/^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(value)) {
430
+ return true;
431
+ }
432
+ const match = /^(rgb|rgba)\(([^)]+)\)$/.exec(value);
433
+ if (!match) {
434
+ return false;
435
+ }
436
+ const parts = match[2].split(",").map((part) => part.trim());
437
+ const expectedLength = match[1] === "rgba" ? 4 : 3;
438
+ if (parts.length !== expectedLength || !parts.slice(0, 3).every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)) {
439
+ return false;
440
+ }
441
+ return expectedLength === 3 || /^(?:0|1|0?\.\d+)$/.test(parts[3]) && Number(parts[3]) <= 1;
442
+ }
246
443
  function isNexusResult(value) {
247
444
  if (!isRecord(value) || typeof value.ok !== "boolean") {
248
445
  return false;
@@ -1,6 +1,7 @@
1
1
  import pluginManifestSchema from "../.generated/plugin-manifest.schema.json";
2
2
  export { pluginManifestSchema };
3
3
  export * from "../.generated/plugin-framework-version";
4
+ export { NEXUS_PLUGIN_RENDERER_THEME } from "../.generated/theme-contract";
4
5
  export * from "./sdk-info";
5
6
  export * from "./types";
6
7
  export * from "./electron-namespaces";
package/dist/src/index.js CHANGED
@@ -17,10 +17,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.pluginManifestSchema = void 0;
20
+ exports.NEXUS_PLUGIN_RENDERER_THEME = exports.pluginManifestSchema = void 0;
21
21
  const plugin_manifest_schema_json_1 = __importDefault(require("../.generated/plugin-manifest.schema.json"));
22
22
  exports.pluginManifestSchema = plugin_manifest_schema_json_1.default;
23
23
  __exportStar(require("../.generated/plugin-framework-version"), exports);
24
+ var theme_contract_1 = require("../.generated/theme-contract");
25
+ Object.defineProperty(exports, "NEXUS_PLUGIN_RENDERER_THEME", { enumerable: true, get: function () { return theme_contract_1.NEXUS_PLUGIN_RENDERER_THEME; } });
24
26
  __exportStar(require("./sdk-info"), exports);
25
27
  __exportStar(require("./types"), exports);
26
28
  __exportStar(require("./electron-namespaces"), exports);
@@ -1,4 +1,4 @@
1
- export declare const PLUGIN_SDK_VERSION = "3.1.1";
1
+ export declare const PLUGIN_SDK_VERSION = "3.2.0";
2
2
  export declare const PLUGIN_SDK_NODE_ENGINE = ">=16.20.0 <17";
3
3
  export declare const PLUGIN_SDK_NPM_ENGINE = ">=6.13.4";
4
4
  export declare const PLUGIN_SDK_ELECTRON_VERSION = "22.3.27";
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getPluginSdkInfo = exports.PLUGIN_SDK_TYPES_NODE_VERSION = exports.PLUGIN_SDK_TYPESCRIPT_VERSION = exports.PLUGIN_SDK_ELECTRON_VERSION = exports.PLUGIN_SDK_NPM_ENGINE = exports.PLUGIN_SDK_NODE_ENGINE = exports.PLUGIN_SDK_VERSION = void 0;
4
4
  const plugin_framework_version_1 = require("../.generated/plugin-framework-version");
5
- exports.PLUGIN_SDK_VERSION = "3.1.1";
5
+ exports.PLUGIN_SDK_VERSION = "3.2.0";
6
6
  exports.PLUGIN_SDK_NODE_ENGINE = ">=16.20.0 <17";
7
7
  exports.PLUGIN_SDK_NPM_ENGINE = ">=6.13.4";
8
8
  exports.PLUGIN_SDK_ELECTRON_VERSION = "22.3.27";
@@ -12,6 +12,42 @@ export type NexusResult<T> = {
12
12
  };
13
13
  export type ThemeMode = "light" | "dark" | "system";
14
14
  export type LaunchPageParams = Record<string, string>;
15
+ export interface PluginThemeTokens {
16
+ background: string;
17
+ surface: string;
18
+ surfaceAlt: string;
19
+ surfaceRaised: string;
20
+ surfaceHover: string;
21
+ surfaceInset: string;
22
+ border: string;
23
+ borderSoft: string;
24
+ borderStrong: string;
25
+ text: string;
26
+ textMuted: string;
27
+ textSoft: string;
28
+ textFaint: string;
29
+ primary: string;
30
+ accent: string;
31
+ onPrimary: string;
32
+ primarySoft: string;
33
+ info: string;
34
+ infoSoft: string;
35
+ positive: string;
36
+ positiveSoft: string;
37
+ warning: string;
38
+ warningSoft: string;
39
+ negative: string;
40
+ negativeSoft: string;
41
+ mutedSoft: string;
42
+ onInfo: string;
43
+ onPositive: string;
44
+ onWarning: string;
45
+ onNegative: string;
46
+ }
47
+ export interface PluginRendererTheme {
48
+ light: PluginThemeTokens;
49
+ dark: PluginThemeTokens;
50
+ }
15
51
  export type PluginPermission = "businessLog" | "settings" | "filesystem" | "udp" | "serial" | "externalProcess" | "dialog";
16
52
  export type PluginPermissionSupportState = "implemented" | "routeGuard" | "reserved";
17
53
  export interface PluginPermissionSummary {
@@ -76,6 +112,7 @@ export interface PluginRendererContext {
76
112
  launchParams?: LaunchPageParams;
77
113
  language: string;
78
114
  themeMode: ThemeMode;
115
+ theme?: PluginRendererTheme;
79
116
  hostApiVersion: string;
80
117
  }
81
118
  export interface PluginSettingsDocument {
@@ -30,6 +30,7 @@ const http = __importStar(require("http"));
30
30
  const path = __importStar(require("path"));
31
31
  const dev_1 = require("./dev");
32
32
  const plugin_framework_version_1 = require("../.generated/plugin-framework-version");
33
+ const theme_contract_1 = require("../.generated/theme-contract");
33
34
  const loopbackHost = "127.0.0.1";
34
35
  const defaultBodyLimit = 1024 * 1024;
35
36
  const browserApiPrefix = "/__nexus_plugin_dev";
@@ -165,6 +166,7 @@ function pluginContext(target) {
165
166
  contributionId: target.contributionId,
166
167
  language: target.language,
167
168
  themeMode: target.themeMode,
169
+ theme: theme_contract_1.NEXUS_PLUGIN_RENDERER_THEME,
168
170
  hostApiVersion: plugin_framework_version_1.PLUGIN_FRAMEWORK_VERSION
169
171
  };
170
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiot-nexus/plugin-sdk",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Types, PluginFrame client, and CLI for AIoT Nexus independent plugins.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -103,6 +103,10 @@ The renderer calls the trusted-main `demo.echo` operation through `@aiot-nexus/p
103
103
 
104
104
  The renderer follows `context.language` and `context.themeMode` from Nexus, subscribes to in-place context updates without reloading the iframe, and includes `en-US` and `zh-CN` message catalogs. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host and simulate Nexus context changes. Below that toolbar, the iframe has no host padding, border, minimum size, or content-driven height and fills the remaining host viewport. Add or edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
105
105
 
106
+ SDK 3.2 automatically maps the active `context.theme` palette to `--nexus-color-*` variables and sets `data-nexus-theme="light|dark"` on the document root. This template consumes those variables with local light/dark fallbacks, so it still renders on an older host. Merely upgrading the SDK does not recolor custom CSS: use the namespaced variables or read the optional palette explicitly, for example `const primary = context.theme?.light.primary`.
107
+
108
+ For a plugin-owned theme, create the client with `createPluginFrameClient({ autoApplyHostTheme: false })`. The palette remains available from `await client.waitForContext()`, but the SDK will not write CSS variables or `data-nexus-theme`. It never overwrites plugin-owned `--bg`, `--accent`, `data-theme`, or `color-scheme` values.
109
+
106
110
  The current SDK has no generic business-event subscription API. For a long-running operation, register a fast `job.start` operation that returns a `taskId`, then expose `job.getStatus` and optional `job.cancel` for renderer polling. See the Nexus repository's plugin IPC and event notification example for the full contract and code.
107
111
 
108
112
  Use `npm run dev:web` for renderer-only UI work in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. It does not execute `mainEntry`, provide Node or Electron main-process APIs, or overwrite the deployable `dist` package. Language and theme controls still simulate Nexus context, while `invokeMain` is intentionally unavailable. Pass `-- --no-open` or `-- --port <port>` when needed; use `npm run dev` for Host API and Electron integration.
@@ -1,4 +1,5 @@
1
1
  import { createPluginFrameClient } from "@aiot-nexus/plugin-sdk/browser";
2
+ import type { PluginThemeTokens } from "@aiot-nexus/plugin-sdk";
2
3
 
3
4
  interface EchoResponse {
4
5
  value: string;
@@ -139,9 +140,11 @@ function applyLanguage(nextLanguage: SupportedLanguage): void {
139
140
  client.notifyResize();
140
141
  }
141
142
 
142
- function applyContext(context: { language: string; themeMode: ThemeMode }): void {
143
+ function applyContext(context: { language: string; themeMode: ThemeMode; theme?: { light: PluginThemeTokens; dark: PluginThemeTokens } }): void {
143
144
  applyLanguage(resolveLanguage(context.language));
144
145
  applyTheme(context.themeMode);
146
+ const resolvedMode = context.themeMode === "dark" || (context.themeMode === "system" && systemDarkMode.matches) ? "dark" : "light";
147
+ contextOutput.dataset.hostPrimary = context.theme?.[resolvedMode].primary || "";
145
148
  revealPluginContent();
146
149
  }
147
150
 
@@ -1,8 +1,28 @@
1
1
  :root {
2
+ --bg: var(--nexus-color-background, #eef1f4);
3
+ --panel: var(--nexus-color-surface, #ffffff);
4
+ --raise: var(--nexus-color-surface-raised, #f7f9fa);
5
+ --hover: var(--nexus-color-surface-hover, #eef3f6);
6
+ --line: var(--nexus-color-border, #d7dde3);
7
+ --line-strong: var(--nexus-color-border-strong, #c2cbd3);
8
+ --fg: var(--nexus-color-text, #212a32);
9
+ --fg-dim: var(--nexus-color-text-muted, #5a6772);
10
+ --brand: var(--nexus-color-primary, #8fc31f);
11
+ --brand-text: var(--nexus-color-accent, #6f9a15);
12
+ --on-brand: var(--nexus-color-on-primary, #ffffff);
13
+ --primary-action: var(--nexus-color-primary, #8fc31f);
14
+ --on-primary: var(--nexus-color-on-primary, #ffffff);
15
+ --on-negative: var(--nexus-color-on-negative, #ffffff);
16
+ --focus-ring: var(--nexus-color-accent, #6f9a15);
17
+ --brand-soft: var(--nexus-color-primary-soft, rgba(143, 195, 31, 0.12));
18
+ --positive: var(--nexus-color-positive, #1f9a4e);
2
19
  color-scheme: light;
3
- font-family: "Segoe UI", Arial, sans-serif;
4
- background: #f3f5f7;
5
- color: #20252b;
20
+ font-family: "Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei",
21
+ "Segoe UI", system-ui, sans-serif;
22
+ background: var(--bg);
23
+ color: var(--fg);
24
+ font-size: 14px;
25
+ letter-spacing: 0;
6
26
  }
7
27
 
8
28
  * {
@@ -16,7 +36,8 @@
16
36
  body {
17
37
  margin: 0;
18
38
  min-height: 0;
19
- background: #f3f5f7;
39
+ background: var(--bg);
40
+ color: var(--fg);
20
41
  }
21
42
 
22
43
  .plugin-shell {
@@ -27,9 +48,9 @@ body {
27
48
 
28
49
  .plugin-header,
29
50
  .echo-panel {
30
- border: 1px solid #d4d9df;
31
- border-radius: 8px;
32
- background: #ffffff;
51
+ border: 1px solid var(--line);
52
+ border-radius: 10px;
53
+ background: var(--panel);
33
54
  }
34
55
 
35
56
  .plugin-header {
@@ -47,22 +68,24 @@ p {
47
68
  }
48
69
 
49
70
  h1 {
50
- font-size: 22px;
71
+ font-size: 18px;
72
+ font-weight: 600;
51
73
  }
52
74
 
53
75
  h2 {
54
- font-size: 15px;
76
+ font-size: 14.5px;
77
+ font-weight: 600;
55
78
  }
56
79
 
57
80
  p,
58
81
  .result-row > span {
59
82
  margin-top: 4px;
60
- color: #65717e;
83
+ color: var(--fg-dim);
61
84
  font-size: 12px;
62
85
  }
63
86
 
64
87
  .status {
65
- color: #12663f;
88
+ color: var(--positive);
66
89
  font-size: 12px;
67
90
  font-weight: 600;
68
91
  }
@@ -87,24 +110,42 @@ label {
87
110
  input {
88
111
  min-width: 0;
89
112
  height: 36px;
90
- border: 1px solid #aeb7c2;
91
- border-radius: 6px;
113
+ border: 1px solid var(--line-strong);
114
+ border-radius: 7px;
92
115
  padding: 0 10px;
93
- background: #ffffff;
94
- color: #20252b;
116
+ background: var(--panel);
117
+ color: var(--fg);
95
118
  font: inherit;
96
119
  }
97
120
 
98
121
  button {
99
122
  height: 36px;
100
- border: 1px solid #1768a6;
101
- border-radius: 6px;
123
+ border: 1px solid var(--brand);
124
+ border-radius: 7px;
102
125
  padding: 0 16px;
103
- background: #1768a6;
104
- color: #ffffff;
126
+ background: var(--brand);
127
+ color: var(--on-brand);
105
128
  font: inherit;
106
129
  font-weight: 600;
107
130
  cursor: pointer;
131
+ transition: filter 0.15s ease, background-color 0.15s ease,
132
+ border-color 0.15s ease, color 0.15s ease;
133
+ }
134
+
135
+ button:not(:disabled):hover {
136
+ border-color: var(--primary-action);
137
+ background: var(--primary-action);
138
+ color: var(--on-primary);
139
+ }
140
+
141
+ button:focus-visible,
142
+ input:focus-visible {
143
+ outline: 2px solid var(--focus-ring);
144
+ outline-offset: 2px;
145
+ }
146
+
147
+ input:hover {
148
+ border-color: var(--brand);
108
149
  }
109
150
 
110
151
  button:disabled {
@@ -117,7 +158,7 @@ button:disabled {
117
158
  grid-template-columns: 80px minmax(0, 1fr);
118
159
  gap: 10px;
119
160
  margin-top: 14px;
120
- border-top: 1px solid #e7eaee;
161
+ border-top: 1px solid var(--line);
121
162
  padding-top: 12px;
122
163
  }
123
164
 
@@ -135,37 +176,22 @@ output {
135
176
  }
136
177
 
137
178
  :root[data-theme="dark"] {
179
+ --bg: var(--nexus-color-background, #101418);
180
+ --panel: var(--nexus-color-surface, #1b222a);
181
+ --raise: var(--nexus-color-surface-raised, #171c22);
182
+ --hover: var(--nexus-color-surface-hover, #222a34);
183
+ --line: var(--nexus-color-border, #2a333e);
184
+ --line-strong: var(--nexus-color-border-strong, #38434f);
185
+ --fg: var(--nexus-color-text, #e2e8ee);
186
+ --fg-dim: var(--nexus-color-text-muted, #97a3af);
187
+ --brand-text: var(--nexus-color-accent, #a5d43e);
188
+ --primary-action: var(--nexus-color-primary, #8fc31f);
189
+ --on-primary: var(--nexus-color-on-primary, #ffffff);
190
+ --on-negative: var(--nexus-color-on-negative, #101418);
191
+ --focus-ring: var(--nexus-color-accent, #a5d43e);
192
+ --brand-soft: var(--nexus-color-primary-soft, rgba(143, 195, 31, 0.14));
193
+ --positive: var(--nexus-color-positive, #46b96e);
138
194
  color-scheme: dark;
139
- background: #16191d;
140
- color: #edf0f2;
141
- }
142
-
143
- :root[data-theme="dark"] body {
144
- background: #16191d;
145
- color: #edf0f2;
146
- }
147
-
148
- :root[data-theme="dark"] .plugin-header,
149
- :root[data-theme="dark"] .echo-panel {
150
- border-color: #454b52;
151
- background: #24282d;
152
- }
153
-
154
- :root[data-theme="dark"] p,
155
- :root[data-theme="dark"] .result-row > span {
156
- color: #aeb7c2;
157
- }
158
-
159
- :root[data-theme="dark"] input {
160
- border-color: #59616a;
161
- background: #191c20;
162
- color: #edf0f2;
163
- }
164
-
165
- :root[data-theme="dark"] .result-row {
166
- border-top-color: #454b52;
167
- }
168
-
169
- :root[data-theme="dark"] .status {
170
- color: #75d6a5;
195
+ background: var(--bg);
196
+ color: var(--fg);
171
197
  }
@@ -51,6 +51,10 @@ The table selector lists ordinary user tables from the `main` schema. Dynamic he
51
51
 
52
52
  The renderer follows `context.language` and `context.themeMode` from Nexus and preserves unfinished editor values during in-place context updates. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host. Below that toolbar, the iframe has no host padding, border, minimum size, or content-driven height and fills the remaining host viewport. This SDK template deliberately has no Quasar dependency, so its data grid uses a native `<table>` as a dependency-minimized template exception. It still provides pagination, contained horizontal scrolling, light/dark themes, `en-US` / `zh-CN`, and busy/empty/error states. Edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
53
53
 
54
+ SDK 3.2 automatically maps the active `context.theme` palette to `--nexus-color-*` variables and sets `data-nexus-theme="light|dark"` on the document root. This template consumes those variables with local light/dark fallbacks, so it still renders on an older host. Merely upgrading the SDK does not recolor custom CSS: use the namespaced variables or read the optional palette explicitly, for example `const primary = context.theme?.light.primary`.
55
+
56
+ For a plugin-owned theme, create the client with `createPluginFrameClient({ autoApplyHostTheme: false })`. The palette remains available from `await client.waitForContext()`, but the SDK will not write CSS variables or `data-nexus-theme`. It never overwrites plugin-owned `--bg`, `--accent`, `data-theme`, or `color-scheme` values.
57
+
54
58
  The current SDK has no generic business-event subscription API. For a long-running operation, register a fast `job.start` operation that returns a `taskId`, then expose `job.getStatus` and optional `job.cancel` for renderer polling. See the Nexus repository's plugin IPC and event notification example for the full contract and code.
55
59
 
56
60
  Use `npm run dev:web` for renderer-only layout, styling, localization, and responsive checks in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, and does not rebuild or load `better-sqlite3`, execute `mainEntry`, provide Node or Electron main-process APIs, copy native runtime files, or overwrite the deployable `dist` package. It builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. The page explicitly reports `PluginCapabilityUnsupported`; file selection, table loading, and CRUD require `npm run dev` or a formal Nexus host. Pass `-- --no-open` or `-- --port <port>` when needed.
@@ -1,4 +1,5 @@
1
1
  import { createPluginFrameClient } from "@aiot-nexus/plugin-sdk/browser";
2
+ import type { PluginThemeTokens } from "@aiot-nexus/plugin-sdk";
2
3
 
3
4
  type DatabaseSource = "default" | "external";
4
5
  type SqliteAffinity = "integer" | "real" | "text" | "blob" | "numeric";
@@ -906,9 +907,11 @@ function applyLanguage(nextLanguage: SupportedLanguage): void {
906
907
  client.notifyResize();
907
908
  }
908
909
 
909
- function applyContext(context: { language: string; themeMode: ThemeMode }): void {
910
+ function applyContext(context: { language: string; themeMode: ThemeMode; theme?: { light: PluginThemeTokens; dark: PluginThemeTokens } }): void {
910
911
  applyLanguage(resolveLanguage(context.language));
911
912
  applyTheme(context.themeMode);
913
+ const resolvedMode = context.themeMode === "dark" || (context.themeMode === "system" && systemDarkMode.matches) ? "dark" : "light";
914
+ document.documentElement.dataset.hostPrimary = context.theme?.[resolvedMode].primary || "";
912
915
  revealPluginContent();
913
916
  }
914
917