dsh-any-background 0.2.7 → 0.2.9

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/lib/client.js CHANGED
@@ -24,10 +24,12 @@ window.__ModuleLoader__.load({
24
24
  close: "关闭",
25
25
  pageColor: "色彩",
26
26
  pageInterface: "界面",
27
+ pageFont: "字体",
27
28
  pageBackground: "背景",
28
29
  pageProfile: "配置",
29
30
  descColor: "拖动色轮或输入精确数值,整个界面的配色将实时跟随变化",
30
31
  descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感",
32
+ descFont: "上传自定义字体为整套界面换字形,并按区域为文字添加描边,让内容在复杂的壁纸上依然清晰",
31
33
  descBackground: "上传一张图片或一段视频作为壁纸并自由摆放,或让算法为你实时生成动态背景",
32
34
  descProfile: "一键套用外观预设,保存多套配置随时切换,设置昼夜自动换装,或导出/导入完整主题",
33
35
  colorTitle: "主题色",
@@ -46,6 +48,25 @@ window.__ModuleLoader__.load({
46
48
  uiTrajectory: "轨迹页",
47
49
  uiPanelRegion: "better-sidebar 工作台",
48
50
  uiProduced: "产出物/高亮内容",
51
+ fontPageTitle: "字体与描边",
52
+ fontTitle: "自定义字体",
53
+ fontHint: "支持 ttf/otf/woff/woff2 字体文件(上限 100MB),将应用到整个界面的文字;代码块的等宽字体保持不变。中文字体通常有几十 MB,首次上传与刷新加载需要片刻",
54
+ fontUpload: "上传字体",
55
+ fontUploading: "上传中…",
56
+ fontRemove: "移除字体",
57
+ fontEnabled: "启用字体",
58
+ fontNone: "未设置,使用宿主默认字体",
59
+ fontFail: "字体上传失败,请确认是有效的 ttf/otf/woff/woff2 文件",
60
+ strokeTitle: "文字描边",
61
+ strokeHint: "按区域为文字添加描边:粗细为 0 表示关闭,「自动」会生成与文字颜色相反的颜色,「主题色」跟随当前主题色。代码块与图标已自动豁免,多色语法不会糊成一团",
62
+ strokeWidth: "描边粗细",
63
+ strokeColor: "描边颜色",
64
+ strokeAuto: "自动反色",
65
+ strokeGray: "灰色",
66
+ strokeBlack: "黑色",
67
+ strokeWhite: "白色",
68
+ strokeTheme: "主题色",
69
+ strokeCustom: "自定义",
49
70
  bgTitle: "背景",
50
71
  bgChoose: "选择图片或视频",
51
72
  bgRemove: "移除背景",
@@ -73,7 +94,7 @@ window.__ModuleLoader__.load({
73
94
  liveBadge: "动态壁纸",
74
95
  bgHint: "「适应」布局模式下悬停预览可编辑图片/视频的位置与缩放(其余布局模式按模式自动排布);动态生成模式下可选择类型、预设与参数。绑定种子后刷新页面壁纸保持不变,取消绑定则每次刷新随机换新",
75
96
  editorTitle: "背景编辑器",
76
- editorHint: "拖动移动画面,滚轮缩放大小",
97
+ editorHint: "拖动移动画面,滚轮或双指缩放大小",
77
98
  editorCommit: "确认",
78
99
  editorCancel: "取消",
79
100
  editorReset: "重置",
@@ -189,10 +210,12 @@ window.__ModuleLoader__.load({
189
210
  close: "Close",
190
211
  pageColor: "Color",
191
212
  pageInterface: "Interface",
213
+ pageFont: "Font",
192
214
  pageBackground: "Background",
193
215
  pageProfile: "Profile",
194
216
  descColor: "Drag the wheel or type exact values — the whole UI follows live",
195
217
  descInterface: "Tune opacity and blur per surface to build depth",
218
+ descFont: "Swap the interface typeface with your own font file, and outline text per region so it stays legible over busy wallpapers",
196
219
  descBackground: "Upload an image or a video as wallpaper and place it freely, or let the algorithm paint a live background",
197
220
  descProfile: "Apply a one-click preset, save multiple named profiles, schedule a day/night switch, or export/import the full theme",
198
221
  colorTitle: "Theme color",
@@ -211,6 +234,25 @@ window.__ModuleLoader__.load({
211
234
  uiTrajectory: "Trajectory view",
212
235
  uiPanelRegion: "better-sidebar workbench",
213
236
  uiProduced: "Produced / highlights",
237
+ fontPageTitle: "Font & outline",
238
+ fontTitle: "Custom font",
239
+ fontHint: "Accepts ttf/otf/woff/woff2 files (up to 100MB) and applies them to the whole interface; code blocks keep their monospace stack. CJK fonts routinely span tens of MB, so the first upload and each reload take a moment",
240
+ fontUpload: "Upload font",
241
+ fontUploading: "Uploading…",
242
+ fontRemove: "Remove font",
243
+ fontEnabled: "Font enabled",
244
+ fontNone: "None — host default",
245
+ fontFail: "Upload failed — make sure it is a valid ttf/otf/woff/woff2 file",
246
+ strokeTitle: "Text outline",
247
+ strokeHint: "Outline text per region: width 0 turns it off, \"auto\" picks the inverse of the text color, and \"theme\" follows the current accent. Code blocks and icons are exempted automatically, so multi-color syntax never smears",
248
+ strokeWidth: "Outline width",
249
+ strokeColor: "Outline color",
250
+ strokeAuto: "Auto contrast",
251
+ strokeGray: "Gray",
252
+ strokeBlack: "Black",
253
+ strokeWhite: "White",
254
+ strokeTheme: "Accent",
255
+ strokeCustom: "Custom",
214
256
  bgTitle: "Background",
215
257
  bgChoose: "Choose image or video",
216
258
  bgRemove: "Remove background",
@@ -238,7 +280,7 @@ window.__ModuleLoader__.load({
238
280
  liveBadge: "Live wallpaper",
239
281
  bgHint: "In Fit layout mode hover the preview to reposition and zoom the image/video (other modes arrange it automatically); in generated mode pick a type, preset and parameters. With the seed locked the wallpaper stays the same across refreshes; unlocked, every refresh randomizes it",
240
282
  editorTitle: "Background editor",
241
- editorHint: "Drag to move, scroll to zoom",
283
+ editorHint: "Drag to move; scroll or pinch with two fingers to zoom",
242
284
  editorCommit: "Confirm",
243
285
  editorCancel: "Cancel",
244
286
  editorReset: "Reset",
@@ -353,25 +395,72 @@ window.__ModuleLoader__.load({
353
395
  const DEFAULT_CONFIG = {
354
396
  color: null,
355
397
  opacities: {
356
- bg: .85,
357
- sidebar: .93,
358
- card: 1,
359
- input: 1
398
+ bg: .5,
399
+ sidebar: .5,
400
+ card: .5,
401
+ input: .5
360
402
  },
361
403
  blurs: {
362
- bg: 0,
363
- sidebar: 0,
364
- card: 0,
365
- settings: 0,
366
- chat: 0,
367
- trajectory: 0,
368
- input: 0,
369
- panel: 0,
370
- produced: 0
404
+ bg: 30,
405
+ sidebar: 30,
406
+ card: 30,
407
+ settings: 30,
408
+ chat: 30,
409
+ trajectory: 30,
410
+ input: 30,
411
+ panel: 30,
412
+ produced: 30
371
413
  },
372
- settingsOpacity: 1,
414
+ strokes: {
415
+ bg: {
416
+ width: 0,
417
+ color: "auto",
418
+ customColor: "#808080"
419
+ },
420
+ sidebar: {
421
+ width: 0,
422
+ color: "auto",
423
+ customColor: "#808080"
424
+ },
425
+ card: {
426
+ width: 0,
427
+ color: "auto",
428
+ customColor: "#808080"
429
+ },
430
+ settings: {
431
+ width: 0,
432
+ color: "auto",
433
+ customColor: "#808080"
434
+ },
435
+ chat: {
436
+ width: 0,
437
+ color: "auto",
438
+ customColor: "#808080"
439
+ },
440
+ trajectory: {
441
+ width: 0,
442
+ color: "auto",
443
+ customColor: "#808080"
444
+ },
445
+ input: {
446
+ width: 0,
447
+ color: "auto",
448
+ customColor: "#808080"
449
+ },
450
+ panel: {
451
+ width: 0,
452
+ color: "auto",
453
+ customColor: "#808080"
454
+ },
455
+ produced: {
456
+ width: 0,
457
+ color: "auto",
458
+ customColor: "#808080"
459
+ }
460
+ },
461
+ settingsOpacity: .5,
373
462
  wallpaperOpacity: 1,
374
- blur: 0,
463
+ blur: 30,
375
464
  bgState: {
376
465
  zoom: 1,
377
466
  x: 0,
@@ -389,12 +478,14 @@ window.__ModuleLoader__.load({
389
478
  backgroundType: "image",
390
479
  bgMode: "fit",
391
480
  videoMime: null,
481
+ fontMime: null,
482
+ fontEnabled: true,
392
483
  generatedBg: null,
393
484
  regenerateOnReload: false,
394
- chatTextOpacity: 0,
395
- trajectoryOpacity: 1,
396
- panelOpacity: 1,
397
- producedOpacity: 1,
485
+ chatTextOpacity: .5,
486
+ trajectoryOpacity: .5,
487
+ panelOpacity: .5,
488
+ producedOpacity: .5,
398
489
  profiles: [],
399
490
  rotation: {
400
491
  enabled: false,
@@ -420,6 +511,7 @@ window.__ModuleLoader__.load({
420
511
  ...DEFAULT_CONFIG,
421
512
  opacities: { ...DEFAULT_CONFIG.opacities },
422
513
  blurs: { ...DEFAULT_CONFIG.blurs },
514
+ strokes: structuredClone(DEFAULT_CONFIG.strokes),
423
515
  bgState: { ...DEFAULT_CONFIG.bgState },
424
516
  videoBgState: { ...DEFAULT_CONFIG.videoBgState }
425
517
  };
@@ -540,6 +632,49 @@ window.__ModuleLoader__.load({
540
632
  }
541
633
  return out;
542
634
  }
635
+ const STROKE_GROUPS = [
636
+ "bg",
637
+ "sidebar",
638
+ "card",
639
+ "settings",
640
+ "chat",
641
+ "trajectory",
642
+ "input",
643
+ "panel",
644
+ "produced"
645
+ ];
646
+ const STROKE_COLOR_KEYS = [
647
+ "auto",
648
+ "gray",
649
+ "black",
650
+ "white",
651
+ "theme",
652
+ "custom"
653
+ ];
654
+ const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
655
+ const DEFAULT_STROKE = {
656
+ width: 0,
657
+ color: "auto",
658
+ customColor: "#808080"
659
+ };
660
+ function adoptStroke(raw) {
661
+ const s = raw ?? {};
662
+ return {
663
+ width: typeof s.width === "number" && isFinite(s.width) ? Math.min(4, Math.max(0, s.width)) : DEFAULT_STROKE.width,
664
+ color: STROKE_COLOR_KEYS.includes(s.color) ? s.color : DEFAULT_STROKE.color,
665
+ customColor: typeof s.customColor === "string" && HEX_RE.test(s.customColor) ? s.customColor : DEFAULT_STROKE.customColor
666
+ };
667
+ }
668
+ /** Move a possibly-partial strokes map into the full shape the UI reads. */
669
+ function strokesFrom(raw) {
670
+ const s = raw ?? {};
671
+ const out = {};
672
+ for (const k of STROKE_GROUPS) out[k] = adoptStroke(s[k]);
673
+ return out;
674
+ }
675
+ function rStrokes() {
676
+ return strokesFrom(cfg.strokes);
677
+ }
543
678
  function rWop() {
544
679
  return clamp01(cfg.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity);
545
680
  }
@@ -611,6 +746,7 @@ window.__ModuleLoader__.load({
611
746
  color: cfg.color,
612
747
  opacities: { ...rOps() },
613
748
  blurs: { ...rBlurs() },
749
+ strokes: rStrokes(),
614
750
  settingsOpacity: rSop(),
615
751
  wallpaperOpacity: rWop(),
616
752
  blur: rBl(),
@@ -631,6 +767,7 @@ window.__ModuleLoader__.load({
631
767
  ...DEFAULT_CONFIG.blurs,
632
768
  ...ap.blurs ?? {}
633
769
  };
770
+ cfg.strokes = strokesFrom(ap.strokes);
634
771
  cfg.settingsOpacity = clamp01(ap.settingsOpacity, DEFAULT_CONFIG.settingsOpacity);
635
772
  cfg.wallpaperOpacity = clamp01(ap.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity);
636
773
  cfg.blur = typeof ap.blur === "number" ? Math.min(60, Math.max(0, ap.blur)) : DEFAULT_CONFIG.blur;
@@ -664,6 +801,7 @@ window.__ModuleLoader__.load({
664
801
  ...DEFAULT_CONFIG.blurs,
665
802
  ...ac.blurs ?? {}
666
803
  },
804
+ strokes: strokesFrom(ac.strokes),
667
805
  settingsOpacity: clamp01(ac.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
668
806
  wallpaperOpacity: clamp01(ac.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity),
669
807
  blur: num(ac.blur, DEFAULT_CONFIG.blur),
@@ -761,6 +899,7 @@ window.__ModuleLoader__.load({
761
899
  input: num(ops.input, DEFAULT_CONFIG.opacities.input)
762
900
  },
763
901
  blurs,
902
+ strokes: strokesFrom(c.strokes),
764
903
  settingsOpacity: num(c.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
765
904
  wallpaperOpacity: num(c.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity),
766
905
  blur: num(c.blur, DEFAULT_CONFIG.blur),
@@ -769,6 +908,8 @@ window.__ModuleLoader__.load({
769
908
  backgroundType: bgType,
770
909
  bgMode,
771
910
  videoMime: typeof c.videoMime === "string" ? c.videoMime : null,
911
+ fontMime: typeof c.fontMime === "string" ? c.fontMime : null,
912
+ fontEnabled: typeof c.fontEnabled === "boolean" ? c.fontEnabled : DEFAULT_CONFIG.fontEnabled,
772
913
  generatedBg: generatedBg ? normalizeGeneratedBg(generatedBg) : null,
773
914
  regenerateOnReload: typeof c.regenerateOnReload === "boolean" ? c.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
774
915
  chatTextOpacity: clamp01(c.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
@@ -828,9 +969,15 @@ window.__ModuleLoader__.load({
828
969
  const WALLPAPER_UPLOAD_URL = "/dsh-any-background/wallpaper/upload";
829
970
  /** HTTP route new videos are POSTed to as raw bytes (see uploadVideo). */
830
971
  const VIDEO_UPLOAD_URL = "/dsh-any-background/video/upload";
972
+ /** Same-origin serve URL of the persisted custom font (enough for @font-face). */
973
+ const FONT_SERVE_URL = "/dsh-any-background/font";
974
+ /** HTTP route custom fonts are POSTed to as raw bytes (see uploadFont). */
975
+ const FONT_UPLOAD_URL = "/dsh-any-background/font/upload";
831
976
  const RPC_NS = "dshAnyBackground";
832
977
  const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
833
978
  let rpcCallFn = null;
979
+ /** Serve URL of the persisted custom font, filled by loadPersisted. */
980
+ let fontServeUrl = null;
834
981
  function initRpc(call) {
835
982
  rpcCallFn = call;
836
983
  }
@@ -868,7 +1015,10 @@ window.__ModuleLoader__.load({
868
1015
  /** Load the persisted theme (config + wallpaper URL + video URL) from the node
869
1016
  * half. Image and video both travel as serve URLs — never bytes — so this RPC
870
1017
  * stays tiny. Resolves true when the server advanced a due wallpaper rotation
871
- * during the read — the restored wallpaper is then already the new pick. */
1018
+ * during the read — the restored wallpaper is then already the new pick.
1019
+ * `firstRun` means the server had no theme-config.json and just materialized
1020
+ * its defaults: the caller should persist the browser side's own defaults and
1021
+ * re-read once so every slider starts from a value that is really on disk. */
872
1022
  async function loadPersisted() {
873
1023
  const data = await rpcCall("read", {});
874
1024
  if (data && typeof data === "object") {
@@ -878,10 +1028,18 @@ window.__ModuleLoader__.load({
878
1028
  else if (d.wallpaperUrl === null) setWpImageUrl(null);
879
1029
  if (typeof d.videoUrl === "string") setWpVideoUrl(d.videoUrl, cfg.videoMime);
880
1030
  else if (d.videoUrl === null) setWpVideoUrl(null, null);
1031
+ if (typeof d.fontUrl === "string") fontServeUrl = d.fontUrl;
1032
+ else if (d.fontUrl === null) fontServeUrl = null;
881
1033
  if (cfg.backgroundType === "image") setWpUrl(rWpImage());
882
- return d.rotated === true;
1034
+ return {
1035
+ rotated: d.rotated === true,
1036
+ firstRun: d.firstRun === true
1037
+ };
883
1038
  }
884
- return false;
1039
+ return {
1040
+ rotated: false,
1041
+ firstRun: false
1042
+ };
885
1043
  }
886
1044
  /** Persist a wallpaper (null removes it); one-shot, no debounce. */
887
1045
  function persistWallpaper(dataUrl) {
@@ -987,6 +1145,28 @@ window.__ModuleLoader__.load({
987
1145
  return false;
988
1146
  }
989
1147
  }
1148
+ /** Upload a custom font's raw bytes over HTTP; resolves the sniffed MIME on
1149
+ * success (the server validates the container from magic bytes) or null. */
1150
+ async function uploadFont(blob) {
1151
+ try {
1152
+ const res = await fetch(FONT_UPLOAD_URL, {
1153
+ method: "POST",
1154
+ headers: { "Content-Type": blob.type || "application/octet-stream" },
1155
+ body: blob
1156
+ });
1157
+ if (!res.ok) return null;
1158
+ const data = await res.json().catch(() => null);
1159
+ return typeof data?.mime === "string" ? data.mime : "font/ttf";
1160
+ } catch (e) {
1161
+ console.warn("dsh-any-background: font upload failed", e);
1162
+ return null;
1163
+ }
1164
+ }
1165
+ /** Remove the stored custom font (server deletes every variant + clears the
1166
+ * recorded MIME); resolves true once the slot is empty. */
1167
+ async function removeFont() {
1168
+ return await rpcCall("removeFont", {}) === true;
1169
+ }
990
1170
  /** Add an image (data URL + small thumbnail) to the server-side rotation pool. */
991
1171
  async function rotationAdd(dataUrl, thumb) {
992
1172
  if (!rpcCallFn) return {
@@ -2400,14 +2580,34 @@ window.__ModuleLoader__.load({
2400
2580
  if (rSchemeOverride() !== "auto") return genTokens(220, .04, scheme === "dark" ? .14 : .92, scheme).tokens;
2401
2581
  return null;
2402
2582
  }
2583
+ /** Surface colors the opacity sliders fade when the plugin has NO palette of
2584
+ * its own (no picked color, no forced scheme), read live from the host's
2585
+ * resolved tokens on `:root` — the same trick applyPanelOverrides uses for the
2586
+ * workbench panel. Without this source the per-part alpha is never emitted at
2587
+ * all and every opacity slider looks inert until the user drags it once.
2588
+ * The sliders only ever supply the alpha: the host still decides the colors,
2589
+ * so a custom host skin survives. Returns null when the host exposes none. */
2590
+ function readHostOpacityTokens() {
2591
+ if (typeof getComputedStyle === "undefined") return null;
2592
+ const cs = getComputedStyle(document.documentElement);
2593
+ const out = {};
2594
+ let found = false;
2595
+ for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) {
2596
+ const v = cs.getPropertyValue(name).trim();
2597
+ if (v === "") continue;
2598
+ out[name] = v;
2599
+ found = true;
2600
+ }
2601
+ return found ? out : null;
2602
+ }
2403
2603
  function applyCustomTokensNow(ops) {
2404
2604
  const hasColor = rHasColor();
2405
2605
  const override = rSchemeOverride();
2406
2606
  const [h, s, l] = rColor();
2407
2607
  const scheme = rScheme();
2408
2608
  const verdict = rBgDark();
2409
- const palette = paletteTokens();
2410
- const tokens = { ...palette };
2609
+ const surfaces = paletteTokens() ?? readHostOpacityTokens();
2610
+ const tokens = { ...surfaces };
2411
2611
  if (override === "auto") {
2412
2612
  const fontDark = hasColor ? rColorScheme() === "dark" : verdict;
2413
2613
  if (fontDark !== null && fontDark !== void 0) {
@@ -2431,10 +2631,17 @@ window.__ModuleLoader__.load({
2431
2631
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
2432
2632
  appliedTokenNames = Object.keys(tokens);
2433
2633
  }
2434
- if (palette === null) return;
2634
+ if (surfaces === null) {
2635
+ if (!hasColor && override === "auto" && verdict === null) clearThemeTokens();
2636
+ return;
2637
+ }
2435
2638
  const root = document.documentElement;
2436
- for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) root.style.setProperty(OPACITY_VARS[name], toRgba(tokens[name] ?? "#000", ops[g.part]));
2437
- root.style.setProperty("--dsh-any-op-menu-cordis", toRgba(tokens["--dsw-specific-menu"] ?? "#000", ops.input));
2639
+ for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) {
2640
+ if (surfaces[name] === void 0) continue;
2641
+ root.style.setProperty(OPACITY_VARS[name], toRgba(surfaces[name], ops[g.part]));
2642
+ }
2643
+ const menu = surfaces["--dsw-specific-menu"];
2644
+ if (menu !== void 0) root.style.setProperty("--dsh-any-op-menu-cordis", toRgba(menu, ops.input));
2438
2645
  const bgKey = `${baseTokenKey}|${ops.bg}`;
2439
2646
  if (bgKey !== lastBgKey) {
2440
2647
  lastBgKey = bgKey;
@@ -2461,6 +2668,151 @@ window.__ModuleLoader__.load({
2461
2668
  const PANEL_TOKEN_RULE = "[data-dsh-bottom-panel],[data-sidebar-right-panel]{--dsw-alias-bg-base:var(--dsh-any-panel-bg-base);--dsw-alias-bg-layer-1:var(--dsh-any-panel-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-panel-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-panel-layer-3);position:fixed!important;z-index:26!important}[data-sidebar-right-panel][data-sidebar-right-panel=\"fullscreen\"]{z-index:40!important}";
2462
2669
  const PANEL_BLUR_RULE = "[data-dsh-bottom-panel],[data-sidebar-right-panel]{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}";
2463
2670
  const PRODUCED_RULE = "[data-code-block-content] pre,[data-code-block-banner],[data-composer-chip],:not(pre)>code{-webkit-backdrop-filter:var(--dsh-any-blur-prod,none);backdrop-filter:var(--dsh-any-blur-prod,none)}@supports (background:color-mix(in srgb,red 50%,transparent)){.md-code-block{background:transparent!important}.md-code-block>div{background-color:transparent!important}.md-code-block [data-code-block-content] pre{background-color:color-mix(in srgb,var(--dsl-code-block-background,var(--dsw-alias-markdown-code-block,transparent)) var(--dsh-any-prod-pct,100%),transparent)!important}.md-code-block [data-code-block-banner]{background-color:color-mix(in srgb,var(--dsl-code-block-banner-background-color,var(--dsw-alias-markdown-code-block-banner,transparent)) var(--dsh-any-prod-pct,100%),transparent)!important}[data-code-block-content]{--shiki-background:color-mix(in srgb,var(--dsw-alias-markdown-code-block,var(--dsl-code-block-background,transparent)) var(--dsh-any-prod-pct,100%),transparent)}:not(pre)>code{background-color:color-mix(in srgb,var(--dsw-alias-markdown-inline-code,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-composer-chip]>*{background-color:color-mix(in srgb,var(--dsw-alias-interactive-bg-hover,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}}";
2671
+ const STROKE_RULE = [
2672
+ `${SETTINGS_PANEL_SEL}{-webkit-text-stroke:var(--dsh-any-stroke-settings-w,0px) var(--dsh-any-stroke-settings-c,transparent);paint-order:stroke fill}`,
2673
+ "[role=\"menu\"]:not([data-dockkit-tab-menu]),[role=\"listbox\"],body>[role=\"tree\"],body>[role=\"dialog\"]:not([aria-modal=\"true\"]){-webkit-text-stroke:var(--dsh-any-stroke-card-w,0px) var(--dsh-any-stroke-card-c,transparent);paint-order:stroke fill}",
2674
+ "[data-composer-card],[data-cordis-panel],[data-composer-card] textarea,[data-composer-card] input,[data-composer-card] [contenteditable],[data-cordis-panel] input,[data-cordis-panel] textarea{-webkit-text-stroke:var(--dsh-any-stroke-input-w,0px) var(--dsh-any-stroke-input-c,transparent);paint-order:stroke fill}",
2675
+ `[data-chat-flow]{-webkit-text-stroke:var(--dsh-any-stroke-chat-w,0px) var(--dsh-any-stroke-chat-c,transparent);paint-order:stroke fill}`,
2676
+ `[data-conversation-composer-overlay]{-webkit-text-stroke:var(--dsh-any-stroke-trajectory-w,0px) var(--dsh-any-stroke-trajectory-c,transparent);paint-order:stroke fill}`,
2677
+ "[data-code-block-content] pre,[data-code-block-content],[data-code-block-banner],[data-composer-chip],:not(pre)>code{-webkit-text-stroke:var(--dsh-any-stroke-produced-w,0px) var(--dsh-any-stroke-produced-c,transparent);paint-order:stroke fill}",
2678
+ `[data-dsh-bottom-panel],[data-sidebar-right-panel]{-webkit-text-stroke:var(--dsh-any-stroke-panel-w,0px) var(--dsh-any-stroke-panel-c,transparent);paint-order:stroke fill}`,
2679
+ `svg{-webkit-text-stroke-width:0!important;paint-order:normal!important}`,
2680
+ `::placeholder{-webkit-text-stroke-width:0!important}`
2681
+ ].join("");
2682
+ /** Groups whose stroke lands on the structurally-discovered columns (inline),
2683
+ * versus every group served by the static STROKE_RULE selectors. */
2684
+ const STROKE_INLINE_GROUPS = ["bg", "sidebar"];
2685
+ const STROKE_VAR_GROUPS = [
2686
+ "card",
2687
+ "settings",
2688
+ "chat",
2689
+ "trajectory",
2690
+ "input",
2691
+ "panel",
2692
+ "produced"
2693
+ ];
2694
+ /** Resolve one group's stroke color key into a concrete CSS color.
2695
+ * 'auto' contrasts the FONT direction (white fonts → black stroke and vice
2696
+ * versa — mirrors the label flip in applyCustomTokensNow); 'theme' follows
2697
+ * the live palette's brand primary (picked color → generated palette, else
2698
+ * the host's own resolved token). */
2699
+ function strokeColor(s) {
2700
+ switch (s.color) {
2701
+ case "gray": return "#808080";
2702
+ case "black": return "#000";
2703
+ case "white": return "#fff";
2704
+ case "custom": return s.customColor;
2705
+ case "theme":
2706
+ if (rHasColor() || rSchemeOverride() !== "auto") {
2707
+ const [h, sa, l] = rColor();
2708
+ return genTokens(h, sa, l, rColorScheme()).tokens["--dsw-alias-brand-primary"] ?? "#808080";
2709
+ }
2710
+ if (typeof getComputedStyle !== "undefined") {
2711
+ const v = getComputedStyle(document.documentElement).getPropertyValue("--dsw-alias-brand-primary").trim();
2712
+ if (v !== "") return v;
2713
+ }
2714
+ return "#808080";
2715
+ default: {
2716
+ let fontDark;
2717
+ if (rHasColor()) fontDark = rColorScheme() === "dark";
2718
+ else if (rSchemeOverride() !== "auto") fontDark = rScheme() === "dark";
2719
+ else if (rBgDark() !== null) fontDark = rBgDark() === true;
2720
+ else fontDark = false;
2721
+ return fontDark ? "#000" : "#fff";
2722
+ }
2723
+ }
2724
+ }
2725
+ /** Write every group's stroke width/color variables + the two column strokes.
2726
+ * Called from applyWp so palette/verdict changes re-derive 'auto'/'theme'. */
2727
+ function applyStrokes() {
2728
+ const strokes = rStrokes();
2729
+ const root = document.documentElement;
2730
+ for (const g of STROKE_VAR_GROUPS) {
2731
+ const s = strokes[g];
2732
+ root.style.setProperty(`--dsh-any-stroke-${g}-w`, `${s.width}px`);
2733
+ root.style.setProperty(`--dsh-any-stroke-${g}-c`, strokeColor(s));
2734
+ }
2735
+ discoverParts();
2736
+ for (const g of STROKE_INLINE_GROUPS) {
2737
+ const el = g === "bg" ? centerEl : sidebarEl;
2738
+ if (el === null) continue;
2739
+ const s = strokes[g];
2740
+ if (s.width > 0) {
2741
+ el.style.setProperty("-webkit-text-stroke", `${s.width}px ${strokeColor(s)}`);
2742
+ el.style.setProperty("paint-order", "stroke fill");
2743
+ } else {
2744
+ el.style.removeProperty("-webkit-text-stroke");
2745
+ el.style.removeProperty("paint-order");
2746
+ }
2747
+ }
2748
+ }
2749
+ /** Live per-group stroke update during slider drag (no full re-apply). */
2750
+ function setPartStroke(part, s) {
2751
+ if (STROKE_VAR_GROUPS.includes(part)) {
2752
+ document.documentElement.style.setProperty(`--dsh-any-stroke-${part}-w`, `${s.width}px`);
2753
+ document.documentElement.style.setProperty(`--dsh-any-stroke-${part}-c`, strokeColor(s));
2754
+ return;
2755
+ }
2756
+ discoverParts();
2757
+ const el = part === "bg" ? centerEl : sidebarEl;
2758
+ if (el === null) return;
2759
+ if (s.width > 0) {
2760
+ el.style.setProperty("-webkit-text-stroke", `${s.width}px ${strokeColor(s)}`);
2761
+ el.style.setProperty("paint-order", "stroke fill");
2762
+ } else {
2763
+ el.style.removeProperty("-webkit-text-stroke");
2764
+ el.style.removeProperty("paint-order");
2765
+ }
2766
+ }
2767
+ /** Teardown only: drop every stroke variable and column inline stroke. */
2768
+ function removeStrokes() {
2769
+ const root = document.documentElement;
2770
+ for (const g of [...STROKE_VAR_GROUPS, ...STROKE_INLINE_GROUPS]) {
2771
+ root.style.removeProperty(`--dsh-any-stroke-${g}-w`);
2772
+ root.style.removeProperty(`--dsh-any-stroke-${g}-c`);
2773
+ }
2774
+ for (const el of [centerEl, sidebarEl]) {
2775
+ if (el === null) continue;
2776
+ el.style.removeProperty("-webkit-text-stroke");
2777
+ el.style.removeProperty("paint-order");
2778
+ }
2779
+ }
2780
+ const FONT_FAMILY = "DAnyFont";
2781
+ let fontStyleEl = null;
2782
+ function fontFormatForMime(mime) {
2783
+ switch (mime) {
2784
+ case "font/woff2": return "woff2";
2785
+ case "font/woff": return "woff";
2786
+ case "font/otf": return "opentype";
2787
+ default: return "truetype";
2788
+ }
2789
+ }
2790
+ /** Apply or clear the custom interface font. `url` is the serve URL (null =
2791
+ * nothing stored); `enabled` gates the token override without deleting the
2792
+ * file. The original host stack is re-read on every apply so a host skin
2793
+ * change is picked up, and stays as the fallback after 'DAnyFont'. */
2794
+ function applyFontFace(url, enabled, mime) {
2795
+ if (url === null || !enabled) {
2796
+ fontStyleEl?.remove();
2797
+ fontStyleEl = null;
2798
+ return;
2799
+ }
2800
+ if (fontStyleEl === null || !fontStyleEl.isConnected) {
2801
+ fontStyleEl = document.createElement("style");
2802
+ fontStyleEl.dataset.plugin = "dsh-any-background-font";
2803
+ document.head.appendChild(fontStyleEl);
2804
+ }
2805
+ let stack = "";
2806
+ if (typeof getComputedStyle !== "undefined") stack = getComputedStyle(document.documentElement).getPropertyValue("--dsw-font-family").trim();
2807
+ if (stack === "") stack = "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif";
2808
+ const fmt = fontFormatForMime(mime);
2809
+ fontStyleEl.textContent = `@font-face{font-family:'${FONT_FAMILY}';src:url('${url}') format('${fmt}');font-display:swap}body{--dsw-font-family:'${FONT_FAMILY}',${stack}}input,textarea,select,button{font-family:var(--dsw-font-family)}`;
2810
+ }
2811
+ /** Teardown only: drop the @font-face + token override. */
2812
+ function removeFontFace() {
2813
+ fontStyleEl?.remove();
2814
+ fontStyleEl = null;
2815
+ }
2464
2816
  /** The host's own default colors for the panel layer tokens, used when the
2465
2817
  * plugin has no palette (no picked color, no wallpaper verdict, no forced
2466
2818
  * scheme). Without these the panel's re-scope would resolve to invalid vars
@@ -2888,6 +3240,7 @@ window.__ModuleLoader__.load({
2888
3240
  const ds = el.dataset;
2889
3241
  ds[prev + "Bg"] = el.style.getPropertyValue("background");
2890
3242
  if (plain) return;
3243
+ ds[prev + "BoxSizing"] = el.style.getPropertyValue("box-sizing");
2891
3244
  ds[prev + "Border"] = el.style.getPropertyValue("border");
2892
3245
  ds[prev + "Radius"] = el.style.getPropertyValue("border-radius");
2893
3246
  ds[prev + "Padding"] = el.style.getPropertyValue("padding");
@@ -2902,9 +3255,11 @@ window.__ModuleLoader__.load({
2902
3255
  };
2903
3256
  restore("background", ds[prev + "Bg"]);
2904
3257
  if (!plain) {
3258
+ restore("box-sizing", ds[prev + "BoxSizing"]);
2905
3259
  restore("border", ds[prev + "Border"]);
2906
3260
  restore("border-radius", ds[prev + "Radius"]);
2907
3261
  restore("padding", ds[prev + "Padding"]);
3262
+ delete ds[prev + "BoxSizing"];
2908
3263
  delete ds[prev + "Border"];
2909
3264
  delete ds[prev + "Radius"];
2910
3265
  delete ds[prev + "Padding"];
@@ -2974,6 +3329,7 @@ window.__ModuleLoader__.load({
2974
3329
  target.style.border = surface !== void 0 ? `1px solid ${toRgba(surface, borderAlpha)}` : "1px solid transparent";
2975
3330
  target.style.borderRadius = "16px";
2976
3331
  target.style.padding = "18px";
3332
+ target.style.boxSizing = "border-box";
2977
3333
  }
2978
3334
  target.setAttribute(spec.mark, "1");
2979
3335
  setBlur(target, blurPx);
@@ -3297,14 +3653,14 @@ window.__ModuleLoader__.load({
3297
3653
  wpVerdict = null;
3298
3654
  setBgDark(null);
3299
3655
  }
3300
- if (rHasColor() || rBgDark() !== null || rSchemeOverride() !== "auto") applyCustomTokens(rOps());
3301
- else if (!url) clearThemeTokens();
3656
+ applyCustomTokens(rOps());
3302
3657
  if (rHasColor()) {
3303
3658
  applySettingsOverrides(rSop());
3304
3659
  applyTrajectoryOverrides(rTrajectoryOpacity());
3305
3660
  }
3306
3661
  applyPanelOverrides(rPanelOpacity());
3307
3662
  applyPartBlurs(rBlurs());
3663
+ applyStrokes();
3308
3664
  }
3309
3665
  function teardownWp() {
3310
3666
  clearDynamicBg();
@@ -3349,6 +3705,8 @@ window.__ModuleLoader__.load({
3349
3705
  pendingOps = null;
3350
3706
  tableFixStyleEl?.remove();
3351
3707
  tableFixStyleEl = null;
3708
+ removeStrokes();
3709
+ removeFontFace();
3352
3710
  setBlur(frameEl, 0);
3353
3711
  setBlur(sidebarEl, 0);
3354
3712
  setBlur(centerEl, 0);
@@ -3556,6 +3914,29 @@ window.__ModuleLoader__.load({
3556
3914
  .dab-toggle.is-on{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
3557
3915
  .dab-toggle.is-on .dab-toggle-knob{transform:translateX(18px);background:#fff}
3558
3916
 
3917
+ /* ── custom font card ────────────────────────────────────────────────────── */
3918
+ .dab-font-title{display:flex;align-items:center;gap:11px}
3919
+ .dab-font-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:14px}
3920
+ .dab-font-name{font-family:var(--dab-mono);font-size:13.5px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));background:var(--dsw-alias-bg-layer-2);border-radius:99px;padding:5px 11px;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3921
+ .dab-font-name.is-empty{color:var(--dsw-alias-label-tertiary)}
3922
+ .dab-font-actions{display:flex;align-items:center;gap:8px;margin-left:auto}
3923
+ /* The font card's labels read one step larger than the rest of the panel —
3924
+ they carry most of the explanation on this page. Scoped to the card so the
3925
+ same .dab-hint / .dab-btn elsewhere keep their size. */
3926
+ .dab-font-actions .dab-btn{font-size:14.5px}
3927
+ .dab-font-card .dab-hint{font-size:13.5px}
3928
+
3929
+ /* ── text-stroke color dots ──────────────────────────────────────────────── */
3930
+ .dab-stroke-dots{display:flex;align-items:center;gap:9px;margin-top:14px}
3931
+ .dab-stroke-dot-wrap{position:relative;width:18px;height:18px;flex:none}
3932
+ .dab-stroke-dot{width:18px;height:18px;padding:0;border-radius:50%;border:1.5px solid var(--dsw-alias-border-l2);background-clip:padding-box;cursor:pointer;transition:transform .18s cubic-bezier(.34,1.56,.64,1),box-shadow .18s ease}
3933
+ .dab-stroke-dot:hover{transform:scale(1.16)}
3934
+ /* 'auto' has no fixed value — half light / half dark reads as "derived". */
3935
+ .dab-stroke-dot-auto{background:conic-gradient(from -90deg,#fff 0 50%,#000 50% 100%)}
3936
+ .dab-stroke-dot.is-active{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1),0 0 0 3.5px var(--dsw-alias-brand-primary);border-color:transparent}
3937
+ /* The native color picker sits invisible on top of the custom swatch. */
3938
+ .dab-stroke-dot-input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;border-radius:50%;background:none;opacity:0;cursor:pointer}
3939
+
3559
3940
  /* ── background preview hero ─────────────────────────────────────────────── */
3560
3941
  .dab-hero{position:relative;border-radius:16px;overflow:hidden;aspect-ratio:16/9;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)}
3561
3942
  .dab-hero-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transition:transform .5s cubic-bezier(.22,1,.36,1)}
@@ -3985,6 +4366,12 @@ window.__ModuleLoader__.load({
3985
4366
  className,
3986
4367
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
3987
4368
  });
4369
+ /** Letterform glyph: a stroked capital A (the font/type page + nav entry). */
4370
+ const FontIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
4371
+ size,
4372
+ className,
4373
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 12.2 8 3.8l4 8.4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.6 9.6h4.8" })]
4374
+ });
3988
4375
  const PlayIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
3989
4376
  size,
3990
4377
  className,
@@ -4975,7 +5362,7 @@ window.__ModuleLoader__.load({
4975
5362
  }
4976
5363
  //#endregion
4977
5364
  //#region src/client/components/pages/InterfacePage.tsx
4978
- const PARTS = [
5365
+ const PARTS$1 = [
4979
5366
  {
4980
5367
  opKey: "bg",
4981
5368
  labelKey: "uiOpacityBg",
@@ -5025,7 +5412,7 @@ window.__ModuleLoader__.load({
5025
5412
  function InterfacePage({ p }) {
5026
5413
  const { t, setOps, setBlurs, setSop, setPanelOp } = p;
5027
5414
  const hasBetterSidebar = useBetterSidebar();
5028
- const parts = PARTS.filter((part) => !(part.isPanel && !hasBetterSidebar));
5415
+ const parts = PARTS$1.filter((part) => !(part.isPanel && !hasBetterSidebar));
5029
5416
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
5030
5417
  className: "dab-head dab-rise",
5031
5418
  style: { "--d": 0 },
@@ -5152,10 +5539,327 @@ window.__ModuleLoader__.load({
5152
5539
  })] });
5153
5540
  }
5154
5541
  //#endregion
5542
+ //#region src/client/components/pages/FontPage.tsx
5543
+ const PARTS = [
5544
+ {
5545
+ key: "bg",
5546
+ labelKey: "uiOpacityBg",
5547
+ Icon: CanvasIcon
5548
+ },
5549
+ {
5550
+ key: "sidebar",
5551
+ labelKey: "uiOpacitySide",
5552
+ Icon: SidebarIcon
5553
+ },
5554
+ {
5555
+ key: "card",
5556
+ labelKey: "uiOpacityCard",
5557
+ Icon: ChatIcon
5558
+ },
5559
+ {
5560
+ key: "input",
5561
+ labelKey: "uiOpacityInput",
5562
+ Icon: InputIcon
5563
+ },
5564
+ {
5565
+ key: "settings",
5566
+ labelKey: "uiSop",
5567
+ Icon: GearIcon
5568
+ },
5569
+ {
5570
+ key: "chat",
5571
+ labelKey: "uiChatRegion",
5572
+ Icon: TextIcon
5573
+ },
5574
+ {
5575
+ key: "trajectory",
5576
+ labelKey: "uiTrajectory",
5577
+ Icon: TrajectoryIcon
5578
+ },
5579
+ {
5580
+ key: "produced",
5581
+ labelKey: "uiProduced",
5582
+ Icon: TextIcon
5583
+ },
5584
+ {
5585
+ key: "panel",
5586
+ labelKey: "uiPanelRegion",
5587
+ Icon: PanelIcon,
5588
+ needsSidebar: true
5589
+ }
5590
+ ];
5591
+ const COLOR_KEYS = [
5592
+ "auto",
5593
+ "gray",
5594
+ "black",
5595
+ "white",
5596
+ "theme",
5597
+ "custom"
5598
+ ];
5599
+ const COLOR_LABEL_KEYS = {
5600
+ auto: "strokeAuto",
5601
+ gray: "strokeGray",
5602
+ black: "strokeBlack",
5603
+ white: "strokeWhite",
5604
+ theme: "strokeTheme",
5605
+ custom: "strokeCustom"
5606
+ };
5607
+ /** Swatch fill for one preset key. 'auto' gets its half-black/half-white
5608
+ * conic gradient from CSS, and 'custom' shows the stored hex. */
5609
+ function dotStyle(key, s) {
5610
+ switch (key) {
5611
+ case "gray": return { background: "#808080" };
5612
+ case "black": return { background: "#000" };
5613
+ case "white": return { background: "#fff" };
5614
+ case "theme": return { background: "var(--dsw-alias-brand-primary)" };
5615
+ case "custom": return { background: s.customColor };
5616
+ default: return {};
5617
+ }
5618
+ }
5619
+ function FontPage({ p, notify }) {
5620
+ const { t, setFont, removeFont, setFontEnabled, setStrokes } = p;
5621
+ const hasBetterSidebar = useBetterSidebar();
5622
+ const fileRef = (0, react.useRef)(null);
5623
+ const [strokes, setStrokesState] = (0, react.useState)(() => rStrokes());
5624
+ const [fontInfo, setFontInfo] = (0, react.useState)(() => ({
5625
+ mime: cfg.fontMime,
5626
+ enabled: cfg.fontEnabled,
5627
+ name: null
5628
+ }));
5629
+ const [busy, setBusy] = (0, react.useState)(false);
5630
+ const hasFont = fontInfo.mime !== null;
5631
+ const fontLabel = fontInfo.name ?? (fontInfo.mime !== null ? fontInfo.mime.replace("font/", "").toUpperCase() : t("fontNone"));
5632
+ /** Write one group's stroke: cfg + live DOM now, disk on commit. `live`
5633
+ * marks slider drags (debounced save, no full re-apply). */
5634
+ const patch = (key, next, live) => {
5635
+ const map = {
5636
+ ...strokes,
5637
+ [key]: {
5638
+ ...strokes[key],
5639
+ ...next
5640
+ }
5641
+ };
5642
+ setStrokesState(map);
5643
+ cfg.strokes = map;
5644
+ setPartStroke(key, map[key]);
5645
+ if (live) saveConfig();
5646
+ else setStrokes(map);
5647
+ };
5648
+ const pickFont = async (file) => {
5649
+ setBusy(true);
5650
+ setFontInfo({
5651
+ ...fontInfo,
5652
+ name: file.name
5653
+ });
5654
+ const ok = await setFont(file);
5655
+ setBusy(false);
5656
+ if (ok) setFontInfo({
5657
+ mime: cfg.fontMime,
5658
+ enabled: cfg.fontEnabled,
5659
+ name: file.name
5660
+ });
5661
+ else {
5662
+ setFontInfo({
5663
+ mime: cfg.fontMime,
5664
+ enabled: cfg.fontEnabled,
5665
+ name: null
5666
+ });
5667
+ notify(t("fontFail"), false);
5668
+ }
5669
+ };
5670
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
5671
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
5672
+ className: "dab-head dab-rise",
5673
+ style: { "--d": 0 },
5674
+ children: [
5675
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5676
+ className: "dab-overline",
5677
+ children: "Typography"
5678
+ }),
5679
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
5680
+ className: "dab-h1",
5681
+ children: t("fontPageTitle")
5682
+ }),
5683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5684
+ className: "dab-desc",
5685
+ children: t("descFont")
5686
+ })
5687
+ ]
5688
+ }),
5689
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5690
+ className: "dab-card dab-rise dab-font-card",
5691
+ style: { "--d": 1 },
5692
+ children: [
5693
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5694
+ className: "dab-row-head",
5695
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5696
+ className: "dab-font-title",
5697
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5698
+ className: "dab-part-ico",
5699
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextIcon, { size: 16 })
5700
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5701
+ className: "dab-part-name",
5702
+ children: t("fontTitle")
5703
+ })]
5704
+ }), hasFont ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5705
+ type: "button",
5706
+ className: `dab-toggle${fontInfo.enabled ? " is-on" : ""}`,
5707
+ role: "switch",
5708
+ "aria-checked": fontInfo.enabled,
5709
+ "aria-label": t("fontEnabled"),
5710
+ onClick: () => {
5711
+ const v = !fontInfo.enabled;
5712
+ setFontEnabled(v);
5713
+ setFontInfo({
5714
+ ...fontInfo,
5715
+ enabled: v
5716
+ });
5717
+ },
5718
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
5719
+ }) : null]
5720
+ }),
5721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5722
+ className: "dab-font-row",
5723
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5724
+ className: `dab-font-name${hasFont ? "" : " is-empty"}`,
5725
+ title: fontLabel,
5726
+ children: fontLabel
5727
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5728
+ className: "dab-font-actions",
5729
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5730
+ type: "button",
5731
+ className: "dab-btn dab-btn-primary",
5732
+ disabled: busy,
5733
+ onClick: () => fileRef.current?.click(),
5734
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 14 }), busy ? t("fontUploading") : t("fontUpload")]
5735
+ }), hasFont ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5736
+ type: "button",
5737
+ className: "dab-btn dab-btn-danger",
5738
+ onClick: () => {
5739
+ removeFont();
5740
+ setFontInfo({
5741
+ mime: null,
5742
+ enabled: cfg.fontEnabled,
5743
+ name: null
5744
+ });
5745
+ },
5746
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 }), t("fontRemove")]
5747
+ }) : null]
5748
+ })]
5749
+ }),
5750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5751
+ className: "dab-hint",
5752
+ style: { marginTop: 10 },
5753
+ children: t("fontHint")
5754
+ }),
5755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5756
+ ref: fileRef,
5757
+ type: "file",
5758
+ accept: ".ttf,.otf,.woff,.woff2,font/*",
5759
+ style: { display: "none" },
5760
+ onChange: (e) => {
5761
+ const f = e.target.files?.[0];
5762
+ e.target.value = "";
5763
+ if (f) pickFont(f);
5764
+ }
5765
+ })
5766
+ ]
5767
+ }),
5768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5769
+ className: "dab-card dab-rise",
5770
+ style: { "--d": 2 },
5771
+ children: [
5772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5773
+ className: "dab-part-head",
5774
+ style: { marginBottom: 10 },
5775
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5776
+ className: "dab-part-name",
5777
+ children: t("strokeTitle")
5778
+ })
5779
+ }),
5780
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5781
+ className: "dab-hint",
5782
+ style: { marginBottom: 14 },
5783
+ children: t("strokeHint")
5784
+ }),
5785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5786
+ className: "dab-grid-parts",
5787
+ children: PARTS.filter((part) => !(part.needsSidebar && !hasBetterSidebar)).map((part, i) => {
5788
+ const s = strokes[part.key];
5789
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5790
+ className: "dab-card dab-card-hover dab-rise",
5791
+ style: { "--d": i + 3 },
5792
+ children: [
5793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5794
+ className: "dab-part-head",
5795
+ children: [
5796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5797
+ className: "dab-part-ico",
5798
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(part.Icon, { size: 16 })
5799
+ }),
5800
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5801
+ className: "dab-part-name",
5802
+ children: t(part.labelKey)
5803
+ }),
5804
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5805
+ className: "dab-part-badge",
5806
+ children: [s.width, "px"]
5807
+ })
5808
+ ]
5809
+ }),
5810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
5811
+ label: t("strokeWidth"),
5812
+ min: 0,
5813
+ max: 4,
5814
+ step: .5,
5815
+ def: s.width,
5816
+ fmt: (v) => `${v}px`,
5817
+ onInput: (v) => patch(part.key, { width: v }, true),
5818
+ onChange: (v) => patch(part.key, { width: v }, false)
5819
+ }),
5820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5821
+ className: "dab-stroke-dots",
5822
+ children: COLOR_KEYS.map((key) => {
5823
+ const active = s.color === key;
5824
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5825
+ className: "dab-stroke-dot-wrap",
5826
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5827
+ type: "button",
5828
+ className: `dab-stroke-dot${key === "auto" ? " dab-stroke-dot-auto" : ""}${active ? " is-active" : ""}`,
5829
+ style: dotStyle(key, s),
5830
+ title: t(COLOR_LABEL_KEYS[key]),
5831
+ "aria-label": t(COLOR_LABEL_KEYS[key]),
5832
+ "aria-pressed": active,
5833
+ onClick: () => patch(part.key, { color: key }, false)
5834
+ }), key === "custom" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5835
+ type: "color",
5836
+ className: "dab-stroke-dot-input",
5837
+ value: s.customColor,
5838
+ "aria-label": t("strokeCustom"),
5839
+ onChange: (e) => patch(part.key, {
5840
+ color: "custom",
5841
+ customColor: e.target.value
5842
+ }, false)
5843
+ }) : null]
5844
+ }, key);
5845
+ })
5846
+ })
5847
+ ]
5848
+ }, part.key);
5849
+ })
5850
+ })
5851
+ ]
5852
+ })
5853
+ ] });
5854
+ }
5855
+ //#endregion
5155
5856
  //#region src/client/components/BgEditor.tsx
5156
5857
  /** Placement slot for the active background type: video framing lives in its
5157
5858
  * own state so image and video edits never overwrite each other. */
5158
5859
  const activeState = () => cfg.backgroundType === "video" ? cfg.videoBgState : cfg.bgState;
5860
+ /** Zoom is shared by every input path (wheel + pinch) so all pictures animate
5861
+ * between the same bounds. */
5862
+ const clampZoom = (z) => Math.max(.1, Math.min(10, z));
5159
5863
  function BgEditor({ url, t, onClose, onCommit }) {
5160
5864
  const pw = Math.min(window.innerWidth * .75, 860);
5161
5865
  const ph = Math.round(pw * window.innerHeight / window.innerWidth);
@@ -5181,6 +5885,19 @@ window.__ModuleLoader__.load({
5181
5885
  spx: 0,
5182
5886
  spy: 0
5183
5887
  });
5888
+ const viewRef = (0, react.useRef)({
5889
+ zoom,
5890
+ pos
5891
+ });
5892
+ viewRef.current = {
5893
+ zoom,
5894
+ pos
5895
+ };
5896
+ /** Pinch gesture baseline (two fingers down): finger spread + midpoint and
5897
+ * the transform that was live at that moment. */
5898
+ const pinchRef = (0, react.useRef)(null);
5899
+ /** Single-finger pan baseline. */
5900
+ const touchDragRef = (0, react.useRef)(null);
5184
5901
  (0, react.useEffect)(() => {
5185
5902
  const img = new Image();
5186
5903
  img.onload = () => {
@@ -5242,7 +5959,7 @@ window.__ModuleLoader__.load({
5242
5959
  const rect = el.getBoundingClientRect();
5243
5960
  const mx = rect.width / 2, my = rect.height / 2;
5244
5961
  const factor = e.deltaY > 0 ? .97 : 1.03;
5245
- const nz = Math.max(.1, Math.min(10, zoom * factor));
5962
+ const nz = clampZoom(zoom * factor);
5246
5963
  const nx = mx - (mx - pos.x) * (nz / zoom);
5247
5964
  const ny = my - (my - pos.y) * (nz / zoom);
5248
5965
  setZoom(nz);
@@ -5257,6 +5974,97 @@ window.__ModuleLoader__.load({
5257
5974
  el.addEventListener("wheel", onWheelCb, { passive: false });
5258
5975
  return () => el.removeEventListener("wheel", onWheelCb);
5259
5976
  }, [onWheelCb]);
5977
+ const onTouchStartCb = (0, react.useCallback)((e) => {
5978
+ const el = containerRef.current;
5979
+ if (!el) return;
5980
+ if (e.touches.length === 1) {
5981
+ pinchRef.current = null;
5982
+ const tp = e.touches[0];
5983
+ touchDragRef.current = {
5984
+ sx: tp.clientX,
5985
+ sy: tp.clientY,
5986
+ spx: viewRef.current.pos.x,
5987
+ spy: viewRef.current.pos.y
5988
+ };
5989
+ return;
5990
+ }
5991
+ if (e.touches.length >= 2) {
5992
+ touchDragRef.current = null;
5993
+ const rect = el.getBoundingClientRect();
5994
+ const a = e.touches[0], b = e.touches[1];
5995
+ pinchRef.current = {
5996
+ dist: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY),
5997
+ zoom: viewRef.current.zoom,
5998
+ cx: (a.clientX + b.clientX) / 2 - rect.left,
5999
+ cy: (a.clientY + b.clientY) / 2 - rect.top,
6000
+ px: viewRef.current.pos.x,
6001
+ py: viewRef.current.pos.y
6002
+ };
6003
+ }
6004
+ }, []);
6005
+ const onTouchMoveCb = (0, react.useCallback)((e) => {
6006
+ const el = containerRef.current;
6007
+ if (!el) return;
6008
+ if (e.touches.length >= 2 && pinchRef.current !== null) {
6009
+ e.preventDefault();
6010
+ const p = pinchRef.current;
6011
+ if (p.dist <= 0) return;
6012
+ const rect = el.getBoundingClientRect();
6013
+ const a = e.touches[0], b = e.touches[1];
6014
+ const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
6015
+ const cx = (a.clientX + b.clientX) / 2 - rect.left;
6016
+ const cy = (a.clientY + b.clientY) / 2 - rect.top;
6017
+ const nz = clampZoom(p.zoom * (dist / p.dist));
6018
+ setZoom(nz);
6019
+ setPos({
6020
+ x: cx - (p.cx - p.px) / p.zoom * nz,
6021
+ y: cy - (p.cy - p.py) / p.zoom * nz
6022
+ });
6023
+ return;
6024
+ }
6025
+ if (e.touches.length === 1 && touchDragRef.current !== null) {
6026
+ e.preventDefault();
6027
+ const d = touchDragRef.current;
6028
+ const tp = e.touches[0];
6029
+ setPos({
6030
+ x: d.spx + tp.clientX - d.sx,
6031
+ y: d.spy + tp.clientY - d.sy
6032
+ });
6033
+ }
6034
+ }, []);
6035
+ const onTouchEndCb = (0, react.useCallback)((e) => {
6036
+ if (e.touches.length === 1) {
6037
+ const tp = e.touches[0];
6038
+ touchDragRef.current = {
6039
+ sx: tp.clientX,
6040
+ sy: tp.clientY,
6041
+ spx: viewRef.current.pos.x,
6042
+ spy: viewRef.current.pos.y
6043
+ };
6044
+ pinchRef.current = null;
6045
+ } else if (e.touches.length === 0) {
6046
+ touchDragRef.current = null;
6047
+ pinchRef.current = null;
6048
+ }
6049
+ }, []);
6050
+ (0, react.useEffect)(() => {
6051
+ const el = containerRef.current;
6052
+ if (!el) return;
6053
+ el.addEventListener("touchstart", onTouchStartCb, { passive: false });
6054
+ el.addEventListener("touchmove", onTouchMoveCb, { passive: false });
6055
+ el.addEventListener("touchend", onTouchEndCb, { passive: false });
6056
+ el.addEventListener("touchcancel", onTouchEndCb, { passive: false });
6057
+ return () => {
6058
+ el.removeEventListener("touchstart", onTouchStartCb);
6059
+ el.removeEventListener("touchmove", onTouchMoveCb);
6060
+ el.removeEventListener("touchend", onTouchEndCb);
6061
+ el.removeEventListener("touchcancel", onTouchEndCb);
6062
+ };
6063
+ }, [
6064
+ onTouchStartCb,
6065
+ onTouchMoveCb,
6066
+ onTouchEndCb
6067
+ ]);
5260
6068
  const resetView = (0, react.useCallback)(() => {
5261
6069
  if (imgSize.w === 0) return;
5262
6070
  setZoom(1);
@@ -5290,7 +6098,10 @@ window.__ModuleLoader__.load({
5290
6098
  background: "#000",
5291
6099
  cursor: "grab",
5292
6100
  width: pw,
5293
- height: ph
6101
+ height: ph,
6102
+ touchAction: "none",
6103
+ WebkitUserSelect: "none",
6104
+ userSelect: "none"
5294
6105
  },
5295
6106
  onMouseDown: onDown,
5296
6107
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
@@ -6075,6 +6886,27 @@ window.__ModuleLoader__.load({
6075
6886
  panel: 0,
6076
6887
  produced: 0
6077
6888
  };
6889
+ /** Presets ship with strokes off — text outlines are an opt-in look. Fresh
6890
+ * objects per call so a profile restore can never alias another preset's
6891
+ * stroke config (applyAppearance copies, but mutation-proofing is free). */
6892
+ const zeroStrokes = () => {
6893
+ const off = () => ({
6894
+ width: 0,
6895
+ color: "auto",
6896
+ customColor: "#808080"
6897
+ });
6898
+ return {
6899
+ bg: off(),
6900
+ sidebar: off(),
6901
+ card: off(),
6902
+ settings: off(),
6903
+ chat: off(),
6904
+ trajectory: off(),
6905
+ input: off(),
6906
+ panel: off(),
6907
+ produced: off()
6908
+ };
6909
+ };
6078
6910
  const BUILTIN_PRESETS = [
6079
6911
  {
6080
6912
  key: "default",
@@ -6087,6 +6919,7 @@ window.__ModuleLoader__.load({
6087
6919
  input: 1
6088
6920
  },
6089
6921
  blurs: { ...zeroBlurs },
6922
+ strokes: zeroStrokes(),
6090
6923
  settingsOpacity: 1,
6091
6924
  wallpaperOpacity: 1,
6092
6925
  blur: 0,
@@ -6120,6 +6953,7 @@ window.__ModuleLoader__.load({
6120
6953
  input: 14,
6121
6954
  panel: 12
6122
6955
  },
6956
+ strokes: zeroStrokes(),
6123
6957
  settingsOpacity: .88,
6124
6958
  wallpaperOpacity: 1,
6125
6959
  blur: 0,
@@ -6140,6 +6974,7 @@ window.__ModuleLoader__.load({
6140
6974
  input: 1
6141
6975
  },
6142
6976
  blurs: { ...zeroBlurs },
6977
+ strokes: zeroStrokes(),
6143
6978
  settingsOpacity: 1,
6144
6979
  wallpaperOpacity: 1,
6145
6980
  blur: 0,
@@ -6173,6 +7008,7 @@ window.__ModuleLoader__.load({
6173
7008
  input: 16,
6174
7009
  panel: 16
6175
7010
  },
7011
+ strokes: zeroStrokes(),
6176
7012
  settingsOpacity: .85,
6177
7013
  wallpaperOpacity: .92,
6178
7014
  blur: 2,
@@ -6206,6 +7042,7 @@ window.__ModuleLoader__.load({
6206
7042
  input: 20,
6207
7043
  panel: 18
6208
7044
  },
7045
+ strokes: zeroStrokes(),
6209
7046
  settingsOpacity: .8,
6210
7047
  wallpaperOpacity: 1,
6211
7048
  blur: 0,
@@ -6230,6 +7067,7 @@ window.__ModuleLoader__.load({
6230
7067
  input: 1
6231
7068
  },
6232
7069
  blurs: { ...zeroBlurs },
7070
+ strokes: zeroStrokes(),
6233
7071
  settingsOpacity: 1,
6234
7072
  wallpaperOpacity: 1,
6235
7073
  blur: 0,
@@ -6689,6 +7527,14 @@ window.__ModuleLoader__.load({
6689
7527
  Icon: LayersIcon,
6690
7528
  node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InterfacePage, { p: props })
6691
7529
  },
7530
+ {
7531
+ label: t("pageFont"),
7532
+ Icon: FontIcon,
7533
+ node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FontPage, {
7534
+ p: props,
7535
+ notify
7536
+ })
7537
+ },
6692
7538
  {
6693
7539
  label: t("pageBackground"),
6694
7540
  Icon: PhotoIcon,
@@ -6826,7 +7672,7 @@ window.__ModuleLoader__.load({
6826
7672
  }, "dsh-any-background: skin dispose");
6827
7673
  const styleEl = document.createElement("style");
6828
7674
  styleEl.dataset.plugin = "dsh-any-background";
6829
- styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${POPOVER_BLUR_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}${MOBILE_HEADER_RULE}${PANEL_TOKEN_RULE}${PANEL_BLUR_RULE}${PRODUCED_RULE}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}`;
7675
+ styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${POPOVER_BLUR_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}${MOBILE_HEADER_RULE}${PANEL_TOKEN_RULE}${PANEL_BLUR_RULE}${PRODUCED_RULE}${STROKE_RULE}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}`;
6830
7676
  document.head.appendChild(styleEl);
6831
7677
  ctx.effect(() => () => {
6832
7678
  styleEl?.parentNode?.removeChild(styleEl);
@@ -7022,8 +7868,14 @@ window.__ModuleLoader__.load({
7022
7868
  applyWp();
7023
7869
  syncBg();
7024
7870
  watchParts();
7025
- loadPersisted().then(async (serverRotated) => {
7871
+ loadPersisted().then(async ({ rotated: serverRotated, firstRun }) => {
7872
+ if (firstRun) {
7873
+ persistConfig();
7874
+ await loadPersisted();
7875
+ applyWp();
7876
+ }
7026
7877
  syncMetaNow();
7878
+ applyFontFace(fontServeUrl, cfg.fontEnabled, cfg.fontMime);
7027
7879
  if (rHasColor()) {
7028
7880
  const [h, s, l] = rColor();
7029
7881
  registerCustom(h, s, l);
@@ -7126,6 +7978,28 @@ window.__ModuleLoader__.load({
7126
7978
  if (ok) persistConfig();
7127
7979
  });
7128
7980
  };
7981
+ const FONT_EXT_MIME = {
7982
+ woff2: "font/woff2",
7983
+ woff: "font/woff",
7984
+ otf: "font/otf",
7985
+ ttf: "font/ttf"
7986
+ };
7987
+ const fontMimeFromName = (name) => {
7988
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
7989
+ return FONT_EXT_MIME[ext] ?? "font/ttf";
7990
+ };
7991
+ let fontPreviewUrl = null;
7992
+ const releaseFontPreview = (delay) => {
7993
+ if (fontPreviewUrl === null) return;
7994
+ const url = fontPreviewUrl;
7995
+ fontPreviewUrl = null;
7996
+ window.setTimeout(() => URL.revokeObjectURL(url), delay);
7997
+ };
7998
+ /** Point the @font-face at whatever is authoritative right now: the
7999
+ * persisted slot when one exists, otherwise nothing. */
8000
+ const applyStoredFont = () => {
8001
+ applyFontFace(cfg.fontMime !== null ? FONT_SERVE_URL : null, cfg.fontEnabled, cfg.fontMime);
8002
+ };
7129
8003
  const [wh, ws, wl] = rColor();
7130
8004
  const [dh, ds, dv] = hslToHsv(wh, ws, wl);
7131
8005
  return {
@@ -7232,6 +8106,41 @@ window.__ModuleLoader__.load({
7232
8106
  syncBg();
7233
8107
  saveConfig();
7234
8108
  },
8109
+ setStrokes: (strokes) => {
8110
+ cfg.strokes = strokes;
8111
+ applyStrokes();
8112
+ saveConfig();
8113
+ },
8114
+ setFont: async (file) => {
8115
+ releaseFontPreview(0);
8116
+ const localUrl = URL.createObjectURL(file);
8117
+ fontPreviewUrl = localUrl;
8118
+ applyFontFace(localUrl, true, fontMimeFromName(file.name));
8119
+ const mime = await uploadFont(file);
8120
+ if (mime === null) {
8121
+ applyStoredFont();
8122
+ releaseFontPreview(4e3);
8123
+ return false;
8124
+ }
8125
+ cfg.fontMime = mime;
8126
+ cfg.fontEnabled = true;
8127
+ applyFontFace(FONT_SERVE_URL, true, mime);
8128
+ releaseFontPreview(4e3);
8129
+ persistConfig();
8130
+ return true;
8131
+ },
8132
+ removeFont: () => {
8133
+ releaseFontPreview(0);
8134
+ cfg.fontMime = null;
8135
+ applyFontFace(null, false, null);
8136
+ removeFont();
8137
+ persistConfig();
8138
+ },
8139
+ setFontEnabled: (v) => {
8140
+ cfg.fontEnabled = v;
8141
+ applyStoredFont();
8142
+ persistConfig();
8143
+ },
7235
8144
  setWop: (v) => {
7236
8145
  cfg.wallpaperOpacity = v;
7237
8146
  applyWp();
@@ -7358,6 +8267,7 @@ window.__ModuleLoader__.load({
7358
8267
  else if (cfg.generatedBg) updateGeneratedBg(cfg.generatedBg);
7359
8268
  }
7360
8269
  persistConfig();
8270
+ syncMetaNow();
7361
8271
  if (rHasColor()) {
7362
8272
  const [h, s, l] = rColor();
7363
8273
  registerCustom(h, s, l);