@nonamelego/dsh-catppuccin 0.5.4 → 0.5.6-beta.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/lib/client.js CHANGED
@@ -134,6 +134,7 @@ window.__ModuleLoader__.load({
134
134
  const style = (0, react.useSyncExternalStore)(subscribePrefs, shikiStyle);
135
135
  const [overridesOpen, setOverridesOpen] = (0, react.useState)(false);
136
136
  const [draftRows, setDraftRows] = (0, react.useState)([]);
137
+ const nextDraftId = (0, react.useRef)(0);
137
138
  const isTokenKey = (key) => key.trim().startsWith("--");
138
139
  /** Commit a draft row on blur. If both fields are valid, promote to persisted;
139
140
  * otherwise keep in draft state. Item XX: changed from onChange to onBlur so
@@ -457,11 +458,13 @@ window.__ModuleLoader__.load({
457
458
  children: "✕"
458
459
  })
459
460
  ]
460
- }, `draft-${index}`)),
461
+ }, row.id)),
461
462
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
462
463
  type: "button",
463
464
  onClick: () => {
465
+ nextDraftId.current += 1;
464
466
  setDraftRows([...draftRows, {
467
+ id: nextDraftId.current,
465
468
  key: "",
466
469
  value: ""
467
470
  }]);
@@ -499,15 +502,11 @@ window.__ModuleLoader__.load({
499
502
  "row.shikiItalicComments": "柔和注释",
500
503
  "row.overrides": "自定义覆盖",
501
504
  "row.overridesHint": "按「--dsw-* 变量: 值」覆盖单个配色 token,例如 --dsw-static-blue-500: #89b4fa。键名与值都在失焦时生效,清空值即删除。",
502
- "row.overridesEmpty": "暂无覆盖。点击“+ 添加”新增一条 token 覆盖。",
505
+ "row.overridesEmpty": "暂无覆盖。点击「+ 添加」新增一条 token 覆盖。",
503
506
  "row.overridesAdd": "添加",
504
507
  "row.overridesRemove": "删除此覆盖",
505
508
  "row.overridesKey": "token 名称",
506
509
  "row.overridesValue": "token 值",
507
- "flavor.latte": "Latte",
508
- "flavor.frappe": "Frappé",
509
- "flavor.macchiato": "Macchiato",
510
- "flavor.mocha": "Mocha",
511
510
  "flavor.latte.subtitle": "浅色",
512
511
  "flavor.frappe.subtitle": "深色",
513
512
  "flavor.macchiato.subtitle": "深色",
@@ -517,8 +516,6 @@ window.__ModuleLoader__.load({
517
516
  "glass.helpLabel": "关于玻璃质感",
518
517
  "glass.help": "在 Catppuccin 主题之上叠加磨砂玻璃皮肤:悬浮卡片(云母效果)或保持原版排版的兼容模式。模糊度、磨砂度、背景亮度均可调,并支持清透 / 标准 / 磨砂三档预设。云母效果会在顶栏、输入框等大面积区域做背景模糊,流式输出时较吃 GPU——在意性能请用兼容模式,或选「清透」预设。",
519
518
  "glass.enable": "总开关",
520
- "glass.on": "开启",
521
- "glass.off": "关闭",
522
519
  "glass.mode": "模式",
523
520
  "glass.modeMica": "云母效果",
524
521
  "glass.modeCompat": "兼容模式",
@@ -536,7 +533,7 @@ window.__ModuleLoader__.load({
536
533
  "update.title": "检查 Catppuccin 插件更新",
537
534
  "update.description": "检测本插件(dsh-catppuccin)在 npm 上的最新版本,判断当前安装是否需要升级。",
538
535
  "update.helpLabel": "关于更新检查",
539
- "update.help": "向 npm registry 查询本插件的最新版本。开启“自动检查”后会在启动时与每 6 小时自动检查;渠道选 beta 可追踪预发布版本。",
536
+ "update.help": "向 npm registry 查询本插件的最新版本。开启「自动检查」后会在启动时与每 6 小时自动检查;渠道选 beta 可追踪预发布版本。",
540
537
  "update.autoCheck": "自动检查更新",
541
538
  "update.channel": "渠道",
542
539
  "update.channelLatest": "稳定版",
@@ -585,10 +582,6 @@ window.__ModuleLoader__.load({
585
582
  "row.overridesRemove": "Remove this override",
586
583
  "row.overridesKey": "Token name",
587
584
  "row.overridesValue": "Token value",
588
- "flavor.latte": "Latte",
589
- "flavor.frappe": "Frappé",
590
- "flavor.macchiato": "Macchiato",
591
- "flavor.mocha": "Mocha",
592
585
  "flavor.latte.subtitle": "Light",
593
586
  "flavor.frappe.subtitle": "Dark",
594
587
  "flavor.macchiato.subtitle": "Dark",
@@ -598,8 +591,6 @@ window.__ModuleLoader__.load({
598
591
  "glass.helpLabel": "About glassmorphism",
599
592
  "glass.help": "A frosted-glass skin layered over the Catppuccin theme: floating cards (Mica) or the stock layout with a glass material (Compatibility). Blur, frost and backdrop brightness are tunable, with Clear / Standard / Frosted presets. Mica blurs large areas (top bar, composer), so it costs GPU while output streams — pick Compatibility or the Clear preset if that matters.",
600
593
  "glass.enable": "Master switch",
601
- "glass.on": "On",
602
- "glass.off": "Off",
603
594
  "glass.mode": "Mode",
604
595
  "glass.modeMica": "Mica",
605
596
  "glass.modeCompat": "Compatibility",
@@ -666,10 +657,6 @@ window.__ModuleLoader__.load({
666
657
  "row.overridesRemove": "この上書きを削除",
667
658
  "row.overridesKey": "トークン名",
668
659
  "row.overridesValue": "トークン値",
669
- "flavor.latte": "Latte",
670
- "flavor.frappe": "Frappé",
671
- "flavor.macchiato": "Macchiato",
672
- "flavor.mocha": "Mocha",
673
660
  "flavor.latte.subtitle": "ライト",
674
661
  "flavor.frappe.subtitle": "ダーク",
675
662
  "flavor.macchiato.subtitle": "ダーク",
@@ -677,10 +664,8 @@ window.__ModuleLoader__.load({
677
664
  "glass.title": "ガラス質感",
678
665
  "glass.description": "トップバー・サイドバー・入力欄・ステータス行・軌跡ビューにすりガラス効果、ワンクリックで ON/OFF。",
679
666
  "glass.helpLabel": "ガラス質感について",
680
- "glass.help": "Catppuccin テーマの上に乗せるガラススキン: 浮遊カード(Mica)または原版レイアウト互換モード。ぼかし・曇り・背景輝度を調整でき、クリア / スタンダード / フォグのプリセット付き。Mica はトップバーや入力欄など広い範囲をぼかすため、ストリーミング中は GPU を消費します。気になる場合は互換モードか「クリア」プリセットを。",
667
+ "glass.help": "Catppuccin テーマの上に乗せるガラススキン: 浮遊カード(Mica)または原版レイアウト互換モード。ぼかし・曇り・背景輝度を調整でき、クリア / スタンダード / 曇りのプリセット付き。Mica はトップバーや入力欄など広い範囲をぼかすため、ストリーミング中は GPU を消費します。気になる場合は互換モードか「クリア」プリセットを。",
681
668
  "glass.enable": "マスタースイッチ",
682
- "glass.on": "オン",
683
- "glass.off": "オフ",
684
669
  "glass.mode": "モード",
685
670
  "glass.modeMica": "Mica",
686
671
  "glass.modeCompat": "互換",
@@ -688,7 +673,7 @@ window.__ModuleLoader__.load({
688
673
  "glass.presets": "プリセット",
689
674
  "glass.presetClear": "クリア",
690
675
  "glass.presetStandard": "スタンダード",
691
- "glass.presetFrosted": "フォグ",
676
+ "glass.presetFrosted": "曇り",
692
677
  "glass.blur": "ぼかし",
693
678
  "glass.frost": "曇り",
694
679
  "glass.brightness": "背景の輝度",
@@ -747,10 +732,6 @@ window.__ModuleLoader__.load({
747
732
  "row.overridesRemove": "이 오버라이드 삭제",
748
733
  "row.overridesKey": "토큰 이름",
749
734
  "row.overridesValue": "토큰 값",
750
- "flavor.latte": "Latte",
751
- "flavor.frappe": "Frappé",
752
- "flavor.macchiato": "Macchiato",
753
- "flavor.mocha": "Mocha",
754
735
  "flavor.latte.subtitle": "밝음",
755
736
  "flavor.frappe.subtitle": "어두움",
756
737
  "flavor.macchiato.subtitle": "어두움",
@@ -758,10 +739,8 @@ window.__ModuleLoader__.load({
758
739
  "glass.title": "글라스 질감",
759
740
  "glass.description": "상단바·사이드바·입력창·통계줄·궤적 뷰에 프러스트드 글라스 효과, 원클릭 ON/OFF.",
760
741
  "glass.helpLabel": "글라스 질감 정보",
761
- "glass.help": "Catppuccin 테마 위에 얹는 글라스 스킨: 떠 있는 카드(Mica) 또는 원본 레이아웃 호환 모드. 블러·프로스트·배경 밝기를 조절할 수 있고 클리어/스탠다드/포그 프리셋이 있습니다. Mica는 상단 바·입력창 등 넓은 영역을 흐리게 하므로 스트리밍 중 GPU를 더 씁니다. 부담되면 호환 모드나 「클리어」 프리셋을 쓰세요.",
742
+ "glass.help": "Catppuccin 테마 위에 얹는 글라스 스킨: 떠 있는 카드(Mica) 또는 원본 레이아웃 호환 모드. 블러·프로스트·배경 밝기를 조절할 수 있고 클리어/스탠다드/프로스트 프리셋이 있습니다. Mica는 상단 바·입력창 등 넓은 영역을 흐리게 하므로 스트리밍 중 GPU를 더 씁니다. 부담되면 호환 모드나 「클리어」 프리셋을 쓰세요.",
762
743
  "glass.enable": "마스터 스위치",
763
- "glass.on": "켜기",
764
- "glass.off": "끄기",
765
744
  "glass.mode": "모드",
766
745
  "glass.modeMica": "Mica",
767
746
  "glass.modeCompat": "호환",
@@ -769,7 +748,7 @@ window.__ModuleLoader__.load({
769
748
  "glass.presets": "프리셋",
770
749
  "glass.presetClear": "클리어",
771
750
  "glass.presetStandard": "스탠다드",
772
- "glass.presetFrosted": "포그",
751
+ "glass.presetFrosted": "프로스트",
773
752
  "glass.blur": "블러",
774
753
  "glass.frost": "프로스트",
775
754
  "glass.brightness": "배경 밝기",
@@ -828,10 +807,6 @@ window.__ModuleLoader__.load({
828
807
  "row.overridesRemove": "Eliminar esta anulación",
829
808
  "row.overridesKey": "Nombre del token",
830
809
  "row.overridesValue": "Valor del token",
831
- "flavor.latte": "Latte",
832
- "flavor.frappe": "Frappé",
833
- "flavor.macchiato": "Macchiato",
834
- "flavor.mocha": "Mocha",
835
810
  "flavor.latte.subtitle": "Claro",
836
811
  "flavor.frappe.subtitle": "Oscuro",
837
812
  "flavor.macchiato.subtitle": "Oscuro",
@@ -841,8 +816,6 @@ window.__ModuleLoader__.load({
841
816
  "glass.helpLabel": "Acerca del vidrio esmerilado",
842
817
  "glass.help": "Una piel de vidrio esmerilado sobre el tema Catppuccin: tarjetas flotantes (Mica) o el diseño original con material de vidrio (Compatibilidad). Desenfoque, escarcha y brillo del fondo ajustables, con preajustes Claro / Estándar / Escarchado. Mica desenfoca zonas grandes (barra superior, cuadro de texto), así que consume GPU mientras llega la respuesta: usa Compatibilidad o el preajuste Claro si te importa.",
843
818
  "glass.enable": "Interruptor general",
844
- "glass.on": "Sí",
845
- "glass.off": "No",
846
819
  "glass.mode": "Modo",
847
820
  "glass.modeMica": "Mica",
848
821
  "glass.modeCompat": "Compatibilidad",
@@ -909,10 +882,6 @@ window.__ModuleLoader__.load({
909
882
  "row.overridesRemove": "Supprimer ce remplacement",
910
883
  "row.overridesKey": "Nom du jeton",
911
884
  "row.overridesValue": "Valeur du jeton",
912
- "flavor.latte": "Latte",
913
- "flavor.frappe": "Frappé",
914
- "flavor.macchiato": "Macchiato",
915
- "flavor.mocha": "Mocha",
916
885
  "flavor.latte.subtitle": "Clair",
917
886
  "flavor.frappe.subtitle": "Sombre",
918
887
  "flavor.macchiato.subtitle": "Sombre",
@@ -922,8 +891,6 @@ window.__ModuleLoader__.load({
922
891
  "glass.helpLabel": "À propos du verre dépoli",
923
892
  "glass.help": "Une peau en verre dépoli par-dessus le thème Catppuccin : cartes flottantes (Mica) ou mise en page d'origine avec un matériau verre (Compatibilité). Flou, givre et luminosité du fond réglables, avec les préréglages Clair / Standard / Givré. Mica floute de grandes zones (barre supérieure, champ de saisie) : cela consomme le GPU pendant la génération — préférez Compatibilité ou le préréglage Clair si c'est gênant.",
924
893
  "glass.enable": "Interrupteur principal",
925
- "glass.on": "Oui",
926
- "glass.off": "Non",
927
894
  "glass.mode": "Mode",
928
895
  "glass.modeMica": "Mica",
929
896
  "glass.modeCompat": "Compatibilité",
@@ -990,10 +957,6 @@ window.__ModuleLoader__.load({
990
957
  "row.overridesRemove": "Diese Überschreibung entfernen",
991
958
  "row.overridesKey": "Token-Name",
992
959
  "row.overridesValue": "Token-Wert",
993
- "flavor.latte": "Latte",
994
- "flavor.frappe": "Frappé",
995
- "flavor.macchiato": "Macchiato",
996
- "flavor.mocha": "Mocha",
997
960
  "flavor.latte.subtitle": "Hell",
998
961
  "flavor.frappe.subtitle": "Dunkel",
999
962
  "flavor.macchiato.subtitle": "Dunkel",
@@ -1003,8 +966,6 @@ window.__ModuleLoader__.load({
1003
966
  "glass.helpLabel": "Über Glasmorphismus",
1004
967
  "glass.help": "Eine Milchglas-Haut über dem Catppuccin-Design: schwebende Karten (Mica) oder das Original-Layout mit Glas-Material (Kompatibilität). Unschärfe, Frost und Hintergrundhelligkeit einstellbar, mit den Voreinstellungen Klar / Standard / Frostig. Mica verwischt große Flächen (Kopfleiste, Eingabefeld) und kostet daher GPU, während die Antwort streamt — nimm Kompatibilität oder die Voreinstellung Klar, wenn das stört.",
1005
968
  "glass.enable": "Hauptschalter",
1006
- "glass.on": "An",
1007
- "glass.off": "Aus",
1008
969
  "glass.mode": "Modus",
1009
970
  "glass.modeMica": "Mica",
1010
971
  "glass.modeCompat": "Kompatibilität",
@@ -2001,6 +1962,21 @@ window.__ModuleLoader__.load({
2001
1962
  for (const [key, value] of Object.entries(input)) if (typeof value === "string" && key.startsWith("--")) out[key] = value;
2002
1963
  return out;
2003
1964
  }
1965
+ /**
1966
+ * Whether a raw overrides map carries entries {@link sanitizeOverrides} would
1967
+ * drop — a key that is not a `--` token, or a non-string value.
1968
+ *
1969
+ * The settings-document schema types `overrides` as a dict of strings only, so
1970
+ * a hand-edited document can hold such entries. The client reads them away but
1971
+ * never wrote the cleaned map back, so every settings publish re-ran the
1972
+ * "document wins" adoption for a difference nothing could clear (audit F7).
1973
+ * The caller uses this to push the sanitized shape back once, so the document
1974
+ * converges instead of drifting forever.
1975
+ */
1976
+ function hasUnpersistableOverrides(raw) {
1977
+ if (typeof raw !== "object" || raw === null) return false;
1978
+ return Object.entries(raw).some(([key, value]) => typeof value !== "string" || !key.startsWith("--"));
1979
+ }
2004
1980
  /** Clamp a finite number into [min, max]; non-finite values fall back. */
2005
1981
  function clampFinite(value, min, max, fallback) {
2006
1982
  return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
@@ -2099,6 +2075,13 @@ window.__ModuleLoader__.load({
2099
2075
  frost: "dsh.catppuccin.glass.frost",
2100
2076
  brightness: "dsh.catppuccin.glass.brightness"
2101
2077
  };
2078
+ /** The localStorage names of the numeric knobs — the reverse of
2079
+ * `NUMERIC_KEYS`. Used by the cross-tab `storage` handler: the event's `key`
2080
+ * is a localStorage name (`dsh.catppuccin.glass.blur`), so it must be matched
2081
+ * against the VALUES, never with `key in NUMERIC_KEYS` (that tests the object's
2082
+ * property names — `blur`/`frost`/`brightness` — and is false for every real
2083
+ * storage key, which silently killed cross-tab knob sync; audit F1). */
2084
+ const NUMERIC_STORAGE_KEYS = Object.values(NUMERIC_KEYS);
2102
2085
  const MODE_KEY = "dsh.catppuccin.glass.mode";
2103
2086
  /** Clamp a numeric knob into its sane range. */
2104
2087
  function clampSetting(key, value) {
@@ -2198,7 +2181,7 @@ window.__ModuleLoader__.load({
2198
2181
  } else if (event.key === "dsh.catppuccin.glass.enabled") {
2199
2182
  this.enabled = readEnabled();
2200
2183
  this.sync();
2201
- } else if (event.key === MODE_KEY || event.key in NUMERIC_KEYS) {
2184
+ } else if (event.key === MODE_KEY || NUMERIC_STORAGE_KEYS.includes(event.key)) {
2202
2185
  this.reloadSettings();
2203
2186
  if (this.enabled) this.applySettings();
2204
2187
  this.publish();
@@ -2488,8 +2471,10 @@ window.__ModuleLoader__.load({
2488
2471
  const { label, value, options, onSelect } = props;
2489
2472
  const [focused, setFocused] = (0, react.useState)(null);
2490
2473
  const refs = (0, react.useRef)(/* @__PURE__ */ new Map());
2474
+ const matchesValue = options.some((option) => option.id === value);
2475
+ const anchor = focused ?? (matchesValue ? value : options[0]?.id);
2491
2476
  const moveFocus = (direction) => {
2492
- const current = focused ?? value;
2477
+ const current = anchor;
2493
2478
  const index = options.findIndex((option) => option.id === current);
2494
2479
  let next;
2495
2480
  if (direction === "home") next = 0;
@@ -2533,7 +2518,7 @@ window.__ModuleLoader__.load({
2533
2518
  type: "button",
2534
2519
  className: option.id === value ? GlassRow_module_css_default.segActive : GlassRow_module_css_default.seg,
2535
2520
  "aria-pressed": option.id === value,
2536
- tabIndex: option.id === (focused ?? value) ? 0 : -1,
2521
+ tabIndex: option.id === anchor ? 0 : -1,
2537
2522
  onFocus: () => {
2538
2523
  setFocused(option.id);
2539
2524
  },
@@ -2823,13 +2808,14 @@ window.__ModuleLoader__.load({
2823
2808
  * @param props - composed slot props.
2824
2809
  * @returns the row element tree.
2825
2810
  */
2826
- function UpdateRow({ t, check, autoCheck, setAutoCheck, channel, setChannel, subscribePrefs, lastAutoResult, subscribeConflict, conflictCount }) {
2811
+ function UpdateRow({ t, check, autoCheck, setAutoCheck, channel, setChannel, subscribePrefs, lastAutoResult, subscribeConflict, conflictCount, activeLocale, subscribeLocale }) {
2827
2812
  const [phase, setPhase] = (0, react.useState)("idle");
2828
2813
  const [payload, setPayload] = (0, react.useState)(null);
2829
2814
  const [copied, setCopied] = (0, react.useState)(false);
2830
2815
  const autoCheckOn = (0, react.useSyncExternalStore)(subscribePrefs, autoCheck);
2831
2816
  const channelValue = (0, react.useSyncExternalStore)(subscribePrefs, channel);
2832
2817
  const conflicts = (0, react.useSyncExternalStore)(subscribeConflict, conflictCount);
2818
+ const locale = (0, react.useSyncExternalStore)(subscribeLocale, activeLocale);
2833
2819
  const [dismissedConflicts, setDismissedConflicts] = (0, react.useState)(0);
2834
2820
  const [retryRemaining, setRetryRemaining] = (0, react.useState)(null);
2835
2821
  const retryTimer = (0, react.useRef)(void 0);
@@ -2848,13 +2834,13 @@ window.__ModuleLoader__.load({
2848
2834
  }
2849
2835
  setRetryRemaining(null);
2850
2836
  };
2851
- const runCheck = async (withChannel = channelValue) => {
2837
+ const runCheck = async (withChannel) => {
2852
2838
  cancelRetry();
2853
2839
  setPhase("checking");
2854
2840
  setCopied(false);
2855
2841
  let next;
2856
2842
  try {
2857
- next = await check(withChannel);
2843
+ next = await check(withChannel ?? channel());
2858
2844
  } catch {
2859
2845
  next = {
2860
2846
  ok: false,
@@ -3124,7 +3110,7 @@ window.__ModuleLoader__.load({
3124
3110
  children: [
3125
3111
  t("update.checkedAt"),
3126
3112
  " ",
3127
- new Date(payload.checkedAt).toLocaleString()
3113
+ new Date(payload.checkedAt).toLocaleString(locale)
3128
3114
  ]
3129
3115
  })
3130
3116
  ]
@@ -3400,6 +3386,39 @@ window.__ModuleLoader__.load({
3400
3386
  pendingTimer = void 0;
3401
3387
  pendingWrite = void 0;
3402
3388
  }
3389
+ /**
3390
+ * Capture-once holder for the revision a debounced write is based on (audit
3391
+ * F2, 2026-09-22).
3392
+ *
3393
+ * The read-side guard in {@link persistStateToScope} only means anything if
3394
+ * `baseRevision` is the revision the LOCAL state was derived from — i.e.
3395
+ * sampled when the change was SCHEDULED. Reading it inside the flush instead
3396
+ * made the comparison a tautology: the flush and the guard's own
3397
+ * `scope.getSnapshot()` run in the same synchronous block, so the two
3398
+ * revisions are always equal and `'stale'` is unreachable. The effect was a
3399
+ * silently lost read-side protection: with two windows open, an external edit
3400
+ * landing inside the 300 ms debounce window was overwritten by the stale local
3401
+ * state and the conflict banner never appeared.
3402
+ *
3403
+ * `capture` keeps the FIRST revision of a burst (`??=`, not assignment): a
3404
+ * slider drag emits several changes, and the local state each of them is based
3405
+ * on is still the one that was current when the burst started. `take` clears
3406
+ * the slot as it reads, so the next burst starts fresh — including when the
3407
+ * flush bails out (unusable scope), where the value must not leak forward.
3408
+ */
3409
+ function createBaseRevisionTracker() {
3410
+ let pending;
3411
+ return {
3412
+ capture(read) {
3413
+ if (pending === void 0) pending = read();
3414
+ },
3415
+ take() {
3416
+ const value = pending;
3417
+ pending = void 0;
3418
+ return value;
3419
+ }
3420
+ };
3421
+ }
3403
3422
  //#endregion
3404
3423
  //#region src/client/index.ts
3405
3424
  /** Locale namespace owned by this plugin. */
@@ -3437,6 +3456,21 @@ window.__ModuleLoader__.load({
3437
3456
  return "off";
3438
3457
  }
3439
3458
  }
3459
+ /** Whether localStorage carries an EXPLICIT `off` flavour choice.
3460
+ *
3461
+ * `readFlavor()` maps both "no value" and "off" to `off`, and the two are not
3462
+ * the same thing: DSH Desktop boots the GUI on a fresh random loopback port, so
3463
+ * its localStorage starts empty on every launch and the settings document —
3464
+ * which may legitimately hold a flavour — is what should win. Only a literal
3465
+ * `off` in storage proves the user (or another window) actually turned the
3466
+ * flavour off, which is the case the cross-tab restore must act on (audit F4). */
3467
+ function readExplicitFlavorOff() {
3468
+ try {
3469
+ return localStorage.getItem(FLAVOR_STORAGE_KEY) === "off";
3470
+ } catch {
3471
+ return false;
3472
+ }
3473
+ }
3440
3474
  /** Persist the flavour choice (storage failures keep the in-memory state). */
3441
3475
  function writeFlavor(choice) {
3442
3476
  try {
@@ -3678,11 +3712,15 @@ window.__ModuleLoader__.load({
3678
3712
  };
3679
3713
  let lastWrittenSection;
3680
3714
  let handleStaleConflict;
3715
+ const baseRevisionTracker = createBaseRevisionTracker();
3716
+ const queuePersist = () => {
3717
+ baseRevisionTracker.capture(() => scope.getSnapshot().revision);
3718
+ scheduleDurablePersist(persistLocal);
3719
+ };
3681
3720
  const persistLocal = () => {
3682
- const snapshot = scope.getSnapshot();
3683
- if (!isScopeUsable(snapshot)) return;
3721
+ const baseRevision = baseRevisionTracker.take();
3722
+ if (!isScopeUsable(scope.getSnapshot())) return;
3684
3723
  const state = buildLocalState();
3685
- const baseRevision = snapshot.revision;
3686
3724
  persistStateToScope(scope, state, {
3687
3725
  baseRevision,
3688
3726
  lastWrittenSection
@@ -3723,7 +3761,11 @@ window.__ModuleLoader__.load({
3723
3761
  const applyDesired = () => {
3724
3762
  rememberBuiltinPreference(theme.getTheme().preference);
3725
3763
  const desired = readFlavor();
3726
- if (desired === "off") return;
3764
+ if (desired === "off") {
3765
+ const preference = theme.getTheme().preference;
3766
+ if (readExplicitFlavorOff() && flavorFromThemeId(preference) !== "off") scheduleRestore(readRestoredPreference());
3767
+ return;
3768
+ }
3727
3769
  const preference = theme.getTheme().preference;
3728
3770
  if (preference === desired) return;
3729
3771
  if (builtinPickWins(preference, liveBuiltinPick)) return;
@@ -3739,11 +3781,16 @@ window.__ModuleLoader__.load({
3739
3781
  try {
3740
3782
  theme.setTheme(next);
3741
3783
  } catch {}
3784
+ return;
3742
3785
  }
3786
+ if (flavorFromThemeId(theme.getTheme().preference) === "off") return;
3787
+ try {
3788
+ theme.setTheme(readRestoredPreference());
3789
+ } catch {}
3743
3790
  };
3744
3791
  window.addEventListener("storage", onStorage);
3745
3792
  const offGlass = glass.subscribe(() => {
3746
- scheduleDurablePersist(persistLocal);
3793
+ queuePersist();
3747
3794
  });
3748
3795
  const applyScopeSnapshot = () => {
3749
3796
  const snapshot = scope.getSnapshot();
@@ -3751,9 +3798,10 @@ window.__ModuleLoader__.load({
3751
3798
  if (state === null) return;
3752
3799
  const local = buildLocalState();
3753
3800
  if (snapshot.user === void 0) {
3754
- if (!isDefaultState(local)) scheduleDurablePersist(persistLocal);
3801
+ if (!isDefaultState(local)) queuePersist();
3755
3802
  return;
3756
3803
  }
3804
+ if (hasUnpersistableOverrides(snapshot.value?.overrides)) queuePersist();
3757
3805
  if (settingsSectionsEqual(settingsSectionFromState(local), settingsSectionFromState(state))) return;
3758
3806
  writeFlavor(state.flavor);
3759
3807
  glass.applyRemote(state.glass);
@@ -3817,7 +3865,7 @@ window.__ModuleLoader__.load({
3817
3865
  if (choice === "off") {
3818
3866
  writeFlavor("off");
3819
3867
  theme.setTheme(readRestoredPreference());
3820
- scheduleDurablePersist(persistLocal);
3868
+ queuePersist();
3821
3869
  return;
3822
3870
  }
3823
3871
  const flavor = flavorInfo(choice);
@@ -3825,21 +3873,21 @@ window.__ModuleLoader__.load({
3825
3873
  writeFlavor(flavor.themeId);
3826
3874
  ensureThemeRegistered(flavor.themeId);
3827
3875
  theme.setTheme(flavor.themeId);
3828
- scheduleDurablePersist(persistLocal);
3876
+ queuePersist();
3829
3877
  },
3830
3878
  overrides: overridesSnapshot,
3831
3879
  setOverrides: (overrides) => {
3832
3880
  writeOverrides(overrides);
3833
3881
  emitPrefs();
3834
3882
  reapplyThemePrefs();
3835
- scheduleDurablePersist(persistLocal);
3883
+ queuePersist();
3836
3884
  },
3837
3885
  shikiStyle: () => readShikiStyle(),
3838
3886
  setShikiStyle: (style) => {
3839
3887
  writeShikiStyle(style);
3840
3888
  emitPrefs();
3841
3889
  reapplyThemePrefs();
3842
- scheduleDurablePersist(persistLocal);
3890
+ queuePersist();
3843
3891
  },
3844
3892
  subscribePrefs: (listener) => {
3845
3893
  prefsListeners.add(listener);
@@ -3900,14 +3948,14 @@ window.__ModuleLoader__.load({
3900
3948
  setAutoCheck: (value) => {
3901
3949
  writeAutoCheck(value);
3902
3950
  emitPrefs();
3903
- scheduleDurablePersist(persistLocal);
3951
+ queuePersist();
3904
3952
  armAutoCheck?.();
3905
3953
  },
3906
3954
  channel: () => readUpdateChannel(),
3907
3955
  setChannel: (channel) => {
3908
3956
  writeUpdateChannel(channel);
3909
3957
  emitPrefs();
3910
- scheduleDurablePersist(persistLocal);
3958
+ queuePersist();
3911
3959
  },
3912
3960
  subscribePrefs: (listener) => {
3913
3961
  prefsListeners.add(listener);
@@ -3922,7 +3970,9 @@ window.__ModuleLoader__.load({
3922
3970
  conflictListeners.delete(listener);
3923
3971
  };
3924
3972
  },
3925
- conflictCount: () => conflictCount
3973
+ conflictCount: () => conflictCount,
3974
+ activeLocale: () => ctx.locale.getLocale().active,
3975
+ subscribeLocale: (listener) => ctx.locale.subscribe(listener)
3926
3976
  });
3927
3977
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({
3928
3978
  name: "settings.general.item",
@@ -3948,6 +3998,7 @@ window.__ModuleLoader__.load({
3948
3998
  exports.inject = inject;
3949
3999
  exports.overridesSnapshot = overridesSnapshot;
3950
4000
  exports.readAutoCheck = readAutoCheck;
4001
+ exports.readExplicitFlavorOff = readExplicitFlavorOff;
3951
4002
  exports.readFlavor = readFlavor;
3952
4003
  exports.readOverrides = readOverrides;
3953
4004
  exports.readRestoredPreference = readRestoredPreference;