@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 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
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.