@eum-agent/personalization 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +13 -0
- package/package.json +51 -3
- package/ui/ui.js +613 -0
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
/** Owns this optional client feature and its browser activation. */
|
|
5
|
+
const plugin = {
|
|
6
|
+
name: "@eum-agent/personalization",
|
|
7
|
+
apply(ctx) {
|
|
8
|
+
const module = fileURLToPath(new URL("../ui/ui.js", import.meta.url));
|
|
9
|
+
if (existsSync(module)) ctx.client.registerUi({ module });
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { plugin as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eum-agent/personalization",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
"displayName": {
|
|
4
|
+
"en-US": "Personalization",
|
|
5
|
+
"zh-CN": "个性化"
|
|
6
|
+
},
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"description": "Theme, typography and interface preferences",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"ui"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"zod": "^4.4.3",
|
|
22
|
+
"i18next": "^26.4.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@eum-agent/plugin": "1.0.0",
|
|
26
|
+
"@types/react": "^19.2.18",
|
|
27
|
+
"@types/react-dom": "^19.2.5",
|
|
28
|
+
"react": "^19.2.0",
|
|
29
|
+
"react-dom": "^19.2.0",
|
|
30
|
+
"vite": "^8.2.2",
|
|
31
|
+
"@eum-agent/protocol": "1.0.0",
|
|
32
|
+
"@eum-agent/kernel": "1.0.0",
|
|
33
|
+
"@types/ws": "^8.18.1",
|
|
34
|
+
"@testing-library/react": "^16.3.3",
|
|
35
|
+
"@testing-library/user-event": "^14.6.6",
|
|
36
|
+
"zustand": "^5.0.15",
|
|
37
|
+
"react-i18next": "^17.0.13",
|
|
38
|
+
"lucide-react": "^1.40.0",
|
|
39
|
+
"@types/semver": "7.8.0"
|
|
40
|
+
},
|
|
41
|
+
"yaoguang": {
|
|
42
|
+
"kind": "extension",
|
|
43
|
+
"description": {
|
|
44
|
+
"en-US": "Theme, typography and interface preferences",
|
|
45
|
+
"zh-CN": "主题、文字排版与界面显示偏好"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {}
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsdown && vite build",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.ui.json"
|
|
53
|
+
}
|
|
6
54
|
}
|
package/ui/ui.js
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, FALLBACK_LANGUAGE, LANGUAGES, LANGUAGE_FLAGS, Picker, Segmented, SlotOutlet, ThemeColorPicker, i18next, localeLeaves, themeSwatchColor, useTranslation } from "@eum-agent/host/framework";
|
|
2
|
+
import React, { useId } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region ui-src/i18n.ts
|
|
5
|
+
/** Platform browser strings share the host's language and React translation runtime. */
|
|
6
|
+
var translations;
|
|
7
|
+
/** Binds this activation's registered namespace without reconstructing the owner's namespace. */
|
|
8
|
+
function configureTranslations(ctx) {
|
|
9
|
+
translations = ctx.i18n;
|
|
10
|
+
}
|
|
11
|
+
/** Observes the shared language while resolving platform-owned copy. */
|
|
12
|
+
function useTranslation$1() {
|
|
13
|
+
const shared = useTranslation();
|
|
14
|
+
if (!translations) throw new Error("platform browser translations are not registered");
|
|
15
|
+
return {
|
|
16
|
+
...shared,
|
|
17
|
+
t: translations.t
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
var en_US_default = {
|
|
21
|
+
settings: {
|
|
22
|
+
"personalization": "Personalization",
|
|
23
|
+
"close": "Close",
|
|
24
|
+
"colorMode": "Color scheme",
|
|
25
|
+
"systemMode": "System",
|
|
26
|
+
"lightMode": "Light",
|
|
27
|
+
"darkMode": "Dark",
|
|
28
|
+
"brand": "Theme colour",
|
|
29
|
+
"contentWidth": "Content width",
|
|
30
|
+
"contentWidthDefault": "Default",
|
|
31
|
+
"contentWidthWide": "Wide",
|
|
32
|
+
"contentWidthFull": "Full",
|
|
33
|
+
"interfaceLanguage": "Interface language"
|
|
34
|
+
},
|
|
35
|
+
brand: {
|
|
36
|
+
"violet": "Violet",
|
|
37
|
+
"alkaid": "Azure",
|
|
38
|
+
"celadon": "Teal",
|
|
39
|
+
"moss": "Green",
|
|
40
|
+
"honey": "Amber",
|
|
41
|
+
"lotus": "Rose"
|
|
42
|
+
},
|
|
43
|
+
language: {
|
|
44
|
+
"zh-CN": "简体中文",
|
|
45
|
+
"en-US": "English"
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var zh_CN_default = {
|
|
49
|
+
settings: {
|
|
50
|
+
"personalization": "个性化设置",
|
|
51
|
+
"close": "关闭",
|
|
52
|
+
"colorMode": "显示模式",
|
|
53
|
+
"systemMode": "跟随系统",
|
|
54
|
+
"lightMode": "浅色",
|
|
55
|
+
"darkMode": "深色",
|
|
56
|
+
"brand": "主题色",
|
|
57
|
+
"contentWidth": "内容宽度",
|
|
58
|
+
"contentWidthDefault": "默认",
|
|
59
|
+
"contentWidthWide": "宽屏",
|
|
60
|
+
"contentWidthFull": "全宽",
|
|
61
|
+
"interfaceLanguage": "界面语言"
|
|
62
|
+
},
|
|
63
|
+
brand: {
|
|
64
|
+
"violet": "鸢紫",
|
|
65
|
+
"alkaid": "澄蓝",
|
|
66
|
+
"celadon": "湖青",
|
|
67
|
+
"moss": "松绿",
|
|
68
|
+
"honey": "琥珀",
|
|
69
|
+
"lotus": "玫瑰"
|
|
70
|
+
},
|
|
71
|
+
language: {
|
|
72
|
+
"zh-CN": "简体中文",
|
|
73
|
+
"en-US": "English"
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region ui-src/catalogues.ts
|
|
78
|
+
/** Registers this owner's localized client copy for its activation. */
|
|
79
|
+
function registerTranslations(ctx) {
|
|
80
|
+
configureTranslations(ctx);
|
|
81
|
+
ctx.i18n.register({
|
|
82
|
+
"en-US": localeLeaves(en_US_default),
|
|
83
|
+
"zh-CN": localeLeaves(zh_CN_default)
|
|
84
|
+
}, "zh-CN");
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region ../../../node_modules/.pnpm/zustand@5.0.15_@types+react@19.2.18_react@19.2.8_use-sync-external-store@1.7.0_react@19.2.8_/node_modules/zustand/esm/vanilla.mjs
|
|
88
|
+
var createStoreImpl = (createState) => {
|
|
89
|
+
let state;
|
|
90
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
91
|
+
const setState = (partial, replace) => {
|
|
92
|
+
const nextState = typeof partial === "function" ? partial(state) : partial;
|
|
93
|
+
if (!Object.is(nextState, state)) {
|
|
94
|
+
const previousState = state;
|
|
95
|
+
state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
|
|
96
|
+
listeners.forEach((listener) => listener(state, previousState));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const getState = () => state;
|
|
100
|
+
const getInitialState = () => initialState;
|
|
101
|
+
const subscribe = (listener) => {
|
|
102
|
+
listeners.add(listener);
|
|
103
|
+
return () => listeners.delete(listener);
|
|
104
|
+
};
|
|
105
|
+
const api = {
|
|
106
|
+
setState,
|
|
107
|
+
getState,
|
|
108
|
+
getInitialState,
|
|
109
|
+
subscribe
|
|
110
|
+
};
|
|
111
|
+
const initialState = state = createState(setState, getState, api);
|
|
112
|
+
return api;
|
|
113
|
+
};
|
|
114
|
+
var createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl);
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region ../../../node_modules/.pnpm/zustand@5.0.15_@types+react@19.2.18_react@19.2.8_use-sync-external-store@1.7.0_react@19.2.8_/node_modules/zustand/esm/react.mjs
|
|
117
|
+
var identity = (arg) => arg;
|
|
118
|
+
function useStore(api, selector = identity) {
|
|
119
|
+
const slice = React.useSyncExternalStore(api.subscribe, React.useCallback(() => selector(api.getState()), [api, selector]), React.useCallback(() => selector(api.getInitialState()), [api, selector]));
|
|
120
|
+
React.useDebugValue(slice);
|
|
121
|
+
return slice;
|
|
122
|
+
}
|
|
123
|
+
var createImpl = (createState) => {
|
|
124
|
+
const api = createStore(createState);
|
|
125
|
+
const useBoundStore = (selector) => useStore(api, selector);
|
|
126
|
+
Object.assign(useBoundStore, api);
|
|
127
|
+
return useBoundStore;
|
|
128
|
+
};
|
|
129
|
+
var create = ((createState) => createState ? createImpl(createState) : createImpl);
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region ../../../node_modules/.pnpm/zustand@5.0.15_@types+react@19.2.18_react@19.2.8_use-sync-external-store@1.7.0_react@19.2.8_/node_modules/zustand/esm/middleware.mjs
|
|
132
|
+
function createJSONStorage(getStorage, options) {
|
|
133
|
+
let storage;
|
|
134
|
+
try {
|
|
135
|
+
storage = getStorage();
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
getItem: (name) => {
|
|
141
|
+
var _a;
|
|
142
|
+
const parse = (str2) => {
|
|
143
|
+
if (str2 === null) return null;
|
|
144
|
+
return JSON.parse(str2, options == null ? void 0 : options.reviver);
|
|
145
|
+
};
|
|
146
|
+
const str = (_a = storage.getItem(name)) != null ? _a : null;
|
|
147
|
+
if (str instanceof Promise) return str.then(parse);
|
|
148
|
+
return parse(str);
|
|
149
|
+
},
|
|
150
|
+
setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options == null ? void 0 : options.replacer)),
|
|
151
|
+
removeItem: (name) => storage.removeItem(name)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
var toThenable = (fn) => (input) => {
|
|
155
|
+
try {
|
|
156
|
+
const result = fn(input);
|
|
157
|
+
if (result instanceof Promise) return result;
|
|
158
|
+
return {
|
|
159
|
+
then(onFulfilled) {
|
|
160
|
+
return toThenable(onFulfilled)(result);
|
|
161
|
+
},
|
|
162
|
+
catch(_onRejected) {
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return {
|
|
168
|
+
then(_onFulfilled) {
|
|
169
|
+
return this;
|
|
170
|
+
},
|
|
171
|
+
catch(onRejected) {
|
|
172
|
+
return toThenable(onRejected)(e);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
var persistImpl = (config, baseOptions) => (set, get, api) => {
|
|
178
|
+
let options = {
|
|
179
|
+
storage: createJSONStorage(() => window.localStorage),
|
|
180
|
+
partialize: (state) => state,
|
|
181
|
+
version: 0,
|
|
182
|
+
merge: (persistedState, currentState) => ({
|
|
183
|
+
...currentState,
|
|
184
|
+
...persistedState
|
|
185
|
+
}),
|
|
186
|
+
...baseOptions
|
|
187
|
+
};
|
|
188
|
+
let hasHydrated = false;
|
|
189
|
+
let hydrationVersion = 0;
|
|
190
|
+
const hydrationListeners = /* @__PURE__ */ new Set();
|
|
191
|
+
const finishHydrationListeners = /* @__PURE__ */ new Set();
|
|
192
|
+
let storage = options.storage;
|
|
193
|
+
if (!storage) return config((...args) => {
|
|
194
|
+
console.warn(`[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`);
|
|
195
|
+
set(...args);
|
|
196
|
+
}, get, api);
|
|
197
|
+
const setItem = () => {
|
|
198
|
+
const state = options.partialize({ ...get() });
|
|
199
|
+
return storage.setItem(options.name, {
|
|
200
|
+
state,
|
|
201
|
+
version: options.version
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
const savedSetState = api.setState;
|
|
205
|
+
api.setState = (state, replace) => {
|
|
206
|
+
savedSetState(state, replace);
|
|
207
|
+
return setItem();
|
|
208
|
+
};
|
|
209
|
+
const configResult = config((...args) => {
|
|
210
|
+
set(...args);
|
|
211
|
+
return setItem();
|
|
212
|
+
}, get, api);
|
|
213
|
+
api.getInitialState = () => configResult;
|
|
214
|
+
let stateFromStorage;
|
|
215
|
+
const hydrate = () => {
|
|
216
|
+
var _a, _b;
|
|
217
|
+
if (!storage) return;
|
|
218
|
+
const currentVersion = ++hydrationVersion;
|
|
219
|
+
hasHydrated = false;
|
|
220
|
+
hydrationListeners.forEach((cb) => {
|
|
221
|
+
var _a2;
|
|
222
|
+
return cb((_a2 = get()) != null ? _a2 : configResult);
|
|
223
|
+
});
|
|
224
|
+
const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;
|
|
225
|
+
return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {
|
|
226
|
+
if (deserializedStorageValue) {
|
|
227
|
+
if (typeof deserializedStorageValue.version === "number" && deserializedStorageValue.version !== options.version) {
|
|
228
|
+
if (options.migrate) {
|
|
229
|
+
const migration = options.migrate(deserializedStorageValue.state, deserializedStorageValue.version);
|
|
230
|
+
if (migration instanceof Promise) return migration.then((result) => [true, result]);
|
|
231
|
+
return [true, migration];
|
|
232
|
+
}
|
|
233
|
+
console.error(`State loaded from storage couldn't be migrated since no migrate function was provided`);
|
|
234
|
+
} else return [false, deserializedStorageValue.state];
|
|
235
|
+
}
|
|
236
|
+
return [false, void 0];
|
|
237
|
+
}).then((migrationResult) => {
|
|
238
|
+
var _a2;
|
|
239
|
+
if (currentVersion !== hydrationVersion) return;
|
|
240
|
+
const [migrated, migratedState] = migrationResult;
|
|
241
|
+
stateFromStorage = options.merge(migratedState, (_a2 = get()) != null ? _a2 : configResult);
|
|
242
|
+
set(stateFromStorage, true);
|
|
243
|
+
if (migrated) return setItem();
|
|
244
|
+
}).then(() => {
|
|
245
|
+
if (currentVersion !== hydrationVersion) return;
|
|
246
|
+
postRehydrationCallback?.(get(), void 0);
|
|
247
|
+
stateFromStorage = get();
|
|
248
|
+
hasHydrated = true;
|
|
249
|
+
finishHydrationListeners.forEach((cb) => cb(stateFromStorage));
|
|
250
|
+
}).catch((e) => {
|
|
251
|
+
if (currentVersion !== hydrationVersion) return;
|
|
252
|
+
postRehydrationCallback?.(void 0, e);
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
api.persist = {
|
|
256
|
+
setOptions: (newOptions) => {
|
|
257
|
+
options = {
|
|
258
|
+
...options,
|
|
259
|
+
...newOptions
|
|
260
|
+
};
|
|
261
|
+
if (newOptions.storage) storage = newOptions.storage;
|
|
262
|
+
},
|
|
263
|
+
clearStorage: () => {
|
|
264
|
+
++hydrationVersion;
|
|
265
|
+
storage?.removeItem(options.name);
|
|
266
|
+
},
|
|
267
|
+
getOptions: () => options,
|
|
268
|
+
rehydrate: () => hydrate(),
|
|
269
|
+
hasHydrated: () => hasHydrated,
|
|
270
|
+
onHydrate: (cb) => {
|
|
271
|
+
hydrationListeners.add(cb);
|
|
272
|
+
return () => {
|
|
273
|
+
hydrationListeners.delete(cb);
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
onFinishHydration: (cb) => {
|
|
277
|
+
finishHydrationListeners.add(cb);
|
|
278
|
+
return () => {
|
|
279
|
+
finishHydrationListeners.delete(cb);
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
if (!options.skipHydration) hydrate();
|
|
284
|
+
return stateFromStorage || configResult;
|
|
285
|
+
};
|
|
286
|
+
var persist = persistImpl;
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region ui-src/lib/theme.ts
|
|
289
|
+
/** Applies a palette atomically, including portals and pseudo-elements, without persisting it. */
|
|
290
|
+
function applyDocumentTheme(mode, brand) {
|
|
291
|
+
const root = document.documentElement;
|
|
292
|
+
if (!(root.dataset.uiTheme !== mode || brand !== void 0 && (root.style.getPropertyValue("--brand-h") !== String(brand.hue) || root.style.getPropertyValue("--brand-c") !== String(brand.chroma)))) return;
|
|
293
|
+
root.dataset.themeChanging = "";
|
|
294
|
+
try {
|
|
295
|
+
root.dataset.uiTheme = mode;
|
|
296
|
+
root.classList.toggle("dark", mode === "dark");
|
|
297
|
+
if (brand) {
|
|
298
|
+
root.style.setProperty("--brand-h", String(brand.hue));
|
|
299
|
+
root.style.setProperty("--brand-c", String(brand.chroma));
|
|
300
|
+
}
|
|
301
|
+
root.offsetHeight;
|
|
302
|
+
} finally {
|
|
303
|
+
delete root.dataset.themeChanging;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region ui-src/store/theme.ts
|
|
308
|
+
/** Browser-local solid color scheme and brand preferences. */
|
|
309
|
+
/** 瑶光 is Alkaid, the star at the end of the Plough's handle, and a B-type
|
|
310
|
+
* star is blue-white — which is where the default hue comes from. */
|
|
311
|
+
var ALKAID = {
|
|
312
|
+
id: "alkaid",
|
|
313
|
+
labelKey: "brand.alkaid",
|
|
314
|
+
hue: 255,
|
|
315
|
+
chroma: .15
|
|
316
|
+
};
|
|
317
|
+
/** Ordered brand presets; Alkaid supplies the default presentation. */
|
|
318
|
+
var BRANDS = [
|
|
319
|
+
ALKAID,
|
|
320
|
+
{
|
|
321
|
+
id: "violet",
|
|
322
|
+
labelKey: "brand.violet",
|
|
323
|
+
hue: 295,
|
|
324
|
+
chroma: .15
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: "celadon",
|
|
328
|
+
labelKey: "brand.celadon",
|
|
329
|
+
hue: 185,
|
|
330
|
+
chroma: .095
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: "moss",
|
|
334
|
+
labelKey: "brand.moss",
|
|
335
|
+
hue: 150,
|
|
336
|
+
chroma: .12
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: "honey",
|
|
340
|
+
labelKey: "brand.honey",
|
|
341
|
+
hue: 70,
|
|
342
|
+
chroma: .12
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: "lotus",
|
|
346
|
+
labelKey: "brand.lotus",
|
|
347
|
+
hue: 355,
|
|
348
|
+
chroma: .15
|
|
349
|
+
}
|
|
350
|
+
];
|
|
351
|
+
/** Resolves persisted color values to a preset, selecting Alkaid for unmatched values. */
|
|
352
|
+
function resolveBrand(stored) {
|
|
353
|
+
return BRANDS.find((one) => one.hue === stored.hue && one.chroma === stored.chroma) ?? ALKAID;
|
|
354
|
+
}
|
|
355
|
+
var CONTENT_WIDTHS = [
|
|
356
|
+
"default",
|
|
357
|
+
"wide",
|
|
358
|
+
"full"
|
|
359
|
+
];
|
|
360
|
+
var useTheme = create()(persist((set) => ({
|
|
361
|
+
mode: "system",
|
|
362
|
+
setMode: (mode) => set({ mode }),
|
|
363
|
+
hue: ALKAID.hue,
|
|
364
|
+
chroma: ALKAID.chroma,
|
|
365
|
+
setBrand: ({ hue, chroma }) => set({
|
|
366
|
+
hue,
|
|
367
|
+
chroma
|
|
368
|
+
}),
|
|
369
|
+
contentWidth: "default",
|
|
370
|
+
setContentWidth: (contentWidth) => set({ contentWidth })
|
|
371
|
+
}), {
|
|
372
|
+
name: "yaoguang.theme",
|
|
373
|
+
partialize: ({ hue, chroma, mode, contentWidth }) => ({
|
|
374
|
+
hue,
|
|
375
|
+
chroma,
|
|
376
|
+
mode,
|
|
377
|
+
contentWidth
|
|
378
|
+
}),
|
|
379
|
+
merge: (persisted, current) => {
|
|
380
|
+
const stored = persisted;
|
|
381
|
+
const brand = resolveBrand({
|
|
382
|
+
hue: stored?.hue ?? current.hue,
|
|
383
|
+
chroma: stored?.chroma ?? current.chroma
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
...current,
|
|
387
|
+
hue: brand.hue,
|
|
388
|
+
chroma: brand.chroma,
|
|
389
|
+
mode: stored?.mode === "dark" || stored?.mode === "light" ? stored.mode : "system",
|
|
390
|
+
contentWidth: CONTENT_WIDTHS.find((width) => width === stored?.contentWidth) ?? "default"
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}));
|
|
394
|
+
/** Applies the scheme, brand and content width to the document, including portalled components. */
|
|
395
|
+
function applyTheme(state) {
|
|
396
|
+
document.documentElement.dataset.contentWidth = state.contentWidth ?? "default";
|
|
397
|
+
applyDocumentTheme(state.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light" : state.mode, state);
|
|
398
|
+
}
|
|
399
|
+
var themeActivation;
|
|
400
|
+
/**
|
|
401
|
+
* Applies the stored brand once and keeps it applied. Called before the first
|
|
402
|
+
* feature renders. Withdrawal removes its listeners and restores the host defaults
|
|
403
|
+
* while retaining stored preferences for the next activation.
|
|
404
|
+
*/
|
|
405
|
+
function initTheme() {
|
|
406
|
+
const activation = Symbol();
|
|
407
|
+
themeActivation = activation;
|
|
408
|
+
applyTheme(useTheme.getState());
|
|
409
|
+
const unsubscribe = useTheme.subscribe((state) => {
|
|
410
|
+
if (themeActivation === activation) applyTheme(state);
|
|
411
|
+
});
|
|
412
|
+
const preference = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
413
|
+
const onChange = () => {
|
|
414
|
+
const state = useTheme.getState();
|
|
415
|
+
if (themeActivation === activation && state.mode === "system") applyTheme(state);
|
|
416
|
+
};
|
|
417
|
+
preference?.addEventListener("change", onChange);
|
|
418
|
+
return () => {
|
|
419
|
+
unsubscribe();
|
|
420
|
+
preference?.removeEventListener("change", onChange);
|
|
421
|
+
if (themeActivation !== activation) return;
|
|
422
|
+
themeActivation = void 0;
|
|
423
|
+
document.documentElement.dataset.contentWidth = "default";
|
|
424
|
+
document.documentElement.style.removeProperty("--brand-h");
|
|
425
|
+
document.documentElement.style.removeProperty("--brand-c");
|
|
426
|
+
applyDocumentTheme("light");
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region ui-src/components/settings/appearance.tsx
|
|
431
|
+
/** Browser-local language, color scheme and brand preferences. */
|
|
432
|
+
function PreferenceRow({ id, label, children }) {
|
|
433
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
434
|
+
className: "flex min-w-0 flex-wrap items-center justify-between gap-x-5 gap-y-3",
|
|
435
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
436
|
+
id,
|
|
437
|
+
className: "min-w-0 text-sm font-normal",
|
|
438
|
+
children: label
|
|
439
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
440
|
+
className: "flex min-w-0 max-w-full items-center justify-end",
|
|
441
|
+
children
|
|
442
|
+
})]
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
/** Preferences apply immediately and persist in this browser without gateway writes. */
|
|
446
|
+
function AppearancePanel() {
|
|
447
|
+
const { t, i18n } = useTranslation$1();
|
|
448
|
+
const languageLabel = useId();
|
|
449
|
+
const current = i18n.resolvedLanguage ?? i18n.language;
|
|
450
|
+
const { mode, setMode, hue, setBrand, contentWidth, setContentWidth } = useTheme();
|
|
451
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
452
|
+
className: "flex min-w-0 flex-col gap-6",
|
|
453
|
+
children: [
|
|
454
|
+
/* @__PURE__ */ jsx(PreferenceRow, {
|
|
455
|
+
label: t("settings.colorMode"),
|
|
456
|
+
children: /* @__PURE__ */ jsx(Segmented, {
|
|
457
|
+
label: t("settings.colorMode"),
|
|
458
|
+
value: mode,
|
|
459
|
+
onSelect: setMode,
|
|
460
|
+
options: [
|
|
461
|
+
{
|
|
462
|
+
value: "system",
|
|
463
|
+
label: t("settings.systemMode")
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
value: "light",
|
|
467
|
+
label: t("settings.lightMode")
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
value: "dark",
|
|
471
|
+
label: t("settings.darkMode")
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
})
|
|
475
|
+
}),
|
|
476
|
+
/* @__PURE__ */ jsx(PreferenceRow, {
|
|
477
|
+
label: t("settings.brand"),
|
|
478
|
+
children: /* @__PURE__ */ jsx(ThemeColorPicker, {
|
|
479
|
+
label: t("settings.brand"),
|
|
480
|
+
value: BRANDS.find((brand) => brand.hue === hue)?.id ?? "alkaid",
|
|
481
|
+
options: BRANDS.map((brand) => ({
|
|
482
|
+
value: brand.id,
|
|
483
|
+
label: t(brand.labelKey),
|
|
484
|
+
color: themeSwatchColor(brand)
|
|
485
|
+
})),
|
|
486
|
+
onValueChange: (value) => {
|
|
487
|
+
const brand = BRANDS.find((brand) => brand.id === value);
|
|
488
|
+
if (brand) setBrand(brand);
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
}),
|
|
492
|
+
/* @__PURE__ */ jsx(PreferenceRow, {
|
|
493
|
+
label: t("settings.contentWidth"),
|
|
494
|
+
children: /* @__PURE__ */ jsx(Segmented, {
|
|
495
|
+
label: t("settings.contentWidth"),
|
|
496
|
+
value: contentWidth,
|
|
497
|
+
onSelect: setContentWidth,
|
|
498
|
+
options: [
|
|
499
|
+
{
|
|
500
|
+
value: "default",
|
|
501
|
+
label: t("settings.contentWidthDefault")
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
value: "wide",
|
|
505
|
+
label: t("settings.contentWidthWide")
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
value: "full",
|
|
509
|
+
label: t("settings.contentWidthFull")
|
|
510
|
+
}
|
|
511
|
+
]
|
|
512
|
+
})
|
|
513
|
+
}),
|
|
514
|
+
/* @__PURE__ */ jsx(SlotOutlet, {
|
|
515
|
+
slot: "shell.personalization.section",
|
|
516
|
+
props: {}
|
|
517
|
+
}),
|
|
518
|
+
/* @__PURE__ */ jsx(PreferenceRow, {
|
|
519
|
+
id: languageLabel,
|
|
520
|
+
label: t("settings.interfaceLanguage"),
|
|
521
|
+
children: /* @__PURE__ */ jsx(Picker, {
|
|
522
|
+
labelledBy: languageLabel,
|
|
523
|
+
slot: "language-picker",
|
|
524
|
+
value: current,
|
|
525
|
+
options: LANGUAGES.map((language) => ({
|
|
526
|
+
value: language,
|
|
527
|
+
label: t(language === "en-US" ? "language.en-US" : "language.zh-CN"),
|
|
528
|
+
prefix: LANGUAGE_FLAGS[language]
|
|
529
|
+
})),
|
|
530
|
+
onSelect: (language) => {
|
|
531
|
+
i18n.changeLanguage(language);
|
|
532
|
+
}
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
]
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
//#endregion
|
|
539
|
+
//#region ui-src/components/personalization-dialog.tsx
|
|
540
|
+
/** Compact browser-local preferences, independent of gateway settings and their drafts. */
|
|
541
|
+
function PersonalizationDialog({ open, onOpenChange: setOpen }) {
|
|
542
|
+
const { t } = useTranslation$1();
|
|
543
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
544
|
+
open,
|
|
545
|
+
onOpenChange: setOpen,
|
|
546
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
547
|
+
closeLabel: t("settings.close"),
|
|
548
|
+
className: "personalization-dialog max-h-[calc(100dvh-4rem)] max-w-lg gap-5 overflow-y-auto p-6 sm:max-w-lg",
|
|
549
|
+
children: [/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: t("settings.personalization") }) }), /* @__PURE__ */ jsx(AppearancePanel, {})]
|
|
550
|
+
})
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region ui-src/lib/language.ts
|
|
555
|
+
/** Browser-local language preference and document metadata owned by the platform activation. */
|
|
556
|
+
/** Browser preference key shared with the platform language editor. */
|
|
557
|
+
var STORAGE_KEY = "yaoguang.lang";
|
|
558
|
+
var languageActivation;
|
|
559
|
+
/** Restores the same saved/navigator language choice and retains changes locally. */
|
|
560
|
+
function initLanguage() {
|
|
561
|
+
const activation = Symbol();
|
|
562
|
+
languageActivation = activation;
|
|
563
|
+
let saved;
|
|
564
|
+
try {
|
|
565
|
+
saved = localStorage.getItem("yaoguang.lang") ?? void 0;
|
|
566
|
+
} catch {}
|
|
567
|
+
const selected = (saved ? [saved] : [...navigator.languages]).map((value) => LANGUAGES.find((language) => language.toLowerCase() === value.toLowerCase()) ?? LANGUAGES.find((language) => language.split("-")[0] === value.split("-")[0])).find(Boolean);
|
|
568
|
+
if (selected) i18next.changeLanguage(selected);
|
|
569
|
+
const changed = () => {
|
|
570
|
+
if (languageActivation !== activation) return;
|
|
571
|
+
const language = i18next.resolvedLanguage ?? "zh-CN";
|
|
572
|
+
document.documentElement.lang = language;
|
|
573
|
+
try {
|
|
574
|
+
localStorage.setItem(STORAGE_KEY, language);
|
|
575
|
+
} catch {}
|
|
576
|
+
};
|
|
577
|
+
changed();
|
|
578
|
+
i18next.on("languageChanged", changed);
|
|
579
|
+
return () => {
|
|
580
|
+
i18next.off("languageChanged", changed);
|
|
581
|
+
if (languageActivation !== activation) return;
|
|
582
|
+
languageActivation = void 0;
|
|
583
|
+
document.documentElement.lang = FALLBACK_LANGUAGE;
|
|
584
|
+
i18next.changeLanguage(FALLBACK_LANGUAGE);
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region ui-src/index.tsx
|
|
589
|
+
/** Browser module version understood by the host. */
|
|
590
|
+
var runtimeVersion = 5;
|
|
591
|
+
/** Personalization contributes presentation without exposing capability-specific state. */
|
|
592
|
+
var provides = [];
|
|
593
|
+
/** Applies preferences and owns their settings, listeners and document presentation. */
|
|
594
|
+
function register(ctx) {
|
|
595
|
+
registerTranslations(ctx);
|
|
596
|
+
const stops = [initLanguage(), initTheme()];
|
|
597
|
+
ctx.slots.register("shell.dialog.surface", {
|
|
598
|
+
id: "personalization",
|
|
599
|
+
component: PersonalizationDialog
|
|
600
|
+
});
|
|
601
|
+
ctx.slots.register("shell.nav.action", {
|
|
602
|
+
id: "personalization-action",
|
|
603
|
+
icon: "palette",
|
|
604
|
+
label: () => ctx.i18n.t("settings.personalization"),
|
|
605
|
+
order: 20,
|
|
606
|
+
onSelect: () => ctx.navigation.openDialog("personalization")
|
|
607
|
+
});
|
|
608
|
+
ctx.signal.addEventListener("abort", () => {
|
|
609
|
+
for (const stop of stops.toReversed()) stop();
|
|
610
|
+
}, { once: true });
|
|
611
|
+
}
|
|
612
|
+
//#endregion
|
|
613
|
+
export { provides, register, runtimeVersion };
|
package/README.md
DELETED