dsh-milestone 0.6.2 → 0.6.4

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.
Files changed (3) hide show
  1. package/README.md +21 -4
  2. package/lib/client.js +1750 -170
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -682,6 +682,8 @@ window.__ModuleLoader__.load({
682
682
  "settings.title": "设置",
683
683
  /** Per-feature toggle label inside the settings menu: keep visible while collapsed. */
684
684
  "settings.pin": "在折叠外显示",
685
+ /** Settings: feature-section hint explaining what the pin switch does. */
686
+ "settings.pin.hint": "开启后,工具栏折叠时该功能仍显示在箭头旁",
685
687
  /** Settings action that clears every pinned feature. */
686
688
  "settings.reset": "恢复默认",
687
689
  /** Settings footer heading above the project links. */
@@ -702,6 +704,16 @@ window.__ModuleLoader__.load({
702
704
  "settings.section.features": "功能与快捷区",
703
705
  /** Settings modal: section heading for the personalization controls. */
704
706
  "settings.section.personal": "个性化",
707
+ /** Settings modal: section heading for the focus-mode controls (0.6.3). */
708
+ "settings.section.focus": "聚焦",
709
+ /** Settings: personalization-section hint shown inside the expanded block. */
710
+ "settings.personal.hint": "圆点、强调色与位置,即调即存",
711
+ /** Settings: aria-label on the personalization block toggle while COLLAPSED. */
712
+ "settings.personal.expand": "展开个性化设置",
713
+ /** Settings: aria-label on the personalization block toggle while EXPANDED. */
714
+ "settings.personal.collapse": "收起个性化设置",
715
+ /** Settings: live value summary on the collapsed personalization header. */
716
+ "settings.personal.summary": "{accent} · 图标 {icon}px · {side}",
705
717
  /** Settings: hover description — in-rail search. */
706
718
  "settings.desc.search": "按完整消息内容过滤并跳转到对应消息",
707
719
  /** Settings: hover description — all-prompts list. */
@@ -730,6 +742,30 @@ window.__ModuleLoader__.load({
730
742
  "settings.side.left": "左侧",
731
743
  /** Settings personalization: side radio — hug the right edge. */
732
744
  "settings.side.right": "右侧",
745
+ /** Settings: focus block — hint shown inside the expanded block. */
746
+ "settings.focus.hint": "这些选项自由组合成你的「聚焦搭配」;总开关仍是工具栏的眼睛按钮",
747
+ /** Settings: aria-label on the focus block toggle while COLLAPSED. */
748
+ "settings.focus.expand": "展开聚焦设置",
749
+ /** Settings: aria-label on the focus block toggle while EXPANDED. */
750
+ "settings.focus.collapse": "收起聚焦设置",
751
+ /** Settings: focus option — dim the think reasoning disclosures. */
752
+ "settings.focus.dimThink": "淡化 think 推理区",
753
+ /** Settings: focus option — dim the tool-call cards. */
754
+ "settings.focus.dimTools": "淡化工具调用卡片",
755
+ /** Settings: focus option — compress think disclosures to a hover strip. */
756
+ "settings.focus.collapseThink": "折叠 think",
757
+ /** Settings: focus option — dim strength slider label. */
758
+ "settings.focus.opacity": "淡化强度",
759
+ /** Settings: live value summary on the collapsed focus header. */
760
+ "settings.focus.summary": "{opts} · 强度 {opacity}%",
761
+ /** Settings: focus summary — short label for the think-dim option. */
762
+ "settings.focus.summary.think": "think 淡化",
763
+ /** Settings: focus summary — short label for the tool-dim option. */
764
+ "settings.focus.summary.tools": "工具淡化",
765
+ /** Settings: focus summary — short label for the think-collapse option. */
766
+ "settings.focus.summary.collapse": "折叠 think",
767
+ /** Settings: focus summary — placeholder when every option is off. */
768
+ "settings.focus.summary.none": "未启用",
733
769
  /** B4 update-check: toolbar button label + title/aria-label. */
734
770
  "update.check": "检查更新",
735
771
  /** B4 update-check: popover title. */
@@ -759,7 +795,73 @@ window.__ModuleLoader__.load({
759
795
  /** Language option: force Chinese copy. */
760
796
  "settings.lang.zh": "中文",
761
797
  /** Language option: force English copy. */
762
- "settings.lang.en": "English"
798
+ "settings.lang.en": "English",
799
+ /** 0.6.4 onboarding: welcome title (also the tutorial dialog's aria-label). */
800
+ "onboarding.welcome.title": "欢迎使用 dsh-milestone",
801
+ /** 0.6.4 onboarding: one-line positioning statement on the welcome page. */
802
+ "onboarding.welcome.subtitle": "一条附着在会话侧边的里程碑导航:悬停圆点看元信息,点击跳到任意提问。",
803
+ /** 0.6.4 onboarding: primary action on the welcome page → step 1. */
804
+ "onboarding.start": "开始引导",
805
+ /** 0.6.4 onboarding: skip action — persists the flag and closes, on EVERY page. */
806
+ "onboarding.skip": "跳过",
807
+ /** 0.6.4 onboarding: step-page progress text `第 n / 4 步`. */
808
+ "onboarding.step.label": "第 {n} / 4 步",
809
+ /** 0.6.4 onboarding: aria-label on one progress dot. */
810
+ "onboarding.step.short": "第 {n} 步",
811
+ /** 0.6.4 onboarding: step-page back navigation. */
812
+ "onboarding.prev": "上一步",
813
+ /** 0.6.4 onboarding: step-page forward navigation (steps 1–3). */
814
+ "onboarding.next": "下一步",
815
+ /** 0.6.4 onboarding: step-4 primary action — persists the flag and closes. */
816
+ "onboarding.finish": "开始使用",
817
+ /** 0.6.4 onboarding: step 1 title — the dot timeline. */
818
+ "onboarding.step1.title": "圆点时间线",
819
+ /** 0.6.4 onboarding: step 1 line 1. */
820
+ "onboarding.step1.desc1": "每条提问在侧边对应一个圆点,自上而下按时间排列。",
821
+ /** 0.6.4 onboarding: step 1 line 2. */
822
+ "onboarding.step1.desc2": "悬停圆点,查看该轮元信息(用时、首字、模型与 Token)。",
823
+ /** 0.6.4 onboarding: step 1 line 3. */
824
+ "onboarding.step1.desc3": "点击任意圆点,会话立即跳到那条提问。",
825
+ /** 0.6.4 onboarding: step 2 title — in-rail search & bookmarks. */
826
+ "onboarding.step2.title": "搜索与收藏",
827
+ /** 0.6.4 onboarding: step 2 line 1. */
828
+ "onboarding.step2.desc1": "输入关键词过滤圆点,只点亮匹配项并显示 N / M 计数;Enter 逐个跳转,Esc 关闭。",
829
+ /** 0.6.4 onboarding: step 2 line 2. */
830
+ "onboarding.step2.desc2": "星标收藏重要消息,再用「只看收藏」一键过滤。",
831
+ /** 0.6.4 onboarding: step 3 title — personalization & settings. */
832
+ "onboarding.step3.title": "个性化与设置",
833
+ /** 0.6.4 onboarding: step 3 line 1. */
834
+ "onboarding.step3.desc1": "调整强调色、圆点大小、距边距离与左右位置,即调即存。",
835
+ /** 0.6.4 onboarding: step 3 line 2. */
836
+ "onboarding.step3.desc2": "切换界面语言、搭配「聚焦」效果,或把常用功能 pin 在折叠外。",
837
+ /** 0.6.4 onboarding: step 4 title — update detection & support. */
838
+ "onboarding.step4.title": "更新检测与支持",
839
+ /** 0.6.4 onboarding: step 4 line 1. */
840
+ "onboarding.step4.desc1": "挂载后自动检查 npm 新版本;有新版本时工具栏会亮起提示。",
841
+ /** 0.6.4 onboarding: step 4 line 2. */
842
+ "onboarding.step4.desc2": "喜欢这个插件?欢迎 Star、提交 Issue,或通过 npm 安装升级。",
843
+ /** 0.6.4 onboarding demo: fake message 1 (step 1 dots / step 2 corpus). */
844
+ "onboarding.demo.m1": "帮我优化这段代码",
845
+ /** 0.6.4 onboarding demo: fake message 2. */
846
+ "onboarding.demo.m2": "解释这个报错",
847
+ /** 0.6.4 onboarding demo: fake message 3. */
848
+ "onboarding.demo.m3": "再检查边界条件",
849
+ /** 0.6.4 onboarding demo: fake message 4. */
850
+ "onboarding.demo.m4": "写个单元测试",
851
+ /** 0.6.4 onboarding demo: fake message 5. */
852
+ "onboarding.demo.m5": "总结一下方案",
853
+ /** 0.6.4 onboarding demo (step 1): hint under the toy dot rail. */
854
+ "onboarding.demo.hoverHint": "试试悬停与点击圆点",
855
+ /** 0.6.4 onboarding demo (step 2): hint under the toy search/favorite. */
856
+ "onboarding.demo.searchHint": "试试输入「报错」,或点星标切换只看收藏",
857
+ /** 0.6.4 onboarding demo (step 3): the 展开箭头 → 齿轮 → 设置 path hint. */
858
+ "onboarding.demo.settingsPath": "展开箭头 → 齿轮 → 设置",
859
+ /** 0.6.4 onboarding demo (step 3): legend label for the focus mix. */
860
+ "onboarding.demo.focusMix": "聚焦搭配",
861
+ /** 0.6.4 onboarding demo (step 3): legend label for pinning features. */
862
+ "onboarding.demo.pin": "pin 固定",
863
+ /** 0.6.4 settings footer action: replay the tutorial. */
864
+ "onboarding.reopen": "重新查看教程"
763
865
  };
764
866
  const en = {
765
867
  "jump.to": "Jump to message {n}",
@@ -808,6 +910,7 @@ window.__ModuleLoader__.load({
808
910
  "toolbar.settings.close": "Close settings",
809
911
  "settings.title": "Settings",
810
912
  "settings.pin": "Show outside collapse",
913
+ "settings.pin.hint": "When on, the feature stays beside the arrow while the toolbar is folded",
811
914
  "settings.reset": "Restore defaults",
812
915
  "settings.support": "Support us",
813
916
  "settings.repo": "GitHub repo",
@@ -818,6 +921,11 @@ window.__ModuleLoader__.load({
818
921
  "settings.close": "Close",
819
922
  "settings.section.features": "Features & Shortcuts",
820
923
  "settings.section.personal": "Personalization",
924
+ "settings.section.focus": "Focus",
925
+ "settings.personal.hint": "Dot size, accent color, and position — saved as you adjust",
926
+ "settings.personal.expand": "Expand personalization",
927
+ "settings.personal.collapse": "Collapse personalization",
928
+ "settings.personal.summary": "{accent} · Icon {icon}px · {side}",
821
929
  "settings.desc.search": "Filter by full message text and jump to the match",
822
930
  "settings.desc.list": "Overview of every prompt in this session",
823
931
  "settings.desc.sessionSearch": "Search across all sessions",
@@ -832,6 +940,18 @@ window.__ModuleLoader__.load({
832
940
  "settings.side": "Position",
833
941
  "settings.side.left": "Left",
834
942
  "settings.side.right": "Right",
943
+ "settings.focus.hint": "Combine these options into your own focus recipe; the eye button on the toolbar stays the master switch",
944
+ "settings.focus.expand": "Expand focus settings",
945
+ "settings.focus.collapse": "Collapse focus settings",
946
+ "settings.focus.dimThink": "Dim think reasoning",
947
+ "settings.focus.dimTools": "Dim tool call cards",
948
+ "settings.focus.collapseThink": "Collapse think",
949
+ "settings.focus.opacity": "Dim strength",
950
+ "settings.focus.summary": "{opts} · Strength {opacity}%",
951
+ "settings.focus.summary.think": "Think dim",
952
+ "settings.focus.summary.tools": "Tools dim",
953
+ "settings.focus.summary.collapse": "Think collapse",
954
+ "settings.focus.summary.none": "Off",
835
955
  "update.check": "Check updates",
836
956
  "update.title": "Update check",
837
957
  "update.current": "Current version",
@@ -846,7 +966,40 @@ window.__ModuleLoader__.load({
846
966
  "settings.language": "Language",
847
967
  "settings.lang.system": "Follow system",
848
968
  "settings.lang.zh": "Chinese",
849
- "settings.lang.en": "English"
969
+ "settings.lang.en": "English",
970
+ "onboarding.welcome.title": "Welcome to dsh-milestone",
971
+ "onboarding.welcome.subtitle": "A milestone rail beside your conversation: hover a dot for metadata, click any dot to jump.",
972
+ "onboarding.start": "Start the tour",
973
+ "onboarding.skip": "Skip",
974
+ "onboarding.step.label": "Step {n} of 4",
975
+ "onboarding.step.short": "Step {n}",
976
+ "onboarding.prev": "Back",
977
+ "onboarding.next": "Next",
978
+ "onboarding.finish": "Get started",
979
+ "onboarding.step1.title": "The dot timeline",
980
+ "onboarding.step1.desc1": "Every prompt is a dot on the side rail, listed top to bottom in time order.",
981
+ "onboarding.step1.desc2": "Hover a dot to see that turn’s metadata (duration, first token, model, tokens).",
982
+ "onboarding.step1.desc3": "Click any dot to jump the conversation straight to that prompt.",
983
+ "onboarding.step2.title": "Search & bookmarks",
984
+ "onboarding.step2.desc1": "Type to filter the dots — matches light up with an N / M counter; Enter jumps, Esc closes.",
985
+ "onboarding.step2.desc2": "Star a message to bookmark it, then use “Bookmarks only” to filter.",
986
+ "onboarding.step3.title": "Personalize & settings",
987
+ "onboarding.step3.desc1": "Tune the accent color, dot size, edge distance, and left/right side — saved as you adjust.",
988
+ "onboarding.step3.desc2": "Switch the UI language, compose your focus effect, or pin features outside the collapse.",
989
+ "onboarding.step4.title": "Updates & support",
990
+ "onboarding.step4.desc1": "A silent check runs after mount; when a new version exists, the toolbar shows a hint.",
991
+ "onboarding.step4.desc2": "Enjoying the plugin? Star us, file an issue, or upgrade via npm.",
992
+ "onboarding.demo.m1": "Help me optimize this code",
993
+ "onboarding.demo.m2": "Explain this error",
994
+ "onboarding.demo.m3": "Check the edge cases again",
995
+ "onboarding.demo.m4": "Write a unit test",
996
+ "onboarding.demo.m5": "Summarize the plan",
997
+ "onboarding.demo.hoverHint": "Try hovering and clicking the dots",
998
+ "onboarding.demo.searchHint": "Try typing “error”, or tap the star for bookmarks-only",
999
+ "onboarding.demo.settingsPath": "Expand arrow → gear → Settings",
1000
+ "onboarding.demo.focusMix": "Focus mix",
1001
+ "onboarding.demo.pin": "Pin keys",
1002
+ "onboarding.reopen": "Replay the tour"
850
1003
  };
851
1004
  /**
852
1005
  * Interpolate `{name}` placeholders with params, matching the harness t seat's
@@ -1275,6 +1428,950 @@ window.__ModuleLoader__.load({
1275
1428
  });
1276
1429
  }
1277
1430
  //#endregion
1431
+ //#region src/client/modal-tokens.ts
1432
+ /**
1433
+ * modal-tokens: the ONE source for the settings modal's dark-panel palette and
1434
+ * radius scale, shared with the 0.6.4 onboarding tutorial modal so the two
1435
+ * dialogs cannot drift (same dark panel on a dark host, same three text tiers,
1436
+ * same 12px panel / 8px control radius, same border tone).
1437
+ *
1438
+ * The static MODAL_CSS block (hover/focus-visible/reduced-motion rules) stays
1439
+ * in MilestoneRail.tsx because it also styles the settings modal's own chrome
1440
+ * (`[data-support-card]`, `[data-toolbar-*]`, ...); the onboarding modal
1441
+ * carries its own small ONBOARDING_CSS block for its own controls, built from
1442
+ * these same tokens. MilestoneRail.tsx imports these constants verbatim so a
1443
+ * future palette change edits exactly one file.
1444
+ */
1445
+ const MODAL_BG = "rgba(20, 24, 32, 0.98)";
1446
+ const MODAL_FG = "#e6e8ee";
1447
+ const MODAL_TITLE = "#c7cede";
1448
+ const MODAL_TEXT = "#b9c2d4";
1449
+ const MODAL_HINT = "#8b96ab";
1450
+ const MODAL_BORDER = "rgba(255, 255, 255, 0.14)";
1451
+ const MODAL_TIP_BG = "#222834";
1452
+ //#endregion
1453
+ //#region src/client/onboarding-store.ts
1454
+ /**
1455
+ * onboarding-store: the persistence layer for the 0.6.4 first-run tutorial —
1456
+ * a single "has the user seen it" flag with NO payload.
1457
+ *
1458
+ * Storage contract: one localStorage key (`dsh-milestone.onboarded`) whose
1459
+ * VALUE is exactly the string `'1'` when the tutorial was completed or
1460
+ * skipped. Anything else (absent key, `'0'`, `'true'`, garbage) means "never
1461
+ * shown" and re-triggers the tutorial.
1462
+ *
1463
+ * Both accessors swallow storage failures silently (SSR, sandboxed iframe,
1464
+ * quota): the tutorial is a best-effort enhancement — a broken storage must
1465
+ * never crash the rail, and a user in that environment simply sees the
1466
+ * tutorial once per page load instead of once ever.
1467
+ */
1468
+ /** The single localStorage key holding the onboarding "seen" flag. */
1469
+ const ONBOARDED_KEY = "dsh-milestone.onboarded";
1470
+ /**
1471
+ * True when the user already completed or skipped the tutorial (the stored
1472
+ * value is exactly `'1'`). Storage unavailability degrades to `false`.
1473
+ */
1474
+ function readOnboardedFlag() {
1475
+ try {
1476
+ return localStorage.getItem(ONBOARDED_KEY) === "1";
1477
+ } catch {
1478
+ return false;
1479
+ }
1480
+ }
1481
+ /**
1482
+ * Persist the tutorial as seen/completed by writing the literal string `'1'`.
1483
+ * Storage failures are swallowed — the flag simply will not survive a reload.
1484
+ */
1485
+ function writeOnboardedFlag() {
1486
+ try {
1487
+ localStorage.setItem(ONBOARDED_KEY, "1");
1488
+ } catch {}
1489
+ }
1490
+ //#endregion
1491
+ //#region src/client/version-meta.ts
1492
+ /**
1493
+ * Installed plugin version. Injected at build time as
1494
+ * `__DSH_MILESTONE_VERSION__`; falls back to `0.0.0-dev` when unbuilt.
1495
+ */
1496
+ const PLUGIN_VERSION = "0.6.4";
1497
+ /** Main repository of dsh-milestone (as referenced by README.md). */
1498
+ const PLUGIN_REPO_URL = "https://github.com/SnowCrescenter-tech/dsh-milestone";
1499
+ /** npm package page (install channel promoted in the settings menu). */
1500
+ const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-milestone";
1501
+ //#endregion
1502
+ //#region src/client/MilestoneOnboarding.tsx
1503
+ /**
1504
+ * MilestoneOnboarding: the 0.6.4 first-run tutorial modal.
1505
+ *
1506
+ * Trigger/ownership lives in MilestoneRail (mount timer + settings reopen);
1507
+ * this component is a fully SELF-CONTAINED dialog: it carries the welcome
1508
+ * page, the 4 step pages with built-in toy demos, step progress, and the
1509
+ * close paths (skip / finish / Escape), and it persists the
1510
+ * `dsh-milestone.onboarded` flag itself via onboarding-store.
1511
+ *
1512
+ * Visual language is copied from the settings modal (MilestoneRail's
1513
+ * MODAL_CSS): dark `rgba(20, 24, 32, 0.98)` panel, three text tiers, 12px
1514
+ * panel / 8px control radii, one accent (inherited through the rail root's
1515
+ * `--ms-accent*` CSS variables), and a small ONBOARDING_CSS block for the
1516
+ * hover / :focus-visible / reduced-motion rules inline styles cannot express.
1517
+ *
1518
+ * Deliberate non-features:
1519
+ * - NO outside-pointerdown dismissal (a misclick must never swallow the
1520
+ * tutorial — the requirement explicitly keeps it off).
1521
+ * - Escape ALWAYS means "skip": it persists the flag and closes.
1522
+ * - The flag is written on IMPRESSION (the welcome page showing is enough);
1523
+ * skip / finish / Escape re-assert it. A mid-tour refresh/crash never
1524
+ * re-pops — replay lives in settings 重新查看教程.
1525
+ */
1526
+ /** Step-page count — the progress indicator shape. */
1527
+ const STEP_COUNT = [
1528
+ 1,
1529
+ 2,
1530
+ 3,
1531
+ 4
1532
+ ];
1533
+ /** The onboarding modal floats above every other rail layer (settings 105). */
1534
+ const ONBOARDING_Z = 110;
1535
+ /** Fake messages the step-1/step-2 demos run on (zh AND en keys exist). */
1536
+ const DEMO_MESSAGE_KEYS = [
1537
+ "onboarding.demo.m1",
1538
+ "onboarding.demo.m2",
1539
+ "onboarding.demo.m3",
1540
+ "onboarding.demo.m4",
1541
+ "onboarding.demo.m5"
1542
+ ];
1543
+ /** Fake per-turn durations for the step-1 hover tooltip (unit text is locale-neutral). */
1544
+ const DEMO_DURATIONS = [
1545
+ "6.2s",
1546
+ "13.4s",
1547
+ "5.1s",
1548
+ "22.0s",
1549
+ "9.8s"
1550
+ ];
1551
+ /**
1552
+ * Static onboarding-modal styling that needs `:hover`/`:focus-visible`
1553
+ * (inline styles cannot express them): one accent focus ring for every
1554
+ * control, hover washes, the demo-link micro-lift, the panel fade-in, and the
1555
+ * reduced-motion kill — mirrors the settings MODAL_CSS pattern. Accent values
1556
+ * come from the rail root's CSS variables (`--ms-accent*`), inherited into
1557
+ * this fixed layer because the overlay renders inside the rail root.
1558
+ */
1559
+ const ONBOARDING_CSS = `
1560
+ @keyframes ms-onboarding-fade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } }
1561
+ [data-onboarding-panel] { animation: ms-onboarding-fade 160ms ease; }
1562
+ [data-onboarding-skip]:hover { background: rgba(255, 255, 255, 0.08); }
1563
+ [data-onboarding-prev]:hover, [data-onboarding-next]:hover, [data-onboarding-start]:hover, [data-onboarding-finish]:hover { background: rgba(255, 255, 255, 0.1); }
1564
+ [data-onboarding-skip]:focus-visible, [data-onboarding-prev]:focus-visible,
1565
+ [data-onboarding-next]:focus-visible, [data-onboarding-start]:focus-visible,
1566
+ [data-onboarding-finish]:focus-visible, [data-demo-star]:focus-visible,
1567
+ [data-demo-dot]:focus-visible, [data-demo-search-input]:focus-visible {
1568
+ box-shadow: 0 0 0 2px var(--ms-accent-soft);
1569
+ }
1570
+ [data-demo-link] { transition: transform 120ms ease, border-color 120ms ease, background 120ms ease; }
1571
+ [data-demo-link]:hover { transform: translateY(-2px); border-color: var(--ms-accent); background: rgba(255, 255, 255, 0.09); }
1572
+ @media (prefers-reduced-motion: reduce) {
1573
+ [data-onboarding-panel] { animation: none; }
1574
+ [data-onboarding-skip], [data-onboarding-prev], [data-onboarding-next],
1575
+ [data-onboarding-start], [data-onboarding-finish], [data-demo-link] { transition: none; }
1576
+ }
1577
+ `;
1578
+ /** Primary (accent) dialog action — start / next / finish. */
1579
+ const PRIMARY_BUTTON = {
1580
+ background: "var(--ms-accent-bg)",
1581
+ color: "var(--ms-accent-soft)",
1582
+ border: "1px solid var(--ms-accent)",
1583
+ borderRadius: 8,
1584
+ padding: "7px 18px",
1585
+ fontSize: 13,
1586
+ fontWeight: 600,
1587
+ cursor: "pointer"
1588
+ };
1589
+ /** Secondary dialog action — back / welcome skip. */
1590
+ const GHOST_BUTTON = {
1591
+ background: "rgba(255, 255, 255, 0.06)",
1592
+ color: MODAL_TEXT,
1593
+ border: `1px solid ${MODAL_BORDER}`,
1594
+ borderRadius: 8,
1595
+ padding: "7px 16px",
1596
+ fontSize: 12.5,
1597
+ cursor: "pointer"
1598
+ };
1599
+ /** Corner skip (step pages) — the quietest of the three button tiers. */
1600
+ const SKIP_BUTTON = {
1601
+ background: "transparent",
1602
+ color: MODAL_HINT,
1603
+ border: "none",
1604
+ padding: "4px 8px",
1605
+ borderRadius: 8,
1606
+ fontSize: 12,
1607
+ cursor: "pointer"
1608
+ };
1609
+ /** Resolve the five fake demo messages through the active locale. */
1610
+ function demoMessages(t) {
1611
+ return DEMO_MESSAGE_KEYS.map((key) => t(key));
1612
+ }
1613
+ /**
1614
+ * Step-1 demo: a toy dot rail with the REAL dot visual — accent gradient dots,
1615
+ * hover enlarges + shows a fake metadata tooltip (position + duration), click
1616
+ * marks the dot with the white ring. All local state; nothing touches real
1617
+ * session data.
1618
+ */
1619
+ function DemoDotRail({ t, accent }) {
1620
+ const messages = demoMessages(t);
1621
+ const [hoverIndex, setHoverIndex] = (0, react.useState)(null);
1622
+ const [activeIndex, setActiveIndex] = (0, react.useState)(null);
1623
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1624
+ "data-demo-dots": true,
1625
+ children: [
1626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1627
+ "data-demo-dot-row": true,
1628
+ style: {
1629
+ display: "flex",
1630
+ alignItems: "center",
1631
+ justifyContent: "center",
1632
+ gap: 22
1633
+ },
1634
+ children: messages.map((msg, i) => {
1635
+ const hovered = hoverIndex === i;
1636
+ const active = activeIndex === i;
1637
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1638
+ type: "button",
1639
+ "data-demo-dot": i,
1640
+ "data-active": active ? "true" : void 0,
1641
+ "aria-label": msg,
1642
+ "aria-pressed": active,
1643
+ onMouseEnter: () => setHoverIndex(i),
1644
+ onMouseLeave: () => setHoverIndex(null),
1645
+ onFocus: () => setHoverIndex(i),
1646
+ onBlur: () => setHoverIndex(null),
1647
+ onClick: () => setActiveIndex(i),
1648
+ style: {
1649
+ width: 36,
1650
+ height: 36,
1651
+ display: "flex",
1652
+ alignItems: "center",
1653
+ justifyContent: "center",
1654
+ background: "transparent",
1655
+ border: "none",
1656
+ padding: 0,
1657
+ cursor: "pointer",
1658
+ borderRadius: "50%"
1659
+ },
1660
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1661
+ width: 13,
1662
+ height: 13,
1663
+ borderRadius: "50%",
1664
+ background: dotColor(i, messages.length, accent),
1665
+ boxShadow: active ? "0 0 0 3px rgba(255, 255, 255, 0.9)" : hovered ? "0 0 0 3px rgba(255, 255, 255, 0.35)" : "none",
1666
+ transform: hovered || active ? "scale(1.35)" : "scale(1)",
1667
+ transition: "transform 120ms ease, box-shadow 120ms ease"
1668
+ } })
1669
+ }, msg);
1670
+ })
1671
+ }),
1672
+ hoverIndex !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1673
+ "data-demo-tooltip": true,
1674
+ style: {
1675
+ marginTop: 8,
1676
+ textAlign: "center",
1677
+ fontSize: 12,
1678
+ color: "#b9c2d4",
1679
+ lineHeight: 1.5
1680
+ },
1681
+ children: [
1682
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pos.of", {
1683
+ n: hoverIndex + 1,
1684
+ m: messages.length
1685
+ }) }),
1686
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: " · " }),
1687
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("duration.label", { name: DEMO_DURATIONS[hoverIndex] }) })
1688
+ ]
1689
+ }),
1690
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1691
+ "data-demo-hint": true,
1692
+ style: {
1693
+ marginTop: 8,
1694
+ textAlign: "center",
1695
+ fontSize: 12,
1696
+ color: MODAL_HINT
1697
+ },
1698
+ children: t("onboarding.demo.hoverHint")
1699
+ })
1700
+ ]
1701
+ });
1702
+ }
1703
+ /**
1704
+ * Step-2 demo: a toy search box that filters the demo messages (N / M count)
1705
+ * plus one bookmark star — active = solid star + "bookmarks only" row list.
1706
+ * Local state only; the real rail/search/bookmarks are untouched.
1707
+ */
1708
+ function DemoSearch({ t, accent }) {
1709
+ const messages = demoMessages(t);
1710
+ const starIndex = 1;
1711
+ const [query, setQuery] = (0, react.useState)("");
1712
+ const [favOnly, setFavOnly] = (0, react.useState)(false);
1713
+ const lower = query.trim().toLowerCase();
1714
+ const visible = favOnly ? messages.filter((_, i) => i === starIndex) : messages;
1715
+ const matches = lower === "" ? visible.length : visible.filter((m) => m.toLowerCase().includes(lower)).length;
1716
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1717
+ "data-demo-search": true,
1718
+ children: [
1719
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1720
+ style: {
1721
+ display: "flex",
1722
+ alignItems: "center",
1723
+ gap: 8
1724
+ },
1725
+ children: [
1726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1727
+ "data-demo-search-input": true,
1728
+ type: "text",
1729
+ value: query,
1730
+ onChange: (e) => setQuery(e.target.value),
1731
+ placeholder: t("search.placeholder"),
1732
+ "aria-label": t("search.label"),
1733
+ style: {
1734
+ flex: 1,
1735
+ minWidth: 0,
1736
+ padding: "6px 10px",
1737
+ borderRadius: 8,
1738
+ border: `1px solid ${MODAL_BORDER}`,
1739
+ background: "rgba(255, 255, 255, 0.05)",
1740
+ color: MODAL_FG,
1741
+ fontSize: 12.5,
1742
+ outline: "none"
1743
+ }
1744
+ }),
1745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1746
+ "data-demo-search-count": true,
1747
+ style: {
1748
+ fontSize: 12,
1749
+ color: MODAL_TEXT,
1750
+ whiteSpace: "nowrap"
1751
+ },
1752
+ children: [
1753
+ matches,
1754
+ " / ",
1755
+ visible.length
1756
+ ]
1757
+ }),
1758
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1759
+ type: "button",
1760
+ "data-demo-star": true,
1761
+ "aria-pressed": favOnly,
1762
+ "aria-label": t("bookmark.filter"),
1763
+ title: t("bookmark.filter"),
1764
+ onClick: () => setFavOnly((v) => !v),
1765
+ style: {
1766
+ width: 30,
1767
+ height: 30,
1768
+ flexShrink: 0,
1769
+ display: "flex",
1770
+ alignItems: "center",
1771
+ justifyContent: "center",
1772
+ background: favOnly ? "var(--ms-accent-bg)" : "transparent",
1773
+ border: `1px solid ${MODAL_BORDER}`,
1774
+ borderRadius: 8,
1775
+ padding: 0,
1776
+ cursor: "pointer",
1777
+ color: favOnly ? "var(--ms-accent-soft)" : MODAL_HINT
1778
+ },
1779
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1780
+ width: "14",
1781
+ height: "14",
1782
+ viewBox: "0 0 24 24",
1783
+ fill: favOnly ? "currentColor" : "none",
1784
+ stroke: "currentColor",
1785
+ strokeWidth: "2",
1786
+ strokeLinejoin: "round",
1787
+ "aria-hidden": "true",
1788
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" })
1789
+ })
1790
+ })
1791
+ ]
1792
+ }),
1793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1794
+ "data-demo-search-rows": true,
1795
+ style: {
1796
+ display: "flex",
1797
+ flexDirection: "column",
1798
+ gap: 6,
1799
+ marginTop: 10
1800
+ },
1801
+ children: visible.map((msg, i) => {
1802
+ const realIndex = favOnly ? starIndex : i;
1803
+ const lit = lower === "" || msg.toLowerCase().includes(lower);
1804
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1805
+ "data-demo-search-row": true,
1806
+ "data-index": realIndex,
1807
+ "data-lit": lit ? "true" : "false",
1808
+ style: {
1809
+ display: "flex",
1810
+ alignItems: "center",
1811
+ gap: 8,
1812
+ fontSize: 12,
1813
+ color: lit ? MODAL_FG : MODAL_HINT,
1814
+ opacity: lit ? 1 : .45,
1815
+ transition: "opacity 120ms ease"
1816
+ },
1817
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1818
+ "aria-hidden": "true",
1819
+ style: {
1820
+ width: 8,
1821
+ height: 8,
1822
+ flexShrink: 0,
1823
+ borderRadius: "50%",
1824
+ background: dotColor(realIndex, messages.length, accent)
1825
+ }
1826
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: msg })]
1827
+ }, msg);
1828
+ })
1829
+ }),
1830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1831
+ "data-demo-search-hint": true,
1832
+ style: {
1833
+ marginTop: 8,
1834
+ textAlign: "center",
1835
+ fontSize: 12,
1836
+ color: MODAL_HINT
1837
+ },
1838
+ children: t("onboarding.demo.searchHint")
1839
+ })
1840
+ ]
1841
+ });
1842
+ }
1843
+ /**
1844
+ * Step-3 demo: a static legend of the personalization controls (accent / size /
1845
+ * distance / side / language / focus mix / pin), each with a tiny aria-hidden
1846
+ * visual, plus the 展开箭头 → 齿轮 → 设置 path hint.
1847
+ */
1848
+ function DemoSettings({ t, accent }) {
1849
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1850
+ "data-demo-settings": true,
1851
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1852
+ style: {
1853
+ display: "flex",
1854
+ flexDirection: "column",
1855
+ gap: 6
1856
+ },
1857
+ children: [
1858
+ {
1859
+ label: t("settings.accent"),
1860
+ visual: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1861
+ "aria-hidden": "true",
1862
+ style: {
1863
+ width: 10,
1864
+ height: 10,
1865
+ borderRadius: "50%",
1866
+ background: accent
1867
+ }
1868
+ })
1869
+ },
1870
+ {
1871
+ label: t("settings.iconSize"),
1872
+ visual: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1873
+ "aria-hidden": "true",
1874
+ style: {
1875
+ display: "flex",
1876
+ alignItems: "flex-end",
1877
+ gap: 2
1878
+ },
1879
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1880
+ width: 7,
1881
+ height: 7,
1882
+ borderRadius: "50%",
1883
+ background: MODAL_HINT
1884
+ } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1885
+ width: 12,
1886
+ height: 12,
1887
+ borderRadius: "50%",
1888
+ background: MODAL_TEXT
1889
+ } })]
1890
+ })
1891
+ },
1892
+ {
1893
+ label: t("settings.inset"),
1894
+ visual: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1895
+ "aria-hidden": "true",
1896
+ style: {
1897
+ width: 12,
1898
+ height: 12,
1899
+ border: `1px solid ${MODAL_BORDER}`,
1900
+ borderRadius: 2
1901
+ }
1902
+ })
1903
+ },
1904
+ {
1905
+ label: t("settings.side"),
1906
+ visual: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1907
+ "aria-hidden": "true",
1908
+ style: {
1909
+ fontSize: 14,
1910
+ color: MODAL_TEXT
1911
+ },
1912
+ children: "⇄"
1913
+ })
1914
+ },
1915
+ {
1916
+ label: t("settings.language"),
1917
+ visual: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1918
+ "aria-hidden": "true",
1919
+ style: {
1920
+ fontSize: 11,
1921
+ color: MODAL_TEXT
1922
+ },
1923
+ children: "中 / EN"
1924
+ })
1925
+ },
1926
+ {
1927
+ label: t("onboarding.demo.focusMix"),
1928
+ visual: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1929
+ "aria-hidden": "true",
1930
+ width: "14",
1931
+ height: "10",
1932
+ viewBox: "0 0 24 24",
1933
+ fill: "none",
1934
+ stroke: "currentColor",
1935
+ strokeWidth: "2.5",
1936
+ strokeLinecap: "round",
1937
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1938
+ cx: "12",
1939
+ cy: "12",
1940
+ r: "3"
1941
+ })]
1942
+ })
1943
+ },
1944
+ {
1945
+ label: t("onboarding.demo.pin"),
1946
+ visual: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1947
+ "aria-hidden": "true",
1948
+ width: "12",
1949
+ height: "14",
1950
+ viewBox: "0 0 24 24",
1951
+ fill: "none",
1952
+ stroke: "currentColor",
1953
+ strokeWidth: "2",
1954
+ strokeLinecap: "round",
1955
+ strokeLinejoin: "round",
1956
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 21v-4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7 4h10l-1.2 6.5a4.6 4.6 0 0 1-7.6 0z" })]
1957
+ })
1958
+ }
1959
+ ].map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1960
+ "data-demo-legend-row": true,
1961
+ style: {
1962
+ display: "flex",
1963
+ alignItems: "center",
1964
+ gap: 10,
1965
+ fontSize: 12.5,
1966
+ color: MODAL_TEXT
1967
+ },
1968
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1969
+ style: {
1970
+ width: 56,
1971
+ flexShrink: 0,
1972
+ display: "flex",
1973
+ justifyContent: "center"
1974
+ },
1975
+ children: row.visual
1976
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: row.label })]
1977
+ }, row.label))
1978
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1979
+ "data-demo-settings-path": true,
1980
+ style: {
1981
+ marginTop: 10,
1982
+ textAlign: "center",
1983
+ fontSize: 12,
1984
+ color: MODAL_HINT
1985
+ },
1986
+ children: t("onboarding.demo.settingsPath")
1987
+ })]
1988
+ });
1989
+ }
1990
+ /** Step-4 demo: update-check pill + the four support links (repo/star/issues/npm). */
1991
+ function DemoSupport({ t }) {
1992
+ const linkStyle = {
1993
+ display: "flex",
1994
+ alignItems: "center",
1995
+ justifyContent: "center",
1996
+ padding: "7px 6px",
1997
+ borderRadius: 8,
1998
+ background: "rgba(255, 255, 255, 0.05)",
1999
+ border: `1px solid ${MODAL_BORDER}`,
2000
+ color: MODAL_TITLE,
2001
+ fontSize: 11.5,
2002
+ textDecoration: "none",
2003
+ textAlign: "center",
2004
+ lineHeight: 1.35
2005
+ };
2006
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2007
+ "data-demo-support": true,
2008
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2009
+ "data-demo-update-pill": true,
2010
+ style: {
2011
+ display: "inline-flex",
2012
+ alignItems: "center",
2013
+ gap: 6,
2014
+ padding: "4px 10px",
2015
+ borderRadius: 999,
2016
+ background: "rgba(255, 255, 255, 0.06)",
2017
+ border: `1px solid ${MODAL_BORDER}`,
2018
+ color: MODAL_TEXT,
2019
+ fontSize: 12
2020
+ },
2021
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2022
+ "aria-hidden": "true",
2023
+ style: {
2024
+ width: 6,
2025
+ height: 6,
2026
+ borderRadius: "50%",
2027
+ background: "var(--ms-accent)"
2028
+ }
2029
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
2030
+ t("update.title"),
2031
+ " · v",
2032
+ PLUGIN_VERSION
2033
+ ] })]
2034
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2035
+ "data-demo-links": true,
2036
+ style: {
2037
+ display: "grid",
2038
+ gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
2039
+ gap: 8,
2040
+ marginTop: 12
2041
+ },
2042
+ children: [
2043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2044
+ href: PLUGIN_REPO_URL,
2045
+ target: "_blank",
2046
+ rel: "noreferrer",
2047
+ "data-demo-link": true,
2048
+ "data-link": "repo",
2049
+ style: linkStyle,
2050
+ children: t("settings.repo")
2051
+ }),
2052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2053
+ href: PLUGIN_REPO_URL,
2054
+ target: "_blank",
2055
+ rel: "noreferrer",
2056
+ "data-demo-link": true,
2057
+ "data-link": "star",
2058
+ style: linkStyle,
2059
+ children: t("settings.star")
2060
+ }),
2061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2062
+ href: `${PLUGIN_REPO_URL}/issues`,
2063
+ target: "_blank",
2064
+ rel: "noreferrer",
2065
+ "data-demo-link": true,
2066
+ "data-link": "issues",
2067
+ style: linkStyle,
2068
+ children: t("settings.issues")
2069
+ }),
2070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2071
+ href: PLUGIN_NPM_URL,
2072
+ target: "_blank",
2073
+ rel: "noreferrer",
2074
+ "data-demo-link": true,
2075
+ "data-link": "npm",
2076
+ style: linkStyle,
2077
+ children: t("settings.npm")
2078
+ })
2079
+ ]
2080
+ })]
2081
+ });
2082
+ }
2083
+ /**
2084
+ * The tutorial dialog. `step` 0 = welcome; 1–4 = the step pages.
2085
+ *
2086
+ * Impressions persist: the moment the dialog mounts (welcome shown), the
2087
+ * onboarded flag is written — closing mid-tour via refresh/crash/navigation
2088
+ * never re-pops (ROADMAP 0.6.4: 印象即持久化). Every explicit close path
2089
+ * (skip anywhere, finish, Escape) re-asserts the same `'1'` and then lets the
2090
+ * parent unmount the modal.
2091
+ */
2092
+ function MilestoneOnboarding({ t, accent, onClose }) {
2093
+ const [step, setStep] = (0, react.useState)(0);
2094
+ const panelRef = (0, react.useRef)(null);
2095
+ (0, react.useEffect)(() => {
2096
+ writeOnboardedFlag();
2097
+ }, []);
2098
+ /** Skip / finish / Escape converge here: persist, then close. */
2099
+ const finish = (0, react.useCallback)(() => {
2100
+ writeOnboardedFlag();
2101
+ onClose();
2102
+ }, [onClose]);
2103
+ (0, react.useEffect)(() => {
2104
+ const onKey = (e) => {
2105
+ if (e.key !== "Escape") return;
2106
+ finish();
2107
+ };
2108
+ window.addEventListener("keydown", onKey);
2109
+ return () => window.removeEventListener("keydown", onKey);
2110
+ }, [finish]);
2111
+ (0, react.useEffect)(() => {
2112
+ panelRef.current?.querySelector("[data-onboarding-start], [data-onboarding-next], [data-onboarding-finish]")?.focus();
2113
+ }, [step]);
2114
+ const goNext = () => setStep((s) => Math.min(4, s + 1));
2115
+ const goPrev = () => setStep((s) => Math.max(1, s - 1));
2116
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2117
+ "data-onboarding-overlay": true,
2118
+ style: {
2119
+ position: "fixed",
2120
+ inset: 0,
2121
+ background: "rgba(8, 10, 15, 0.55)",
2122
+ zIndex: ONBOARDING_Z,
2123
+ display: "flex",
2124
+ alignItems: "center",
2125
+ justifyContent: "center",
2126
+ padding: 16
2127
+ },
2128
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2129
+ ref: panelRef,
2130
+ "data-onboarding-panel": true,
2131
+ role: "dialog",
2132
+ "aria-modal": "true",
2133
+ "aria-label": t("onboarding.welcome.title"),
2134
+ onClick: (e) => e.stopPropagation(),
2135
+ style: {
2136
+ width: "min(560px, 92vw)",
2137
+ maxHeight: "80vh",
2138
+ overflowY: "auto",
2139
+ padding: 20,
2140
+ background: MODAL_BG,
2141
+ color: MODAL_FG,
2142
+ borderRadius: 12,
2143
+ boxShadow: "0 24px 64px rgba(0, 0, 0, 0.55)",
2144
+ scrollbarWidth: "thin",
2145
+ scrollbarColor: "rgba(255, 255, 255, 0.2) transparent"
2146
+ },
2147
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: ONBOARDING_CSS }), step === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2148
+ "data-onboarding-welcome": true,
2149
+ children: [
2150
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2151
+ "data-onboarding-welcome-title": true,
2152
+ style: {
2153
+ fontSize: 17,
2154
+ fontWeight: 700,
2155
+ color: MODAL_FG
2156
+ },
2157
+ children: t("onboarding.welcome.title")
2158
+ }),
2159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2160
+ "data-onboarding-welcome-subtitle": true,
2161
+ style: {
2162
+ marginTop: 8,
2163
+ fontSize: 13,
2164
+ lineHeight: 1.65,
2165
+ color: MODAL_TEXT
2166
+ },
2167
+ children: t("onboarding.welcome.subtitle")
2168
+ }),
2169
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2170
+ "data-onboarding-welcome-actions": true,
2171
+ style: {
2172
+ display: "flex",
2173
+ justifyContent: "center",
2174
+ gap: 10,
2175
+ marginTop: 18
2176
+ },
2177
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2178
+ type: "button",
2179
+ "data-onboarding-start": true,
2180
+ onClick: () => setStep(1),
2181
+ style: PRIMARY_BUTTON,
2182
+ children: t("onboarding.start")
2183
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2184
+ type: "button",
2185
+ "data-onboarding-skip": true,
2186
+ onClick: finish,
2187
+ style: GHOST_BUTTON,
2188
+ children: t("onboarding.skip")
2189
+ })]
2190
+ })
2191
+ ]
2192
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2193
+ "data-onboarding-step": true,
2194
+ "data-step": step,
2195
+ children: [
2196
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2197
+ style: {
2198
+ display: "flex",
2199
+ alignItems: "flex-start",
2200
+ justifyContent: "space-between",
2201
+ gap: 10
2202
+ },
2203
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2204
+ "data-onboarding-step-title": true,
2205
+ style: {
2206
+ fontSize: 15,
2207
+ fontWeight: 600,
2208
+ color: MODAL_FG
2209
+ },
2210
+ children: [
2211
+ step === 1 && t("onboarding.step1.title"),
2212
+ step === 2 && t("onboarding.step2.title"),
2213
+ step === 3 && t("onboarding.step3.title"),
2214
+ step === 4 && t("onboarding.step4.title")
2215
+ ]
2216
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2217
+ type: "button",
2218
+ "data-onboarding-skip": true,
2219
+ onClick: finish,
2220
+ style: SKIP_BUTTON,
2221
+ children: t("onboarding.skip")
2222
+ })]
2223
+ }),
2224
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2225
+ style: {
2226
+ display: "flex",
2227
+ flexDirection: "column",
2228
+ gap: 6,
2229
+ marginTop: 10
2230
+ },
2231
+ children: [
2232
+ step === 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2234
+ "data-onboarding-step-desc": true,
2235
+ "data-desc": "1",
2236
+ children: t("onboarding.step1.desc1")
2237
+ }),
2238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2239
+ "data-onboarding-step-desc": true,
2240
+ "data-desc": "2",
2241
+ children: t("onboarding.step1.desc2")
2242
+ }),
2243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2244
+ "data-onboarding-step-desc": true,
2245
+ "data-desc": "3",
2246
+ children: t("onboarding.step1.desc3")
2247
+ })
2248
+ ] }),
2249
+ step === 2 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2250
+ "data-onboarding-step-desc": true,
2251
+ "data-desc": "1",
2252
+ children: t("onboarding.step2.desc1")
2253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2254
+ "data-onboarding-step-desc": true,
2255
+ "data-desc": "2",
2256
+ children: t("onboarding.step2.desc2")
2257
+ })] }),
2258
+ step === 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2259
+ "data-onboarding-step-desc": true,
2260
+ "data-desc": "1",
2261
+ children: t("onboarding.step3.desc1")
2262
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2263
+ "data-onboarding-step-desc": true,
2264
+ "data-desc": "2",
2265
+ children: t("onboarding.step3.desc2")
2266
+ })] }),
2267
+ step === 4 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2268
+ "data-onboarding-step-desc": true,
2269
+ "data-desc": "1",
2270
+ children: t("onboarding.step4.desc1")
2271
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2272
+ "data-onboarding-step-desc": true,
2273
+ "data-desc": "2",
2274
+ children: t("onboarding.step4.desc2")
2275
+ })] })
2276
+ ]
2277
+ }),
2278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2279
+ "data-onboarding-demo": true,
2280
+ style: {
2281
+ marginTop: 14,
2282
+ padding: 12,
2283
+ borderRadius: 8,
2284
+ background: MODAL_TIP_BG,
2285
+ border: `1px solid ${MODAL_BORDER}`
2286
+ },
2287
+ children: [
2288
+ step === 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DemoDotRail, {
2289
+ t,
2290
+ accent
2291
+ }),
2292
+ step === 2 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DemoSearch, {
2293
+ t,
2294
+ accent
2295
+ }),
2296
+ step === 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DemoSettings, {
2297
+ t,
2298
+ accent
2299
+ }),
2300
+ step === 4 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DemoSupport, { t })
2301
+ ]
2302
+ }),
2303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
2304
+ style: {
2305
+ display: "flex",
2306
+ alignItems: "center",
2307
+ justifyContent: "space-between",
2308
+ gap: 12,
2309
+ marginTop: 16
2310
+ },
2311
+ children: [
2312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2313
+ type: "button",
2314
+ "data-onboarding-prev": true,
2315
+ onClick: goPrev,
2316
+ disabled: step === 1,
2317
+ style: GHOST_BUTTON,
2318
+ children: t("onboarding.prev")
2319
+ }),
2320
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2321
+ style: {
2322
+ display: "flex",
2323
+ flexDirection: "column",
2324
+ alignItems: "center",
2325
+ gap: 6
2326
+ },
2327
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2328
+ "data-onboarding-progress": true,
2329
+ style: {
2330
+ fontSize: 12,
2331
+ color: MODAL_HINT
2332
+ },
2333
+ children: t("onboarding.step.label", { n: step })
2334
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2335
+ style: {
2336
+ display: "flex",
2337
+ gap: 5
2338
+ },
2339
+ children: STEP_COUNT.map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2340
+ "data-onboarding-progress-dot": true,
2341
+ "data-active": step === n ? "true" : void 0,
2342
+ "aria-label": t("onboarding.step.short", { n }),
2343
+ "aria-current": step === n ? "step" : void 0,
2344
+ style: {
2345
+ width: 16,
2346
+ height: 4,
2347
+ borderRadius: 2,
2348
+ background: step === n ? "var(--ms-accent)" : "rgba(255, 255, 255, 0.18)",
2349
+ transition: "background 120ms ease, width 120ms ease"
2350
+ }
2351
+ }, n))
2352
+ })]
2353
+ }),
2354
+ step < 4 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2355
+ type: "button",
2356
+ "data-onboarding-next": true,
2357
+ onClick: goNext,
2358
+ style: PRIMARY_BUTTON,
2359
+ children: t("onboarding.next")
2360
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2361
+ type: "button",
2362
+ "data-onboarding-finish": true,
2363
+ onClick: finish,
2364
+ style: PRIMARY_BUTTON,
2365
+ children: t("onboarding.finish")
2366
+ })
2367
+ ]
2368
+ })
2369
+ ]
2370
+ })]
2371
+ })
2372
+ });
2373
+ }
2374
+ //#endregion
1278
2375
  //#region src/client/MilestoneRailTooltip.tsx
1279
2376
  /**
1280
2377
  * @param props - the hovered mark + bookmark wiring (see {@link MilestoneRailTooltipProps}).
@@ -1739,11 +2836,15 @@ window.__ModuleLoader__.load({
1739
2836
  * JSON object:
1740
2837
  *
1741
2838
  * { "pinned": string[], "accent": "#rrggbb", "iconSize": number,
1742
- * "inset": number, "side": "left" | "right", "locale": "system"|"zh"|"en" }
2839
+ * "inset": number, "side": "left" | "right", "locale": "system"|"zh"|"en",
2840
+ * "focus": { "dimThink": boolean, "dimTools": boolean,
2841
+ * "collapseThink": boolean, "opacity": number } }
1743
2842
  *
1744
2843
  * Backward compatibility: the pre-personalization blob `{ "pinned": string[] }`
1745
2844
  * (and an entirely absent value) parses to the DEFAULT prefs with the new
1746
- * fields at their defaults — old users keep their pins untouched.
2845
+ * fields at their defaults — old users keep their pins untouched. The same
2846
+ * rule covers the `focus` object: a blob stored before 0.6.3 (no `focus`
2847
+ * field) gains the default focus mix.
1747
2848
  *
1748
2849
  * All reads are sanitized per field:
1749
2850
  * - `pinned`: whitelisted ids only (`TOOLBAR_PIN_IDS`), duplicates dropped,
@@ -1752,7 +2853,10 @@ window.__ModuleLoader__.load({
1752
2853
  * back to the default blue;
1753
2854
  * - `iconSize` / `inset`: finite numbers snapped to the slider step
1754
2855
  * (even values) and clamped to the slider range;
1755
- * - `side`: exactly `'left'` or `'right'`.
2856
+ * - `side`: exactly `'left'` or `'right'`;
2857
+ * - `focus`: three booleans (`dimThink` / `dimTools` / `collapseThink`)
2858
+ * defaulting to `true` / `false` / `false`, plus the dim `opacity`
2859
+ * snapped to the 0.1 step and clamped to [0.2, 0.8].
1756
2860
  *
1757
2861
  * The whitelist lives HERE (not in MilestoneRail) so the pure functions stay
1758
2862
  * dependency-free and unit-testable; MilestoneRail's feature registry keys
@@ -1776,14 +2880,27 @@ window.__ModuleLoader__.load({
1776
2880
  "updateCheck",
1777
2881
  "settings"
1778
2882
  ];
2883
+ /** The default accent (the classic milestone blue). */
2884
+ const DEFAULT_ACCENT = "#4d7cfd";
2885
+ /** Slider domain for the focus dim strength (0.2 = 20% .. 0.8 = 80%). */
2886
+ const FOCUS_OPACITY_MIN = .2;
2887
+ const FOCUS_OPACITY_MAX = .8;
2888
+ /** The canonical default focus mix (dim think at 40%; tools/collapse off). */
2889
+ const DEFAULT_FOCUS_PREFS = {
2890
+ dimThink: true,
2891
+ dimTools: false,
2892
+ collapseThink: false,
2893
+ opacity: .4
2894
+ };
1779
2895
  /** The canonical default prefs ("恢复默认" target; also the read fallback). */
1780
2896
  const DEFAULT_PREFS = {
1781
2897
  pinned: [],
1782
- accent: "#4d7cfd",
2898
+ accent: DEFAULT_ACCENT,
1783
2899
  iconSize: 28,
1784
2900
  inset: 14,
1785
2901
  side: "right",
1786
- locale: "system"
2902
+ locale: "system",
2903
+ focus: { ...DEFAULT_FOCUS_PREFS }
1787
2904
  };
1788
2905
  /** Type guard for registry ids — unknown strings never survive a parse. */
1789
2906
  function isToolbarPinId(id) {
@@ -1811,11 +2928,36 @@ window.__ModuleLoader__.load({
1811
2928
  const snapped = Math.round(Math.min(max, Math.max(min, value)) / step) * step;
1812
2929
  return Math.min(max, Math.max(min, snapped));
1813
2930
  }
2931
+ /** Boolean sanitizer for the focus mix flags: only `true`/`false` survive. */
2932
+ function sanitizeBoolean(value, fallback) {
2933
+ return typeof value === "boolean" ? value : fallback;
2934
+ }
2935
+ /**
2936
+ * Focus-mix sanitizer: `dimTools`/`collapseThink` default off, `dimThink`
2937
+ * defaults ON (the classic pre-0.6.3 behavior), and `opacity` snaps to the
2938
+ * 0.1 step inside [0.2, 0.8]. Snapping works in tenths (step × 10 = integer)
2939
+ * instead of a raw division so hand-edited floats like `0.30000000000000004`
2940
+ * always converge on exactly `0.3`.
2941
+ */
2942
+ function sanitizeFocus(raw) {
2943
+ if (typeof raw !== "object" || raw === null) return { ...DEFAULT_FOCUS_PREFS };
2944
+ const { dimThink, dimTools, collapseThink, opacity } = raw;
2945
+ const clamped = typeof opacity === "number" && Number.isFinite(opacity) ? Math.min(FOCUS_OPACITY_MAX, Math.max(FOCUS_OPACITY_MIN, opacity)) : DEFAULT_FOCUS_PREFS.opacity;
2946
+ const tenths = Math.round(clamped * 10);
2947
+ const snapped = Math.min(FOCUS_OPACITY_MAX, Math.max(FOCUS_OPACITY_MIN, tenths / 10));
2948
+ return {
2949
+ dimThink: sanitizeBoolean(dimThink, DEFAULT_FOCUS_PREFS.dimThink),
2950
+ dimTools: sanitizeBoolean(dimTools, DEFAULT_FOCUS_PREFS.dimTools),
2951
+ collapseThink: sanitizeBoolean(collapseThink, DEFAULT_FOCUS_PREFS.collapseThink),
2952
+ opacity: snapped
2953
+ };
2954
+ }
1814
2955
  /**
1815
2956
  * Parse + sanitize the raw persisted blob: `null` (nothing stored), invalid
1816
2957
  * JSON, or a non-object shape all degrade to the DEFAULT prefs. Each field is
1817
2958
  * sanitized independently, so a half-corrupt blob keeps its valid parts
1818
- * (e.g. an old `{pinned}`-only blob gains the default accent/size/inset/side).
2959
+ * (e.g. an old `{pinned}`-only blob gains the default accent/size/inset/side
2960
+ * AND the default focus mix).
1819
2961
  */
1820
2962
  function parsePrefs(raw) {
1821
2963
  if (raw === null) return { ...DEFAULT_PREFS };
@@ -1826,14 +2968,15 @@ window.__ModuleLoader__.load({
1826
2968
  return { ...DEFAULT_PREFS };
1827
2969
  }
1828
2970
  if (typeof parsed !== "object" || parsed === null) return { ...DEFAULT_PREFS };
1829
- const { pinned, accent, iconSize, inset, side, locale } = parsed;
2971
+ const { pinned, accent, iconSize, inset, side, locale, focus } = parsed;
1830
2972
  return {
1831
2973
  pinned: sanitizePinned(pinned),
1832
2974
  accent: typeof accent === "string" && isHexColor(accent) ? accent.toLowerCase() : DEFAULT_PREFS.accent,
1833
2975
  iconSize: clampStep(iconSize, 20, 36, 2, DEFAULT_PREFS.iconSize),
1834
2976
  inset: clampStep(inset, 0, 40, 2, DEFAULT_PREFS.inset),
1835
2977
  side: side === "left" || side === "right" ? side : DEFAULT_PREFS.side,
1836
- locale: locale === "zh" || locale === "en" || locale === "system" ? locale : DEFAULT_PREFS.locale
2978
+ locale: locale === "zh" || locale === "en" || locale === "system" ? locale : DEFAULT_PREFS.locale,
2979
+ focus: sanitizeFocus(focus)
1837
2980
  };
1838
2981
  }
1839
2982
  /**
@@ -2174,13 +3317,6 @@ window.__ModuleLoader__.load({
2174
3317
  return fresh;
2175
3318
  }
2176
3319
  //#endregion
2177
- //#region src/client/version-meta.ts
2178
- /**
2179
- * Installed plugin version. Injected at build time as
2180
- * `__DSH_MILESTONE_VERSION__`; falls back to `0.0.0-dev` when unbuilt.
2181
- */
2182
- const PLUGIN_VERSION = "0.6.2";
2183
- //#endregion
2184
3320
  //#region src/client/MilestoneRail.tsx
2185
3321
  /**
2186
3322
  * MilestoneRail: the milestone.rail entry (session scope). Renders a fixed
@@ -2224,6 +3360,13 @@ window.__ModuleLoader__.load({
2224
3360
  * hover descriptions, the personalization controls, and the support-us card
2225
3361
  * grid); everything the modal changes persists under `dsh-milestone.toolbar`.
2226
3362
  */
3363
+ /**
3364
+ * Settings modal shared palette + geometry — ONE source in modal-tokens.ts,
3365
+ * also consumed by the 0.6.4 onboarding tutorial modal so the two dialogs
3366
+ * cannot drift. Dark panel on a dark host, three text tiers (primary /
3367
+ * section title / hint + muted), one border tone, a 12px panel / 8px control
3368
+ * radius scale and a 4-unit spacing scale (4 / 8 / 12 / 16 / 20).
3369
+ */
2227
3370
  /** Minimum user messages before the rail adds value. */
2228
3371
  const MIN_MARKS = 2;
2229
3372
  const PREVIEW_LENGTH = 80;
@@ -2266,39 +3409,161 @@ window.__ModuleLoader__.load({
2266
3409
  /** Tooltip anchor width: its maxWidth cap, so a tooltip never overlaps the rail. */
2267
3410
  const TOOLTIP_ANCHOR_WIDTH = 300;
2268
3411
  /**
2269
- * P3 focus mode: dims the harness's AI thinking/scratchpad blocks so the
2270
- * conversation reads cleaner. The rule targets the stable, un-hashed
2271
- * `data-variant="think"` attribute on the thinking-block ROOT (the harness
2272
- * renders it as `data-variant="think"` with `data-state="running|ok"`), so an
2273
- * overlay plugin can dim it with plain CSS. Hovering a dimmed block (or
2274
- * opening it, `[data-open]`) restores full opacity. Kept in an inline
2275
- * <style> so the plugin stays zero-asset.
3412
+ * P3 focus mode (0.6.3: user-tuned "聚焦搭配"): when the eye toggle is armed,
3413
+ * an inline <style> (zero-asset, same pattern as the original FOCUS_CSS)
3414
+ * dims/collapses the harness content classes the USER opted into, at the
3415
+ * strength the user picked. Which content to dim and whether to additionally
3416
+ * collapse think is a persisted `prefs.focus` mix; the master on/off switch
3417
+ * stays the toolbar eye button.
3418
+ *
3419
+ * STABLE SELECTORS — researched against the rc.2 official build products:
3420
+ *
3421
+ * - think: `dsh-client-ui-conversation` renders every assistant reasoning
3422
+ * disclosure as a root `div[data-variant="think"][data-state="running|ok"]`
3423
+ * (ReasoningRow). The reasoning body lives INSIDE that root — the
3424
+ * DisclosureRow's children — so the root is a single clampable container:
3425
+ * CSS `max-height` + `overflow: hidden` collapses exactly the body while
3426
+ * the header row ("Think · summary") stays visible. → collapseThink is
3427
+ * feasible with pure CSS (no JS, no harness-internal interaction).
3428
+ *
3429
+ * - tool calls: `dsh-client-ui-tool` wraps EVERY atomic call (all ToolRow
3430
+ * presentation variants AND the bash-sample row) in
3431
+ * `div[data-chat-call-id]` (with `data-chat-anchor-key="call:<callId>"`);
3432
+ * no other card type in the harness uses that attribute (verified across
3433
+ * the whole @deepseek-ai install). → `[data-chat-call-id]` is the stable,
3434
+ * variant-independent tool-call-card selector.
3435
+ *
3436
+ * Hover/open restore mirrors the classic rule: `:hover` restores, and the
3437
+ * DisclosureRow inside the collapsed target sets `[data-open]` when the user
3438
+ * opens it (the DESCENDANT form `target [data-open]` is required — same as
3439
+ * pre-0.6.3). The collapse strip releases on hover AND on `[data-open]` so
3440
+ * an opened think disclosure never stays crushed.
3441
+ */
3442
+ const FOCUS_SELECTOR_THINK = "[data-variant=\"think\"]";
3443
+ const FOCUS_SELECTOR_TOOL = "[data-chat-call-id]";
3444
+ /** Restored height when hovering/opening a collapsed think disclosure. */
3445
+ const FOCUS_THINK_MAX_HEIGHT = "78vh";
3446
+ /**
3447
+ * Compose the focus-mode stylesheet from the persisted focus mix. Pure —
3448
+ * exported so unit tests can pin the exact rule text. `''` when no option is
3449
+ * armed (the master switch simply injects nothing).
2276
3450
  */
2277
- const FOCUS_CSS = `[data-variant="think"] { opacity: 0.4; transition: opacity 0.2s; }
2278
- [data-variant="think"]:hover, [data-variant="think"] [data-open] { opacity: 1; }`;
3451
+ function buildFocusCss(focus) {
3452
+ const { dimThink, dimTools, collapseThink } = focus;
3453
+ const strength = focus.opacity.toFixed(1);
3454
+ const rules = [];
3455
+ if (dimThink || collapseThink) {
3456
+ const decls = [];
3457
+ if (dimThink) decls.push(`opacity: ${strength}`);
3458
+ if (collapseThink) decls.push(`max-height: 36px`, "overflow: hidden");
3459
+ rules.push(`${FOCUS_SELECTOR_THINK} { ${decls.join("; ")}; transition: opacity 0.2s${collapseThink ? ", max-height 0.2s" : ""}; }`);
3460
+ rules.push(`${FOCUS_SELECTOR_THINK}:hover, ${FOCUS_SELECTOR_THINK} [data-open] { opacity: 1;${collapseThink ? ` max-height: ${FOCUS_THINK_MAX_HEIGHT};` : ""} }`);
3461
+ }
3462
+ if (dimTools) {
3463
+ rules.push(`${FOCUS_SELECTOR_TOOL} { opacity: ${strength}; transition: opacity 0.2s; }`);
3464
+ rules.push(`${FOCUS_SELECTOR_TOOL}:hover, ${FOCUS_SELECTOR_TOOL} [data-open] { opacity: 1; }`);
3465
+ }
3466
+ return rules.join("\n");
3467
+ }
3468
+ /** Near-row description tip: how far its right edge sits from the row's right
3469
+ * edge — clears the 32px switch + its 10px padding. */
3470
+ const MODAL_TIP_RIGHT = 54;
2279
3471
  /**
2280
- * Static settings-modal styling that needs `:hover` (which inline styles
2281
- * cannot express): the support-us card grid (micro-lift + accent highlight)
2282
- * and the pin-row hover wash. Accent values come from the rail root's CSS
2283
- * variables (`--ms-accent`), so one block serves every accent. The
3472
+ * Static settings-modal styling that needs `:hover`/`:focus-visible` (which
3473
+ * inline styles cannot express): the support-us card grid (micro-lift +
3474
+ * accent highlight), the pin-row / personal-header hover washes, one accent
3475
+ * focus ring for every modal control, the near-row tip's little arrow, the
3476
+ * personalization body's reveal, and a themed thin scrollbar. Accent values
3477
+ * come from the rail root's CSS variables (`--ms-accent*`), so one block
3478
+ * serves every accent. Inline styles keep these elements background-free
3479
+ * (except where noted) so the `:hover` washes below actually win. The
2284
3480
  * search-toggle recolor rule makes the EXTERNAL RailSearchUi chrome follow
2285
3481
  * the accent too (that file is owned by an earlier phase and cannot change);
2286
- * `!important` is required because the toggle's own inline styles win
3482
+ * `!important` is required because that toggle's own inline styles win
2287
3483
  * otherwise.
2288
3484
  */
2289
3485
  const MODAL_CSS = `
2290
3486
  [data-support-card] {
2291
3487
  display: flex; align-items: center; justify-content: center; gap: 8px;
2292
3488
  padding: 10px 12px; border-radius: 8px;
2293
- background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.12);
3489
+ background: rgba(255, 255, 255, 0.05); border: 1px solid ${MODAL_BORDER};
2294
3490
  color: #c7cede; text-decoration: none; font-size: 12.5px; line-height: 1.4;
2295
3491
  transition: transform 120ms ease, border-color 120ms ease, background 120ms ease;
2296
3492
  }
2297
3493
  [data-support-card]:hover { transform: translateY(-2px); border-color: var(--ms-accent); background: rgba(255, 255, 255, 0.09); }
3494
+ /* Row and header washes (the inline styles deliberately leave backgrounds
3495
+ unset so these rules win over the default padding-box background). */
2298
3496
  [data-toolbar-pin-toggle]:hover, [data-toolbar-pin-toggle]:focus-visible { background: rgba(255, 255, 255, 0.06); }
3497
+ [data-personal-toggle]:hover, [data-focus-toggle-settings]:hover { background: rgba(255, 255, 255, 0.05); }
3498
+ [data-focus-option]:hover { background: rgba(255, 255, 255, 0.04); }
3499
+ [data-toolbar-settings-close]:hover { background: rgba(255, 255, 255, 0.08); }
3500
+ [data-toolbar-settings-reset], [data-onboarding-reopen] { background: rgba(255, 255, 255, 0.06); }
3501
+ [data-toolbar-settings-reset]:hover, [data-onboarding-reopen]:hover { background: rgba(255, 255, 255, 0.1); }
3502
+ /* ONE accent ring for keyboard focus on every modal control. */
3503
+ [data-toolbar-pin-toggle]:focus-visible, [data-personal-toggle]:focus-visible,
3504
+ [data-focus-toggle-settings]:focus-visible,
3505
+ [data-toolbar-settings-close]:focus-visible, [data-toolbar-settings-reset]:focus-visible,
3506
+ [data-onboarding-reopen]:focus-visible {
3507
+ box-shadow: 0 0 0 2px var(--ms-accent-soft);
3508
+ }
3509
+ /* Near-row description tip: a rotated square peeks out of the LEFT edge so
3510
+ the apex points back at the row's label. */
3511
+ [data-settings-tip]::before {
3512
+ content: ''; position: absolute; left: -3px; top: 50%;
3513
+ width: 7px; height: 7px; transform: translateY(-50%) rotate(45deg);
3514
+ background: ${MODAL_TIP_BG};
3515
+ border-left: 1px solid ${MODAL_BORDER};
3516
+ border-bottom: 1px solid ${MODAL_BORDER};
3517
+ }
3518
+ /* Tip + chevron motion lives here (not inline) so reduced-motion can kill it. */
3519
+ [data-settings-tip] { transition: opacity 140ms ease, transform 140ms ease, visibility 140ms; }
3520
+ [data-personal-toggle] svg, [data-focus-toggle-settings] svg { transition: transform 150ms ease; }
3521
+ /* One authored reveal: the personalization/focus bodies fade in on expand. */
3522
+ @keyframes ms-settings-fade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } }
3523
+ [data-settings-personal-body], [data-settings-focus-body] { animation: ms-settings-fade 140ms ease; }
3524
+ /* Thin themed scrollbar for the scrollable modal panel. */
3525
+ [data-toolbar-settings-panel]::-webkit-scrollbar { width: 10px; }
3526
+ [data-toolbar-settings-panel]::-webkit-scrollbar-thumb {
3527
+ background: rgba(255, 255, 255, 0.16); border-radius: 6px; border: 3px solid transparent; background-clip: padding-box;
3528
+ }
3529
+ [data-toolbar-settings-panel]::-webkit-scrollbar-track { background: transparent; }
3530
+ @media (prefers-reduced-motion: reduce) {
3531
+ [data-settings-tip], [data-personal-toggle] svg, [data-focus-toggle-settings] svg, [data-support-card] { transition: none; }
3532
+ [data-settings-personal-body], [data-settings-focus-body] { animation: none; }
3533
+ }
2299
3534
  [data-search-toggle] { color: #8b96ab !important; }
2300
3535
  [data-search-toggle][aria-pressed="true"] { background: var(--ms-accent-bg) !important; color: var(--ms-accent-soft) !important; }
2301
3536
  `;
3537
+ /**
3538
+ * Shared collapsible-section chrome (personalization + focus blocks): the
3539
+ * header button (chevron + title + live summary) and the summary text span.
3540
+ * One source so the two settings blocks cannot drift.
3541
+ */
3542
+ const SECTION_TOGGLE_STYLE = {
3543
+ display: "flex",
3544
+ alignItems: "center",
3545
+ gap: 8,
3546
+ width: "100%",
3547
+ padding: "8px 10px",
3548
+ border: "none",
3549
+ borderRadius: 8,
3550
+ cursor: "pointer",
3551
+ color: MODAL_TITLE,
3552
+ fontSize: 13,
3553
+ fontWeight: 600,
3554
+ textAlign: "left"
3555
+ };
3556
+ const SECTION_SUMMARY_STYLE = {
3557
+ flex: 1,
3558
+ minWidth: 0,
3559
+ textAlign: "right",
3560
+ fontWeight: 400,
3561
+ fontSize: 12,
3562
+ color: MODAL_HINT,
3563
+ overflow: "hidden",
3564
+ textOverflow: "ellipsis",
3565
+ whiteSpace: "nowrap"
3566
+ };
2302
3567
  /**
2303
3568
  * P3 deep links (`#msg=<anchor-key>`): initial delay before the first
2304
3569
  * deep-link attempt — the harness scrolls the conversation to the bottom on
@@ -2314,6 +3579,9 @@ window.__ModuleLoader__.load({
2314
3579
  /** B4 update-check: mount-time silent check delay (ms) — give the harness
2315
3580
  * time to settle before hitting the registry. */
2316
3581
  const UPDATE_CHECK_MOUNT_DELAY = 1500;
3582
+ /** 0.6.4 first-run tutorial: mount-time show delay (ms) — let the rail settle
3583
+ * before the onboarding modal pops. */
3584
+ const ONBOARDING_MOUNT_DELAY = 800;
2317
3585
  /** Initial state: no check has completed yet, nothing to show. */
2318
3586
  const NO_UPDATE_CHECK = {
2319
3587
  phase: "idle",
@@ -2501,6 +3769,28 @@ window.__ModuleLoader__.load({
2501
3769
  return next;
2502
3770
  });
2503
3771
  };
3772
+ /** 0.6.3: patch ONE focus-mix flag/strength (nested field, same write-through). */
3773
+ const updateFocus = (patch) => {
3774
+ updatePrefs({ focus: {
3775
+ ...prefs.focus,
3776
+ ...patch
3777
+ } });
3778
+ };
3779
+ /**
3780
+ * 0.6.3: the focus block's live summary — the armed options joined into a
3781
+ * "聚焦搭配" line, plus the strength percentage. e.g. `think 淡化 · 强度 40%`.
3782
+ */
3783
+ const focusSummary = (() => {
3784
+ const parts = [
3785
+ prefs.focus.dimThink ? t("settings.focus.summary.think") : null,
3786
+ prefs.focus.dimTools ? t("settings.focus.summary.tools") : null,
3787
+ prefs.focus.collapseThink ? t("settings.focus.summary.collapse") : null
3788
+ ].filter((part) => part !== null);
3789
+ return t("settings.focus.summary", {
3790
+ opts: parts.length > 0 ? parts.join(" · ") : t("settings.focus.summary.none"),
3791
+ opacity: Math.round(prefs.focus.opacity * 100)
3792
+ });
3793
+ })();
2504
3794
  /** B1: flip one feature's pin — state and the persisted blob update together. */
2505
3795
  const onTogglePin = (id) => {
2506
3796
  setPrefs((prev) => {
@@ -2519,9 +3809,16 @@ window.__ModuleLoader__.load({
2519
3809
  const [expandHovered, setExpandHovered] = (0, react.useState)(false);
2520
3810
  const [settingsHovered, setSettingsHovered] = (0, react.useState)(false);
2521
3811
  const [settingsOpen, setSettingsOpen] = (0, react.useState)(false);
2522
- /** The feature whose description the settings modal's right pane shows
2523
- * (defaults to the first registry feature 'search'). */
2524
- const [descFeature, setDescFeature] = (0, react.useState)("search");
3812
+ const [onboardingOpen, setOnboardingOpen] = (0, react.useState)(false);
3813
+ /** The feature whose near-row description tip is currently visible
3814
+ * (`null` = none — tips only appear on hover/focus of their own row). */
3815
+ const [descFeature, setDescFeature] = (0, react.useState)(null);
3816
+ /** B-design: the personalization block collapsess by default so only a
3817
+ * value summary leads the section; expanding reveals the controls. */
3818
+ const [personalOpen, setPersonalOpen] = (0, react.useState)(false);
3819
+ /** B-design (0.6.3): the focus block mirrors the personalization block —
3820
+ * collapsed by default, the header leads with a live option summary. */
3821
+ const [focusOpen, setFocusOpen] = (0, react.useState)(false);
2525
3822
  const settingsRef = (0, react.useRef)(null);
2526
3823
  const settingsBtnRef = (0, react.useRef)(null);
2527
3824
  const [updateOpen, setUpdateOpen] = (0, react.useState)(false);
@@ -2592,6 +3889,12 @@ window.__ModuleLoader__.load({
2592
3889
  const timer = window.setTimeout(runUpdateCheck, UPDATE_CHECK_MOUNT_DELAY);
2593
3890
  return () => window.clearTimeout(timer);
2594
3891
  }, []);
3892
+ (0, react.useEffect)(() => {
3893
+ const timer = window.setTimeout(() => {
3894
+ if (!readOnboardedFlag()) setOnboardingOpen(true);
3895
+ }, ONBOARDING_MOUNT_DELAY);
3896
+ return () => window.clearTimeout(timer);
3897
+ }, []);
2595
3898
  useOutsideDismiss(updatePanelRef, updateOpen, () => {
2596
3899
  setUpdateOpen(false);
2597
3900
  updateBtnRef.current?.focus();
@@ -2755,6 +4058,13 @@ window.__ModuleLoader__.load({
2755
4058
  setSettingsOpen(false);
2756
4059
  settingsBtnRef.current?.focus();
2757
4060
  };
4061
+ /** 0.6.4: settings → 重新查看教程 — close settings and replay the tutorial
4062
+ * immediately (the flag may or may not be set; skipping/completing it
4063
+ * re-persists the flag anyway). */
4064
+ const reopenOnboarding = () => {
4065
+ setSettingsOpen(false);
4066
+ setOnboardingOpen(true);
4067
+ };
2758
4068
  /** B1: a feature renders while the toolbar is EXPANDED or while it is pinned. */
2759
4069
  const featureVisible = (id) => toolbarExpanded || pinned.includes(id);
2760
4070
  /** Base chrome-button style: accent-defined active tint, scaled hit area. */
@@ -3130,27 +4440,25 @@ window.__ModuleLoader__.load({
3130
4440
  forkAt(mark.seq).then(() => setForkedKey(mark.key));
3131
4441
  };
3132
4442
  const showLoadOlder = hasMore && marks.length >= MIN_MARKS;
3133
- const railStyle = {
3134
- position: "fixed",
3135
- top: railBox.top,
3136
- ...side === "left" ? { left: railBox.left } : { right: railBox.right },
3137
- height: railBox.height,
3138
- width: hit,
3139
- pointerEvents: "auto",
3140
- zIndex: 100,
3141
- display: "flex",
3142
- flexDirection: "column",
3143
- gap: 6,
3144
- paddingTop: 6,
3145
- "--ms-accent": accent,
3146
- "--ms-accent-soft": accentSoft,
3147
- "--ms-accent-bg": accentBg,
3148
- "--ms-icon": `${iconSize}px`,
3149
- "--ms-inset": `${inset}px`
3150
- };
3151
- const descKey = `settings.desc.${descFeature}`;
3152
4443
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3153
- style: railStyle,
4444
+ style: {
4445
+ position: "fixed",
4446
+ top: railBox.top,
4447
+ ...side === "left" ? { left: railBox.left } : { right: railBox.right },
4448
+ height: railBox.height,
4449
+ width: hit,
4450
+ pointerEvents: "auto",
4451
+ zIndex: 100,
4452
+ display: "flex",
4453
+ flexDirection: "column",
4454
+ gap: 6,
4455
+ paddingTop: 6,
4456
+ "--ms-accent": accent,
4457
+ "--ms-accent-soft": accentSoft,
4458
+ "--ms-accent-bg": accentBg,
4459
+ "--ms-icon": `${iconSize}px`,
4460
+ "--ms-inset": `${inset}px`
4461
+ },
3154
4462
  "aria-label": t("rail.label"),
3155
4463
  "data-focus-active": focusActive ? "true" : void 0,
3156
4464
  "data-accent": accent,
@@ -3159,7 +4467,7 @@ window.__ModuleLoader__.load({
3159
4467
  "data-inset": String(inset),
3160
4468
  children: [
3161
4469
  pulseCss !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: pulseCss }),
3162
- focusActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: FOCUS_CSS }),
4470
+ focusActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: buildFocusCss(prefs.focus) }),
3163
4471
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: MODAL_CSS }),
3164
4472
  showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3165
4473
  type: "button",
@@ -3239,11 +4547,13 @@ window.__ModuleLoader__.load({
3239
4547
  width: "min(600px, 92vw)",
3240
4548
  maxHeight: "78vh",
3241
4549
  overflowY: "auto",
3242
- padding: "16px 18px",
4550
+ padding: 20,
3243
4551
  background: "rgba(20, 24, 32, 0.98)",
3244
4552
  color: "#e6e8ee",
3245
4553
  borderRadius: 12,
3246
- boxShadow: "0 20px 60px rgba(0, 0, 0, 0.5)"
4554
+ boxShadow: "0 24px 64px rgba(0, 0, 0, 0.55)",
4555
+ scrollbarWidth: "thin",
4556
+ scrollbarColor: "rgba(255, 255, 255, 0.2) transparent"
3247
4557
  },
3248
4558
  children: [
3249
4559
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3252,7 +4562,7 @@ window.__ModuleLoader__.load({
3252
4562
  alignItems: "center",
3253
4563
  justifyContent: "space-between",
3254
4564
  gap: 8,
3255
- marginBottom: 12
4565
+ marginBottom: 18
3256
4566
  },
3257
4567
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3258
4568
  "data-toolbar-settings-title": true,
@@ -3269,44 +4579,57 @@ window.__ModuleLoader__.load({
3269
4579
  title: t("settings.close"),
3270
4580
  onClick: closeSettings,
3271
4581
  style: {
3272
- width: 26,
3273
- height: 26,
4582
+ width: 28,
4583
+ height: 28,
3274
4584
  flexShrink: 0,
3275
4585
  display: "flex",
3276
4586
  alignItems: "center",
3277
4587
  justifyContent: "center",
3278
- background: "transparent",
3279
4588
  border: "none",
3280
4589
  padding: 0,
3281
4590
  cursor: "pointer",
3282
4591
  color: "#8b96ab",
3283
- borderRadius: 6,
3284
- fontSize: 14,
4592
+ borderRadius: 8,
3285
4593
  lineHeight: 1
3286
4594
  },
3287
- children: ""
4595
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4596
+ width: "14",
4597
+ height: "14",
4598
+ viewBox: "0 0 24 24",
4599
+ fill: "none",
4600
+ stroke: "currentColor",
4601
+ strokeWidth: "2",
4602
+ strokeLinecap: "round",
4603
+ "aria-hidden": "true",
4604
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 6 6 18" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 6 12 12" })]
4605
+ })
3288
4606
  })]
3289
4607
  }),
3290
4608
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3291
4609
  "data-settings-section": true,
3292
- style: { marginBottom: 14 },
3293
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3294
- "data-settings-section-title": true,
3295
- style: {
3296
- fontSize: 13,
3297
- fontWeight: 600,
3298
- color: "#c7cede",
3299
- marginBottom: 8
3300
- },
3301
- children: t("settings.section.features")
3302
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3303
- style: {
3304
- display: "grid",
3305
- gridTemplateColumns: "minmax(0, 1fr) 190px",
3306
- gap: 12,
3307
- alignItems: "stretch"
3308
- },
3309
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4610
+ style: { marginBottom: 20 },
4611
+ children: [
4612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4613
+ "data-settings-section-title": true,
4614
+ style: {
4615
+ fontSize: 13,
4616
+ fontWeight: 600,
4617
+ color: "#c7cede",
4618
+ marginBottom: 4
4619
+ },
4620
+ children: t("settings.section.features")
4621
+ }),
4622
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4623
+ "data-settings-pin-hint": true,
4624
+ style: {
4625
+ fontSize: 12,
4626
+ color: "#8b96ab",
4627
+ lineHeight: 1.5,
4628
+ marginBottom: 10
4629
+ },
4630
+ children: t("settings.pin.hint")
4631
+ }),
4632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3310
4633
  style: {
3311
4634
  display: "flex",
3312
4635
  flexDirection: "column",
@@ -3314,92 +4637,163 @@ window.__ModuleLoader__.load({
3314
4637
  },
3315
4638
  children: toolbarFeatures.map((feature) => {
3316
4639
  const checked = pinned.includes(feature.id);
3317
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3318
- type: "button",
3319
- role: "switch",
3320
- "data-toolbar-pin-toggle": true,
3321
- "data-pin-id": feature.id,
3322
- "aria-checked": checked,
3323
- "aria-label": t(feature.labelKey),
3324
- onMouseEnter: () => setDescFeature(feature.id),
3325
- onFocus: () => setDescFeature(feature.id),
3326
- onClick: () => onTogglePin(feature.id),
3327
- style: {
3328
- display: "flex",
3329
- alignItems: "center",
3330
- justifyContent: "space-between",
3331
- gap: 10,
3332
- width: "100%",
3333
- padding: "7px 10px",
3334
- background: "transparent",
3335
- border: "none",
3336
- borderRadius: 8,
3337
- cursor: "pointer",
3338
- color: "#e6e8ee",
3339
- fontSize: 13,
3340
- textAlign: "left"
3341
- },
3342
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(feature.labelKey) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3343
- "aria-hidden": "true",
4640
+ const active = descFeature === feature.id;
4641
+ const tipId = `ms-settings-tip-${feature.id}`;
4642
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4643
+ "data-toolbar-pin-row": true,
4644
+ "data-row-id": feature.id,
4645
+ style: { position: "relative" },
4646
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4647
+ type: "button",
4648
+ role: "switch",
4649
+ "data-toolbar-pin-toggle": true,
4650
+ "data-pin-id": feature.id,
4651
+ "aria-checked": checked,
4652
+ "aria-label": t(feature.labelKey),
4653
+ "aria-describedby": tipId,
4654
+ onMouseEnter: () => setDescFeature(feature.id),
4655
+ onMouseLeave: () => setDescFeature((prev) => prev === feature.id ? null : prev),
4656
+ onFocus: () => setDescFeature(feature.id),
4657
+ onBlur: () => setDescFeature((prev) => prev === feature.id ? null : prev),
4658
+ onClick: () => onTogglePin(feature.id),
3344
4659
  style: {
3345
- position: "relative",
3346
- width: 32,
3347
- height: 18,
3348
- flexShrink: 0,
3349
- borderRadius: 9,
3350
- background: checked ? accentStrong : "rgba(255, 255, 255, 0.16)",
3351
- transition: "background 120ms ease"
4660
+ display: "flex",
4661
+ alignItems: "center",
4662
+ justifyContent: "space-between",
4663
+ gap: 10,
4664
+ width: "100%",
4665
+ padding: "8px 10px",
4666
+ border: "none",
4667
+ borderRadius: 8,
4668
+ cursor: "pointer",
4669
+ color: "#e6e8ee",
4670
+ fontSize: 13,
4671
+ textAlign: "left"
3352
4672
  },
3353
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
4673
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4674
+ style: {
4675
+ overflow: "hidden",
4676
+ textOverflow: "ellipsis",
4677
+ whiteSpace: "nowrap"
4678
+ },
4679
+ children: t(feature.labelKey)
4680
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4681
+ "aria-hidden": "true",
4682
+ style: {
4683
+ position: "relative",
4684
+ width: 32,
4685
+ height: 18,
4686
+ flexShrink: 0,
4687
+ borderRadius: 9,
4688
+ background: checked ? accentStrong : "rgba(255, 255, 255, 0.16)",
4689
+ transition: "background 120ms ease"
4690
+ },
4691
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
4692
+ position: "absolute",
4693
+ top: 2,
4694
+ left: checked ? 16 : 2,
4695
+ width: 14,
4696
+ height: 14,
4697
+ borderRadius: "50%",
4698
+ background: "#ffffff",
4699
+ transition: "left 120ms ease"
4700
+ } })
4701
+ })]
4702
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4703
+ id: tipId,
4704
+ role: "tooltip",
4705
+ "data-settings-tip": true,
4706
+ "data-tip-for": feature.id,
4707
+ "data-tip-visible": active ? "true" : void 0,
4708
+ style: {
3354
4709
  position: "absolute",
3355
- top: 2,
3356
- left: checked ? 16 : 2,
3357
- width: 14,
3358
- height: 14,
3359
- borderRadius: "50%",
3360
- background: "#ffffff",
3361
- transition: "left 120ms ease"
3362
- } })
4710
+ right: MODAL_TIP_RIGHT,
4711
+ top: "50%",
4712
+ maxWidth: "55%",
4713
+ transform: `translateY(-50%) translateX(${active ? 0 : 4}px)`,
4714
+ padding: "5px 10px",
4715
+ borderRadius: 8,
4716
+ background: "#222834",
4717
+ border: `1px solid rgba(255, 255, 255, 0.14)`,
4718
+ boxShadow: "0 8px 24px rgba(0, 0, 0, 0.35)",
4719
+ color: "#b9c2d4",
4720
+ fontSize: 12,
4721
+ lineHeight: 1.45,
4722
+ opacity: active ? 1 : 0,
4723
+ visibility: active ? "visible" : "hidden",
4724
+ pointerEvents: "none",
4725
+ zIndex: 4
4726
+ },
4727
+ children: t(`settings.desc.${feature.id}`)
3363
4728
  })]
3364
4729
  }, feature.id);
3365
4730
  })
3366
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3367
- "data-settings-desc": true,
3368
- "aria-live": "polite",
3369
- style: {
3370
- display: "flex",
3371
- alignItems: "center",
3372
- padding: "10px 12px",
3373
- borderRadius: 8,
3374
- background: "rgba(255, 255, 255, 0.05)",
3375
- color: "#b9c2d4",
3376
- fontSize: 12.5,
3377
- lineHeight: 1.5
3378
- },
3379
- children: t(descKey)
3380
- })]
3381
- })]
4731
+ })
4732
+ ]
3382
4733
  }),
3383
4734
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3384
4735
  "data-settings-section": true,
3385
4736
  "data-settings-personal": true,
3386
- style: { marginBottom: 14 },
3387
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3388
- "data-settings-section-title": true,
3389
- style: {
3390
- fontSize: 13,
3391
- fontWeight: 600,
3392
- color: "#c7cede",
3393
- marginBottom: 8
3394
- },
3395
- children: t("settings.section.personal")
3396
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4737
+ style: { marginBottom: 20 },
4738
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4739
+ type: "button",
4740
+ "data-personal-toggle": true,
4741
+ "aria-expanded": personalOpen,
4742
+ "aria-label": personalOpen ? t("settings.personal.collapse") : t("settings.personal.expand"),
4743
+ title: personalOpen ? t("settings.personal.collapse") : t("settings.personal.expand"),
4744
+ onClick: () => setPersonalOpen((v) => !v),
4745
+ style: SECTION_TOGGLE_STYLE,
4746
+ children: [
4747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4748
+ width: "14",
4749
+ height: "14",
4750
+ viewBox: "0 0 24 24",
4751
+ fill: "none",
4752
+ stroke: "currentColor",
4753
+ strokeWidth: "2.5",
4754
+ strokeLinecap: "round",
4755
+ strokeLinejoin: "round",
4756
+ "aria-hidden": "true",
4757
+ style: {
4758
+ flexShrink: 0,
4759
+ transform: personalOpen ? "rotate(90deg)" : "none"
4760
+ },
4761
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m9 18 6-6-6-6" })
4762
+ }),
4763
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4764
+ "data-settings-section-title": true,
4765
+ style: { flexShrink: 0 },
4766
+ children: t("settings.section.personal")
4767
+ }),
4768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4769
+ "data-settings-personal-summary": true,
4770
+ style: SECTION_SUMMARY_STYLE,
4771
+ children: t("settings.personal.summary", {
4772
+ accent,
4773
+ icon: iconSize,
4774
+ side: side === "left" ? t("settings.side.left") : t("settings.side.right")
4775
+ })
4776
+ })
4777
+ ]
4778
+ }), personalOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4779
+ "data-settings-personal-body": true,
3397
4780
  style: {
4781
+ padding: "10px 4px 8px",
3398
4782
  display: "flex",
3399
4783
  flexDirection: "column",
3400
- gap: 10
4784
+ gap: 12
3401
4785
  },
3402
4786
  children: [
4787
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4788
+ "data-settings-personal-hint": true,
4789
+ style: {
4790
+ fontSize: 12,
4791
+ color: "#8b96ab",
4792
+ lineHeight: 1.5,
4793
+ padding: "0 6px"
4794
+ },
4795
+ children: t("settings.personal.hint")
4796
+ }),
3403
4797
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3404
4798
  style: {
3405
4799
  display: "flex",
@@ -3612,10 +5006,171 @@ window.__ModuleLoader__.load({
3612
5006
  ]
3613
5007
  })]
3614
5008
  }),
5009
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5010
+ "data-settings-section": true,
5011
+ "data-focus-settings": true,
5012
+ style: { marginBottom: 20 },
5013
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5014
+ type: "button",
5015
+ "data-focus-toggle-settings": true,
5016
+ "aria-expanded": focusOpen,
5017
+ "aria-label": focusOpen ? t("settings.focus.collapse") : t("settings.focus.expand"),
5018
+ title: focusOpen ? t("settings.focus.collapse") : t("settings.focus.expand"),
5019
+ onClick: () => setFocusOpen((v) => !v),
5020
+ style: SECTION_TOGGLE_STYLE,
5021
+ children: [
5022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
5023
+ width: "14",
5024
+ height: "14",
5025
+ viewBox: "0 0 24 24",
5026
+ fill: "none",
5027
+ stroke: "currentColor",
5028
+ strokeWidth: "2.5",
5029
+ strokeLinecap: "round",
5030
+ strokeLinejoin: "round",
5031
+ "aria-hidden": "true",
5032
+ style: {
5033
+ flexShrink: 0,
5034
+ transform: focusOpen ? "rotate(90deg)" : "none"
5035
+ },
5036
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m9 18 6-6-6-6" })
5037
+ }),
5038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5039
+ "data-settings-section-title": true,
5040
+ style: { flexShrink: 0 },
5041
+ children: t("settings.section.focus")
5042
+ }),
5043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5044
+ "data-focus-summary": true,
5045
+ style: SECTION_SUMMARY_STYLE,
5046
+ children: focusSummary
5047
+ })
5048
+ ]
5049
+ }), focusOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5050
+ "data-settings-focus-body": true,
5051
+ style: {
5052
+ padding: "10px 4px 8px",
5053
+ display: "flex",
5054
+ flexDirection: "column",
5055
+ gap: 12
5056
+ },
5057
+ children: [
5058
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5059
+ "data-settings-focus-hint": true,
5060
+ style: {
5061
+ fontSize: 12,
5062
+ color: "#8b96ab",
5063
+ lineHeight: 1.5,
5064
+ padding: "0 6px"
5065
+ },
5066
+ children: t("settings.focus.hint")
5067
+ }),
5068
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5069
+ "data-focus-option": true,
5070
+ style: {
5071
+ display: "flex",
5072
+ alignItems: "center",
5073
+ gap: 8,
5074
+ padding: "6px 8px",
5075
+ borderRadius: 8,
5076
+ fontSize: 13,
5077
+ color: "#e6e8ee",
5078
+ cursor: "pointer"
5079
+ },
5080
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5081
+ type: "checkbox",
5082
+ "data-focus-dim-think": true,
5083
+ checked: prefs.focus.dimThink,
5084
+ onChange: (e) => updateFocus({ dimThink: e.target.checked })
5085
+ }), t("settings.focus.dimThink")]
5086
+ }),
5087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5088
+ "data-focus-option": true,
5089
+ style: {
5090
+ display: "flex",
5091
+ alignItems: "center",
5092
+ gap: 8,
5093
+ padding: "6px 8px",
5094
+ borderRadius: 8,
5095
+ fontSize: 13,
5096
+ color: "#e6e8ee",
5097
+ cursor: "pointer"
5098
+ },
5099
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5100
+ type: "checkbox",
5101
+ "data-focus-dim-tools": true,
5102
+ checked: prefs.focus.dimTools,
5103
+ onChange: (e) => updateFocus({ dimTools: e.target.checked })
5104
+ }), t("settings.focus.dimTools")]
5105
+ }),
5106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5107
+ "data-focus-option": true,
5108
+ style: {
5109
+ display: "flex",
5110
+ alignItems: "center",
5111
+ gap: 8,
5112
+ padding: "6px 8px",
5113
+ borderRadius: 8,
5114
+ fontSize: 13,
5115
+ color: "#e6e8ee",
5116
+ cursor: "pointer"
5117
+ },
5118
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5119
+ type: "checkbox",
5120
+ "data-focus-collapse-think": true,
5121
+ checked: prefs.focus.collapseThink,
5122
+ onChange: (e) => updateFocus({ collapseThink: e.target.checked })
5123
+ }), t("settings.focus.collapseThink")]
5124
+ }),
5125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5126
+ style: {
5127
+ display: "flex",
5128
+ alignItems: "center",
5129
+ gap: 10,
5130
+ flexWrap: "wrap"
5131
+ },
5132
+ children: [
5133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5134
+ style: {
5135
+ fontSize: 12.5,
5136
+ color: "#8b96ab",
5137
+ width: 90,
5138
+ flexShrink: 0
5139
+ },
5140
+ children: t("settings.focus.opacity")
5141
+ }),
5142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5143
+ type: "range",
5144
+ "data-focus-opacity": true,
5145
+ min: .2,
5146
+ max: .8,
5147
+ step: .1,
5148
+ value: prefs.focus.opacity,
5149
+ onChange: (e) => updateFocus({ opacity: Number(e.target.value) }),
5150
+ style: {
5151
+ flex: 1,
5152
+ minWidth: 140,
5153
+ maxWidth: 260
5154
+ }
5155
+ }),
5156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5157
+ "data-focus-opacity-value": true,
5158
+ style: {
5159
+ fontSize: 12.5,
5160
+ color: "#b9c2d4",
5161
+ width: 44
5162
+ },
5163
+ children: [Math.round(prefs.focus.opacity * 100), "%"]
5164
+ })
5165
+ ]
5166
+ })
5167
+ ]
5168
+ })]
5169
+ }),
3615
5170
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3616
5171
  "data-settings-section": true,
3617
5172
  "data-settings-lang": true,
3618
- style: { marginBottom: 14 },
5173
+ style: { marginBottom: 20 },
3619
5174
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3620
5175
  "data-settings-section-title": true,
3621
5176
  style: {
@@ -3765,26 +5320,51 @@ window.__ModuleLoader__.load({
3765
5320
  ]
3766
5321
  })]
3767
5322
  }),
3768
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3769
- type: "button",
3770
- "data-toolbar-settings-reset": true,
3771
- onClick: onResetAll,
5323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5324
+ "data-toolbar-settings-actions": true,
3772
5325
  style: {
3773
- display: "block",
3774
- margin: "14px auto 0",
3775
- padding: "7px 16px",
3776
- background: "rgba(255, 255, 255, 0.06)",
3777
- border: "1px solid rgba(255, 255, 255, 0.14)",
3778
- borderRadius: 8,
3779
- cursor: "pointer",
3780
- color: "#b9c2d4",
3781
- fontSize: 12.5
5326
+ display: "flex",
5327
+ alignItems: "center",
5328
+ justifyContent: "center",
5329
+ gap: 10,
5330
+ margin: "16px auto 2px"
3782
5331
  },
3783
- children: t("settings.reset")
5332
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5333
+ type: "button",
5334
+ "data-toolbar-settings-reset": true,
5335
+ onClick: onResetAll,
5336
+ style: {
5337
+ padding: "7px 16px",
5338
+ border: `1px solid rgba(255, 255, 255, 0.14)`,
5339
+ borderRadius: 8,
5340
+ cursor: "pointer",
5341
+ color: "#b9c2d4",
5342
+ fontSize: 12.5
5343
+ },
5344
+ children: t("settings.reset")
5345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5346
+ type: "button",
5347
+ "data-onboarding-reopen": true,
5348
+ onClick: reopenOnboarding,
5349
+ style: {
5350
+ padding: "7px 16px",
5351
+ border: `1px solid rgba(255, 255, 255, 0.14)`,
5352
+ borderRadius: 8,
5353
+ cursor: "pointer",
5354
+ color: "#b9c2d4",
5355
+ fontSize: 12.5
5356
+ },
5357
+ children: t("onboarding.reopen")
5358
+ })]
3784
5359
  })
3785
5360
  ]
3786
5361
  })
3787
5362
  }),
5363
+ onboardingOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneOnboarding, {
5364
+ t,
5365
+ accent,
5366
+ onClose: () => setOnboardingOpen(false)
5367
+ }),
3788
5368
  updateOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3789
5369
  ref: updatePanelRef,
3790
5370
  "data-update-panel": true,
@@ -3821,7 +5401,7 @@ window.__ModuleLoader__.load({
3821
5401
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3822
5402
  style: { color: "#8b96ab" },
3823
5403
  children: [t("update.current"), ": "]
3824
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.6.2" })] }),
5404
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.6.4" })] }),
3825
5405
  updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3826
5406
  "data-update-latest": true,
3827
5407
  children: [