dsh-milestone 0.6.4 → 0.6.6

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 +56 -119
  2. package/lib/client.js +499 -967
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -598,6 +598,9 @@ window.__ModuleLoader__.load({
598
598
  "turn.label": "第 {n} 轮",
599
599
  /** Hover position: `第 {hover.index + 1} / {hover.total} 条`. */
600
600
  "pos.of": "第 {n} / {m} 条",
601
+ /** Hover position of a collapsed-turn summary dot: `第 {a}–{b} / {m} 条`
602
+ * (the range of messages the summary dot represents). */
603
+ "pos.range": "第 {a}–{b} / {m} 条",
601
604
  /** Search input placeholder. */
602
605
  "search.placeholder": "搜索消息内容",
603
606
  /** aria-label on the search toggle button and the search input. */
@@ -658,6 +661,8 @@ window.__ModuleLoader__.load({
658
661
  "list.close": "收起列表",
659
662
  /** Header title of the all-prompts list panel. */
660
663
  "list.label": "全部提问",
664
+ /** Bottom hint of the all-prompts list while it drains older pages. */
665
+ "list.loading": "正在加载更早消息…",
661
666
  /** Header title + input placeholder of the cross-session search panel. */
662
667
  "search.cross": "跨会话搜索",
663
668
  /** aria-label + title on the cross-session search toggle when the panel is CLOSED. */
@@ -796,78 +801,48 @@ window.__ModuleLoader__.load({
796
801
  "settings.lang.zh": "中文",
797
802
  /** Language option: force English copy. */
798
803
  "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": "重新查看教程"
804
+ /** 0.6.5 tour: bubble 0 title (also the bubble's aria-label). */
805
+ "tour.welcome.title": "欢迎使用 dsh-milestone",
806
+ /** 0.6.5 tour: bubble 0 description — the REAL rail list anchor. */
807
+ "tour.welcome.desc": "这是你的会话导航条:每条提问一个圆点,点击跳转,悬停查看元信息。",
808
+ /** 0.6.5 tour: bubble 1 title — the expand arrow. */
809
+ "tour.step1.title": "展开工具栏",
810
+ /** 0.6.5 tour: bubble 1 description — what clicking REALLY does (auto-advance on the real click). */
811
+ "tour.step1.desc": "点这里展开全部功能键:搜索、列表、收藏、聚焦、更新检测与设置都在里面。",
812
+ /** 0.6.5 tour: bubble 2 title — the settings gear. */
813
+ "tour.step2.title": "打开设置",
814
+ /** 0.6.5 tour: bubble 2 description — what opening settings contains (auto-advance on the real open). */
815
+ "tour.step2.desc": "打开设置:钉住常用键、个性化外观、切换语言、搭配「聚焦」。",
816
+ /** 0.6.5 tour: bubble 3 title — the dots (real hover/click behavior). */
817
+ "tour.step3.title": "圆点导航",
818
+ /** 0.6.5 tour: bubble 3 description. */
819
+ "tour.step3.desc": "每个圆点 = 一条提问——点击跳转,悬停查看时间、模型与用量。",
820
+ /** 0.6.5 tour: bubble 4 title — done. */
821
+ "tour.step4.title": "开始使用",
822
+ /** 0.6.5 tour: bubble 4 description — finish line. */
823
+ "tour.step4.desc": "去试试吧;有问题欢迎到 GitHub 提 Issue。",
824
+ /** 0.6.5 tour: primary action on bubble 0 (begin) and bubble 4 (finish). */
825
+ "tour.start": "开始使用",
826
+ /** 0.6.5 tour: forward navigation (bubbles 1–3; the real-click auto-advance comes first). */
827
+ "tour.next": "下一步",
828
+ /** 0.6.5 tour: back navigation (bubbles 1–4). */
829
+ "tour.prev": "上一步",
830
+ /** 0.6.5 tour: corner skip — every bubble, persists the flag. */
831
+ "tour.skip": "跳过",
832
+ /** 0.6.5 tour: progress text `第 n / 5 步`. */
833
+ "tour.step.label": "第 {n} / 5 步",
834
+ /** 0.6.5 tour: aria-label on one progress dot. */
835
+ "tour.step.short": "第 {n} 步",
836
+ /** 0.6.5 settings footer action: replay the tour (data-onboarding-reopen kept). */
837
+ "tour.reopen": "重新查看教程"
865
838
  };
866
839
  const en = {
867
840
  "jump.to": "Jump to message {n}",
868
841
  "window.hint": "Showing {n} messages · more below",
869
842
  "turn.label": "Turn {n}",
870
843
  "pos.of": "Message {n} of {m}",
844
+ /** Collapsed-summary dot position: the message RANGE the dot represents. */
845
+ "pos.range": "Messages {a}–{b} of {m}",
871
846
  "search.placeholder": "Search message content",
872
847
  "search.label": "Search messages",
873
848
  "bookmark.filter": "Bookmarks only",
@@ -898,6 +873,8 @@ window.__ModuleLoader__.load({
898
873
  "list.open": "Open list",
899
874
  "list.close": "Close list",
900
875
  "list.label": "All prompts",
876
+ /** Bottom hint of the all-prompts list while it drains older pages. */
877
+ "list.loading": "Loading earlier messages…",
901
878
  "search.cross": "Cross-session search",
902
879
  "search.cross.open": "Open cross-session search",
903
880
  "search.cross.close": "Close cross-session search",
@@ -967,39 +944,23 @@ window.__ModuleLoader__.load({
967
944
  "settings.lang.system": "Follow system",
968
945
  "settings.lang.zh": "Chinese",
969
946
  "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"
947
+ "tour.welcome.title": "Welcome to dsh-milestone",
948
+ "tour.welcome.desc": "This is your session milestone rail: every prompt is a dot — click to jump, hover for metadata.",
949
+ "tour.step1.title": "Expand the toolbar",
950
+ "tour.step1.desc": "Click to reveal every function key: search, list, bookmarks, focus, update check, and settings.",
951
+ "tour.step2.title": "Open settings",
952
+ "tour.step2.desc": "Open settings: pin your favorite keys, personalize the look, switch language, and tune focus.",
953
+ "tour.step3.title": "Dot navigation",
954
+ "tour.step3.desc": "Each dot is one prompt — click to jump, hover for time, model, and usage.",
955
+ "tour.step4.title": "Get started",
956
+ "tour.step4.desc": "Go try it — questions and issues are welcome on GitHub.",
957
+ "tour.start": "Get started",
958
+ "tour.next": "Next",
959
+ "tour.prev": "Back",
960
+ "tour.skip": "Skip",
961
+ "tour.step.label": "Step {n} of 5",
962
+ "tour.step.short": "Step {n}",
963
+ "tour.reopen": "Replay the tour"
1003
964
  };
1004
965
  /**
1005
966
  * Interpolate `{name}` placeholders with params, matching the harness t seat's
@@ -1022,6 +983,31 @@ window.__ModuleLoader__.load({
1022
983
  //#endregion
1023
984
  //#region src/client/turn-group-logic.ts
1024
985
  /**
986
+ * Renumber the raw harness turn numbers into a compact 1-based DISPLAY
987
+ * sequence over the marks that actually render. The harness numbers every
988
+ * engine turn (subagent/injected turns included), so the raw numbers show
989
+ * gaps (turns that produced no user mark) and repeats (several marks sharing
990
+ * one turn); labels fed through this map read as clean 1, 2, 3, … rounds.
991
+ *
992
+ * Grouping and collapse logic keep operating on the RAW turn (same-turn marks
993
+ * are contiguous in rail order, so first-appearance ranking preserves the
994
+ * partition) — only labels consume this map.
995
+ *
996
+ * @param marks - marks in rail order (only `turn` is consulted).
997
+ * @returns raw turn -> display round (1-based), in first-appearance order;
998
+ * turns that never appear (or marks without turn info) get no entry.
999
+ */
1000
+ function buildDisplayTurns(marks) {
1001
+ const display = /* @__PURE__ */ new Map();
1002
+ let rank = 0;
1003
+ for (const mark of marks) {
1004
+ if (mark.turn === void 0) continue;
1005
+ if (display.has(mark.turn)) continue;
1006
+ display.set(mark.turn, ++rank);
1007
+ }
1008
+ return display;
1009
+ }
1010
+ /**
1025
1011
  * Partition consecutive marks by turn. Marks with the same numeric turn that
1026
1012
  * appear one after another share a group; each mark with `turn === undefined`
1027
1013
  * becomes its own singleton group with `turn: null`.
@@ -1334,9 +1320,10 @@ window.__ModuleLoader__.load({
1334
1320
  /**
1335
1321
  * @param props - the panel anchor, the full marks array, and the rail's jump handler.
1336
1322
  */
1337
- function MilestoneListPanel({ panelTop, panelRight, marks, onJump, onClose, t }) {
1323
+ function MilestoneListPanel({ panelTop, panelRight, marks, onJump, onClose, loading = false, t }) {
1338
1324
  const panelRef = (0, react.useRef)(null);
1339
1325
  useOutsideDismiss(panelRef, true, onClose, { exclude: (target) => outsideDismissMatches(target, "[data-list-toggle]") });
1326
+ const displayTurns = (0, react.useMemo)(() => buildDisplayTurns(marks), [marks]);
1340
1327
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1341
1328
  ref: panelRef,
1342
1329
  "data-milestone-list": true,
@@ -1376,7 +1363,7 @@ window.__ModuleLoader__.load({
1376
1363
  children: marks.length
1377
1364
  })]
1378
1365
  }),
1379
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1366
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1380
1367
  style: {
1381
1368
  maxHeight: 300,
1382
1369
  overflowY: "auto",
@@ -1384,7 +1371,7 @@ window.__ModuleLoader__.load({
1384
1371
  flexDirection: "column",
1385
1372
  gap: 2
1386
1373
  },
1387
- children: marks.map((mark, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1374
+ children: [marks.map((mark, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1388
1375
  type: "button",
1389
1376
  "data-list-item": true,
1390
1377
  "data-jump-key": mark.key,
@@ -1411,7 +1398,7 @@ window.__ModuleLoader__.load({
1411
1398
  children: [t("pos.of", {
1412
1399
  n: i + 1,
1413
1400
  m: marks.length
1414
- }), mark.turn !== void 0 ? ` · ${t("turn.label", { n: mark.turn })}` : null]
1401
+ }), mark.turn !== void 0 ? ` · ${t("turn.label", { n: displayTurns.get(mark.turn) ?? mark.turn })}` : null]
1415
1402
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1416
1403
  style: {
1417
1404
  fontSize: 13,
@@ -1422,27 +1409,20 @@ window.__ModuleLoader__.load({
1422
1409
  },
1423
1410
  children: mark.preview || t("no.text")
1424
1411
  })]
1425
- }, mark.key))
1412
+ }, mark.key)), loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1413
+ "data-list-loading": true,
1414
+ style: {
1415
+ fontSize: 12,
1416
+ color: "#8b96ab",
1417
+ textAlign: "center",
1418
+ padding: "6px 8px"
1419
+ },
1420
+ children: t("list.loading")
1421
+ })]
1426
1422
  })
1427
1423
  ]
1428
1424
  });
1429
1425
  }
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
1426
  const MODAL_FG = "#e6e8ee";
1447
1427
  const MODAL_TITLE = "#c7cede";
1448
1428
  const MODAL_TEXT = "#b9c2d4";
@@ -1488,610 +1468,137 @@ window.__ModuleLoader__.load({
1488
1468
  } catch {}
1489
1469
  }
1490
1470
  //#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
1471
+ //#region src/client/MilestoneTour.tsx
1503
1472
  /**
1504
- * MilestoneOnboarding: the 0.6.4 first-run tutorial modal.
1473
+ * MilestoneTour: the 0.6.5 first-run coach-bubble tutorial.
1474
+ *
1475
+ * Unlike the deleted 0.6.4 MilestoneOnboarding modal (built-in toy demos on a
1476
+ * fake overlay), this tour never fakes anything. Each of the 5 bubbles anchors
1477
+ * a REAL rail element (`data-rail-list`, `data-toolbar-expand`,
1478
+ * `data-toolbar-settings`, the first `data-rail-dot`, the rail root), explains
1479
+ * what clicking that control ACTUALLY does, and auto-advances by OBSERVING the
1480
+ * live state the rail already owns:
1481
+ * - bubble 1 (expand arrow) waits for `toolbarExpanded` → true — the user
1482
+ * really clicked the arrow (「下一步」 remains a fallback);
1483
+ * - bubble 2 (settings gear) waits for `settingsOpen` → true — the settings
1484
+ * panel appearing IS the proof — and SUSPENDS while the modal is open
1485
+ * (state is kept; closing settings resumes the bubble at the current step);
1486
+ * - bubble 3 (dots) needs no interaction — hover/click are explained.
1505
1487
  *
1506
1488
  * 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.
1489
+ * this component is presentation + state machine only. It persists the
1490
+ * `dsh-milestone.onboarded` flag itself via onboarding-store on IMPRESSION
1491
+ * (showing bubble 0 is enough) and on every close path (skip / Esc / finish).
1511
1492
  *
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.
1493
+ * Positioning: `position: fixed`, opened on the target's FREE side (rail on
1494
+ * the right → bubble left of the target; rail on the left → bubble right of
1495
+ * it), measured with getBoundingClientRect and clamped to the viewport.
1496
+ * Re-measured on step changes, whenever the anchor may have (un)mounted (the
1497
+ * forced toolbar expansion), and on window resize. There is NO overlay and the
1498
+ * bubble never covers its own target — the user is supposed to CLICK the real
1499
+ * controls, so pointer-events live only on the bubble box itself.
1517
1500
  *
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 重新查看教程.
1501
+ * Anchor guarantee: entering any step ≥ 2 calls `onSetToolbarExpanded(true)`
1502
+ * because `data-toolbar-settings` renders only while the toolbar is expanded
1503
+ * (or the gear pinned) — the 「下一步」 fallback path lands there un-expanded.
1504
+ *
1505
+ * Visual language reuses modal-tokens (MODAL_TIP_BG panel, one border tone,
1506
+ * the 12/8px radius scale, three text tiers) and the rail root's inherited
1507
+ * `--ms-accent*` CSS variables. The highlight ring is zero-asset: a
1508
+ * `data-tour-highlight` attribute + one injected CSS rule.
1525
1509
  */
1526
- /** Step-page count — the progress indicator shape. */
1510
+ /** The tour bubble floats above every other rail layer (settings overlay 105). */
1511
+ const TOUR_Z = 106;
1512
+ /** Gap between the target element and the bubble (px). */
1513
+ const TOUR_GAP = 12;
1514
+ /** Bubble width (px) — the left-edge positioning math clamps against it. */
1515
+ const TOUR_WIDTH = 280;
1516
+ /** Minimum padding between the bubble and the viewport edges (px). */
1517
+ const VIEWPORT_PAD = 8;
1518
+ /** Fallback bubble height used when the layout has not settled (px). */
1519
+ const TOUR_HEIGHT_FALLBACK = 120;
1520
+ /** Progress-dot count — the 5 bubble steps (0 = welcome … 4 = done). */
1527
1521
  const STEP_COUNT = [
1522
+ 0,
1528
1523
  1,
1529
1524
  2,
1530
1525
  3,
1531
1526
  4
1532
1527
  ];
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
1528
  /**
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.
1529
+ * Static tour styling that `:hover`/`:focus-visible`/reduced-motion need (the
1530
+ * `:focus-visible` ring and the base-state backgrounds inline styles cannot
1531
+ * duplicate). BASE-states first — the UA default button face must never flood
1532
+ * through on the dark bubble (that exact bug already shipped once).
1558
1533
  */
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 {
1534
+ const TOUR_CSS = `
1535
+ @keyframes ms-tour-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
1536
+ [data-tour-bubble] { animation: ms-tour-fade 160ms ease; }
1537
+ /* BASE-state backgrounds for every bubble control (transparent or themed). */
1538
+ [data-tour-skip] { background: transparent; }
1539
+ [data-tour-prev] { background: rgba(255, 255, 255, 0.06); }
1540
+ [data-tour-primary] { background: var(--ms-accent-bg); color: var(--ms-accent-soft); }
1541
+ /* Hover washes on top of the explicit base states. */
1542
+ [data-tour-skip]:hover { background: rgba(255, 255, 255, 0.08); }
1543
+ [data-tour-prev]:hover:not(:disabled) { background: rgba(255, 255, 255, 0.12); }
1544
+ [data-tour-primary]:hover { background: rgba(255, 255, 255, 0.14); }
1545
+ /* ONE accent focus ring for keyboard use. */
1546
+ [data-tour-skip]:focus-visible, [data-tour-prev]:focus-visible, [data-tour-primary]:focus-visible {
1568
1547
  box-shadow: 0 0 0 2px var(--ms-accent-soft);
1569
1548
  }
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); }
1549
+ /* The real-target highlight ring: accent halo + a subtle scale hint. */
1550
+ [data-tour-highlight] {
1551
+ box-shadow: 0 0 0 2px var(--ms-accent-soft), 0 0 0 4px var(--ms-accent);
1552
+ transform: scale(1.04);
1553
+ transition: box-shadow 140ms ease, transform 140ms ease;
1554
+ }
1572
1555
  @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; }
1556
+ [data-tour-bubble] { animation: none; }
1557
+ [data-tour-highlight] { transform: none; transition: none; }
1558
+ [data-tour-skip], [data-tour-prev], [data-tour-primary] { transition: none; }
1576
1559
  }
1577
1560
  `;
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
- });
1561
+ /** Rail root — every step's fallback anchor and bubble 4's target. */
1562
+ function railRoot() {
1563
+ return document.querySelector("[data-side]");
1989
1564
  }
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
- });
1565
+ const STEP_DEFS = [
1566
+ {
1567
+ titleKey: "tour.welcome.title",
1568
+ descKey: "tour.welcome.desc",
1569
+ anchor: () => document.querySelector("[data-rail-list]")
1570
+ },
1571
+ {
1572
+ titleKey: "tour.step1.title",
1573
+ descKey: "tour.step1.desc",
1574
+ anchor: () => document.querySelector("[data-toolbar-expand]")
1575
+ },
1576
+ {
1577
+ titleKey: "tour.step2.title",
1578
+ descKey: "tour.step2.desc",
1579
+ anchor: () => document.querySelector("[data-toolbar-settings]")
1580
+ },
1581
+ {
1582
+ titleKey: "tour.step3.title",
1583
+ descKey: "tour.step3.desc",
1584
+ anchor: () => document.querySelector("[data-rail-dot]") ?? railRoot()
1585
+ },
1586
+ {
1587
+ titleKey: "tour.step4.title",
1588
+ descKey: "tour.step4.desc",
1589
+ anchor: () => railRoot()
1590
+ }
1591
+ ];
1592
+ /** Clamp `value` into [min, max], never letting min exceed max. */
1593
+ function clamp(value, min, max) {
1594
+ return Math.min(Math.max(value, min), Math.max(min, max));
2082
1595
  }
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 }) {
1596
+ function MilestoneTour({ t, side, toolbarExpanded, settingsOpen, onSetToolbarExpanded, onClose }) {
2093
1597
  const [step, setStep] = (0, react.useState)(0);
2094
- const panelRef = (0, react.useRef)(null);
1598
+ const [pos, setPos] = (0, react.useState)(null);
1599
+ const bubbleRef = (0, react.useRef)(null);
1600
+ const primaryRef = (0, react.useRef)(null);
1601
+ const stepDef = STEP_DEFS[step];
2095
1602
  (0, react.useEffect)(() => {
2096
1603
  writeOnboardedFlag();
2097
1604
  }, []);
@@ -2101,274 +1608,227 @@ window.__ModuleLoader__.load({
2101
1608
  onClose();
2102
1609
  }, [onClose]);
2103
1610
  (0, react.useEffect)(() => {
1611
+ if (settingsOpen) return;
2104
1612
  const onKey = (e) => {
2105
1613
  if (e.key !== "Escape") return;
2106
1614
  finish();
2107
1615
  };
2108
1616
  window.addEventListener("keydown", onKey);
2109
1617
  return () => window.removeEventListener("keydown", onKey);
2110
- }, [finish]);
1618
+ }, [finish, settingsOpen]);
1619
+ (0, react.useEffect)(() => {
1620
+ if (step === 1 && toolbarExpanded) setStep(2);
1621
+ }, [step, toolbarExpanded]);
1622
+ (0, react.useEffect)(() => {
1623
+ if (step === 2 && settingsOpen) setStep(3);
1624
+ }, [step, settingsOpen]);
1625
+ (0, react.useEffect)(() => {
1626
+ if (step >= 2 && !toolbarExpanded) onSetToolbarExpanded(true);
1627
+ }, [
1628
+ step,
1629
+ toolbarExpanded,
1630
+ onSetToolbarExpanded
1631
+ ]);
1632
+ /** Measure the current anchor and place the bubble on its free side. */
1633
+ const measure = (0, react.useCallback)(() => {
1634
+ const anchor = STEP_DEFS[step].anchor();
1635
+ const bubble = bubbleRef.current;
1636
+ if (anchor === null || bubble === null) {
1637
+ setPos(null);
1638
+ return;
1639
+ }
1640
+ const rect = anchor.getBoundingClientRect();
1641
+ const bw = bubble.offsetWidth || TOUR_WIDTH;
1642
+ const bh = bubble.offsetHeight || TOUR_HEIGHT_FALLBACK;
1643
+ const left = clamp(side === "left" ? rect.right + TOUR_GAP : rect.left - TOUR_GAP - bw, VIEWPORT_PAD, window.innerWidth - bw - VIEWPORT_PAD);
1644
+ const top = clamp(rect.top + rect.height / 2 - bh / 2, VIEWPORT_PAD, window.innerHeight - bh - VIEWPORT_PAD);
1645
+ setPos({
1646
+ left,
1647
+ top
1648
+ });
1649
+ }, [step, side]);
1650
+ (0, react.useLayoutEffect)(() => {
1651
+ measure();
1652
+ window.addEventListener("resize", measure);
1653
+ return () => window.removeEventListener("resize", measure);
1654
+ }, [
1655
+ measure,
1656
+ toolbarExpanded,
1657
+ settingsOpen
1658
+ ]);
2111
1659
  (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,
1660
+ document.querySelectorAll("[data-tour-highlight]").forEach((el) => el.removeAttribute("data-tour-highlight"));
1661
+ const anchor = STEP_DEFS[step].anchor();
1662
+ if (anchor !== null) anchor.setAttribute("data-tour-highlight", "");
1663
+ return () => {
1664
+ document.querySelectorAll("[data-tour-highlight]").forEach((el) => el.removeAttribute("data-tour-highlight"));
1665
+ };
1666
+ }, [
1667
+ step,
1668
+ toolbarExpanded,
1669
+ settingsOpen
1670
+ ]);
1671
+ (0, react.useEffect)(() => {
1672
+ if (settingsOpen) return;
1673
+ primaryRef.current?.focus();
1674
+ }, [step, settingsOpen]);
1675
+ const goNext = () => setStep((s) => Math.min(STEP_COUNT.length - 1, s + 1));
1676
+ const goPrev = () => setStep((s) => Math.max(0, s - 1));
1677
+ if (settingsOpen) return null;
1678
+ const isLast = step === STEP_COUNT.length - 1;
1679
+ const primaryLabel = step === 0 || isLast ? t("tour.start") : t("tour.next");
1680
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1681
+ ref: bubbleRef,
1682
+ "data-tour-bubble": true,
1683
+ "data-tour-step": step,
1684
+ role: "dialog",
1685
+ "aria-label": t(stepDef.titleKey),
2118
1686
  style: {
2119
1687
  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
1688
+ left: pos?.left,
1689
+ top: pos?.top,
1690
+ width: TOUR_WIDTH,
1691
+ maxWidth: "calc(100vw - 24px)",
1692
+ boxSizing: "border-box",
1693
+ zIndex: TOUR_Z,
1694
+ padding: 14,
1695
+ background: MODAL_TIP_BG,
1696
+ color: MODAL_FG,
1697
+ borderRadius: 12,
1698
+ border: `1px solid ${MODAL_BORDER}`,
1699
+ boxShadow: "0 12px 36px rgba(0, 0, 0, 0.45)"
2127
1700
  },
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,
1701
+ children: [
1702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: TOUR_CSS }),
1703
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1704
+ style: {
1705
+ display: "flex",
1706
+ alignItems: "flex-start",
1707
+ justifyContent: "space-between",
1708
+ gap: 8
1709
+ },
1710
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1711
+ "data-tour-title": true,
1712
+ style: {
1713
+ fontSize: 14,
1714
+ fontWeight: 700,
1715
+ color: MODAL_FG,
1716
+ lineHeight: 1.35
1717
+ },
1718
+ children: t(stepDef.titleKey)
1719
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1720
+ type: "button",
1721
+ "data-tour-skip": true,
1722
+ onClick: finish,
1723
+ style: {
1724
+ flexShrink: 0,
1725
+ background: "transparent",
1726
+ border: "none",
1727
+ padding: "2px 6px",
1728
+ borderRadius: 8,
1729
+ cursor: "pointer",
1730
+ color: MODAL_HINT,
1731
+ fontSize: 12,
1732
+ lineHeight: 1.4
1733
+ },
1734
+ children: t("tour.skip")
1735
+ })]
1736
+ }),
1737
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1738
+ "data-tour-desc": true,
1739
+ style: {
1740
+ marginTop: 6,
1741
+ fontSize: 12.5,
1742
+ lineHeight: 1.5,
1743
+ color: MODAL_TEXT
1744
+ },
1745
+ children: t(stepDef.descKey)
1746
+ }),
1747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
1748
+ style: {
1749
+ display: "flex",
1750
+ alignItems: "center",
1751
+ justifyContent: "space-between",
1752
+ gap: 10,
1753
+ marginTop: 12
1754
+ },
2149
1755
  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,
1756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1757
+ type: "button",
1758
+ "data-tour-prev": true,
1759
+ disabled: step === 0,
1760
+ onClick: goPrev,
2161
1761
  style: {
2162
- marginTop: 8,
2163
- fontSize: 13,
2164
- lineHeight: 1.65,
2165
- color: MODAL_TEXT
1762
+ flexShrink: 0,
1763
+ background: "rgba(255, 255, 255, 0.06)",
1764
+ border: `1px solid ${MODAL_BORDER}`,
1765
+ borderRadius: 8,
1766
+ padding: "5px 12px",
1767
+ cursor: step === 0 ? "default" : "pointer",
1768
+ color: step === 0 ? MODAL_HINT : MODAL_TEXT,
1769
+ fontSize: 12,
1770
+ lineHeight: 1.4,
1771
+ opacity: step === 0 ? .55 : 1
2166
1772
  },
2167
- children: t("onboarding.welcome.subtitle")
1773
+ children: t("tour.prev")
2168
1774
  }),
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
1775
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2197
1776
  style: {
2198
1777
  display: "flex",
2199
- alignItems: "flex-start",
2200
- justifyContent: "space-between",
2201
- gap: 10
1778
+ flexDirection: "column",
1779
+ alignItems: "center",
1780
+ gap: 5,
1781
+ minWidth: 74
2202
1782
  },
2203
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2204
- "data-onboarding-step-title": true,
1783
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1784
+ "data-tour-progress": true,
2205
1785
  style: {
2206
- fontSize: 15,
2207
- fontWeight: 600,
2208
- color: MODAL_FG
1786
+ fontSize: 11.5,
1787
+ color: MODAL_HINT,
1788
+ lineHeight: 1.2
2209
1789
  },
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")
1790
+ children: t("tour.step.label", { n: step + 1 })
1791
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1792
+ style: {
1793
+ display: "flex",
1794
+ gap: 4
1795
+ },
1796
+ children: STEP_COUNT.map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1797
+ "data-tour-progress-dot": true,
1798
+ "data-active": step === n ? "true" : void 0,
1799
+ "aria-label": t("tour.step.short", { n: n + 1 }),
1800
+ "aria-current": step === n ? "step" : void 0,
1801
+ style: {
1802
+ width: 14,
1803
+ height: 4,
1804
+ borderRadius: 2,
1805
+ background: step === n ? "var(--ms-accent)" : "rgba(255, 255, 255, 0.18)"
1806
+ }
1807
+ }, n))
2222
1808
  })]
2223
1809
  }),
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,
1810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1811
+ type: "button",
1812
+ ref: primaryRef,
1813
+ "data-tour-primary": true,
1814
+ onClick: isLast ? finish : goNext,
2280
1815
  style: {
2281
- marginTop: 14,
2282
- padding: 12,
1816
+ flexShrink: 0,
1817
+ background: "var(--ms-accent-bg)",
1818
+ color: "var(--ms-accent-soft)",
1819
+ border: "1px solid var(--ms-accent)",
2283
1820
  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
1821
+ padding: "5px 14px",
1822
+ cursor: "pointer",
1823
+ fontSize: 12.5,
1824
+ fontWeight: 600,
1825
+ lineHeight: 1.4
2310
1826
  },
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
- ]
1827
+ children: primaryLabel
2368
1828
  })
2369
1829
  ]
2370
- })]
2371
- })
1830
+ })
1831
+ ]
2372
1832
  });
2373
1833
  }
2374
1834
  //#endregion
@@ -2415,10 +1875,7 @@ window.__ModuleLoader__.load({
2415
1875
  marginBottom: 4
2416
1876
  },
2417
1877
  children: [
2418
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pos.of", {
2419
- n: hover.index + 1,
2420
- m: hover.total
2421
- }) }),
1878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.posLabel }),
2422
1879
  hover.turnLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.turnLabel }),
2423
1880
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2424
1881
  type: "button",
@@ -3317,6 +2774,13 @@ window.__ModuleLoader__.load({
3317
2774
  return fresh;
3318
2775
  }
3319
2776
  //#endregion
2777
+ //#region src/client/version-meta.ts
2778
+ /**
2779
+ * Installed plugin version. Injected at build time as
2780
+ * `__DSH_MILESTONE_VERSION__`; falls back to `0.0.0-dev` when unbuilt.
2781
+ */
2782
+ const PLUGIN_VERSION = "0.6.6";
2783
+ //#endregion
3320
2784
  //#region src/client/MilestoneRail.tsx
3321
2785
  /**
3322
2786
  * MilestoneRail: the milestone.rail entry (session scope). Renders a fixed
@@ -3499,6 +2963,14 @@ window.__ModuleLoader__.load({
3499
2963
  [data-toolbar-settings-close]:hover { background: rgba(255, 255, 255, 0.08); }
3500
2964
  [data-toolbar-settings-reset], [data-onboarding-reopen] { background: rgba(255, 255, 255, 0.06); }
3501
2965
  [data-toolbar-settings-reset]:hover, [data-onboarding-reopen]:hover { background: rgba(255, 255, 255, 0.1); }
2966
+ /* BASE state reset: every modal surface must sit transparent on the dark
2967
+ panel — without it the UA default button face (light gray) floods through
2968
+ and rows become unreadable light-on-light. Hover washes above take over
2969
+ on interaction. */
2970
+ [data-toolbar-pin-toggle], [data-personal-toggle], [data-focus-toggle-settings],
2971
+ [data-toolbar-settings-close], [data-focus-option] {
2972
+ background: transparent;
2973
+ }
3502
2974
  /* ONE accent ring for keyboard focus on every modal control. */
3503
2975
  [data-toolbar-pin-toggle]:focus-visible, [data-personal-toggle]:focus-visible,
3504
2976
  [data-focus-toggle-settings]:focus-visible,
@@ -3579,8 +3051,8 @@ window.__ModuleLoader__.load({
3579
3051
  /** B4 update-check: mount-time silent check delay (ms) — give the harness
3580
3052
  * time to settle before hitting the registry. */
3581
3053
  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. */
3054
+ /** 0.6.5 first-run coach tour: mount-time show delay (ms) — let the rail settle
3055
+ * before the first bubble pops. */
3584
3056
  const ONBOARDING_MOUNT_DELAY = 800;
3585
3057
  /** Initial state: no check has completed yet, nothing to show. */
3586
3058
  const NO_UPDATE_CHECK = {
@@ -3687,6 +3159,8 @@ window.__ModuleLoader__.load({
3687
3159
  const [bookmarksOnly, setBookmarksOnly] = (0, react.useState)(false);
3688
3160
  const [focusActive, setFocusActive] = (0, react.useState)(false);
3689
3161
  const [listOpen, setListOpen] = (0, react.useState)(false);
3162
+ const [drainPage, setDrainPage] = (0, react.useState)(false);
3163
+ const [drainFailed, setDrainFailed] = (0, react.useState)(false);
3690
3164
  const [crossOpen, setCrossOpen] = (0, react.useState)(false);
3691
3165
  const [copiedKey, setCopiedKey] = (0, react.useState)(null);
3692
3166
  const [forkedKey, setForkedKey] = (0, react.useState)(null);
@@ -3739,6 +3213,7 @@ window.__ModuleLoader__.load({
3739
3213
  }
3740
3214
  return counts;
3741
3215
  }, [displayMarks]);
3216
+ const displayTurns = (0, react.useMemo)(() => buildDisplayTurns(marks), [marks]);
3742
3217
  const [prefs, setPrefs] = (0, react.useState)(() => loadPrefs());
3743
3218
  const { pinned, accent, iconSize, inset, side } = prefs;
3744
3219
  const scale = iconSize / DOT_HIT;
@@ -3809,7 +3284,8 @@ window.__ModuleLoader__.load({
3809
3284
  const [expandHovered, setExpandHovered] = (0, react.useState)(false);
3810
3285
  const [settingsHovered, setSettingsHovered] = (0, react.useState)(false);
3811
3286
  const [settingsOpen, setSettingsOpen] = (0, react.useState)(false);
3812
- const [onboardingOpen, setOnboardingOpen] = (0, react.useState)(false);
3287
+ const [tourOpen, setTourOpen] = (0, react.useState)(false);
3288
+ const [tourRun, setTourRun] = (0, react.useState)(0);
3813
3289
  /** The feature whose near-row description tip is currently visible
3814
3290
  * (`null` = none — tips only appear on hover/focus of their own row). */
3815
3291
  const [descFeature, setDescFeature] = (0, react.useState)(null);
@@ -3891,7 +3367,7 @@ window.__ModuleLoader__.load({
3891
3367
  }, []);
3892
3368
  (0, react.useEffect)(() => {
3893
3369
  const timer = window.setTimeout(() => {
3894
- if (!readOnboardedFlag()) setOnboardingOpen(true);
3370
+ if (!readOnboardedFlag()) setTourOpen(true);
3895
3371
  }, ONBOARDING_MOUNT_DELAY);
3896
3372
  return () => window.clearTimeout(timer);
3897
3373
  }, []);
@@ -4009,6 +3485,22 @@ window.__ModuleLoader__.load({
4009
3485
  window.addEventListener("keydown", onKey);
4010
3486
  return () => window.removeEventListener("keydown", onKey);
4011
3487
  }, [listOpen, crossOpen]);
3488
+ const listDraining = listOpen && (drainPage || hasMore && !drainFailed);
3489
+ (0, react.useEffect)(() => {
3490
+ if (!listOpen) return;
3491
+ setDrainFailed(false);
3492
+ }, [listOpen]);
3493
+ (0, react.useEffect)(() => {
3494
+ if (!listOpen || !hasMore || drainPage || drainFailed) return;
3495
+ setDrainPage(true);
3496
+ loadOlder().catch(() => setDrainFailed(true)).finally(() => setDrainPage(false));
3497
+ }, [
3498
+ listOpen,
3499
+ hasMore,
3500
+ drainPage,
3501
+ drainFailed,
3502
+ loadOlder
3503
+ ]);
4012
3504
  if (railBox === null || marks.length < MIN_MARKS) return null;
4013
3505
  const updateQuery = (query) => {
4014
3506
  setSearch({
@@ -4058,12 +3550,14 @@ window.__ModuleLoader__.load({
4058
3550
  setSettingsOpen(false);
4059
3551
  settingsBtnRef.current?.focus();
4060
3552
  };
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 = () => {
3553
+ /** 0.6.5: settings → 重新查看教程 — close settings and replay the coach
3554
+ * tour immediately (the flag may or may not be set; skipping/completing it
3555
+ * re-persists the flag anyway). The `tourRun` bump remounts the tour so the
3556
+ * replay always restarts from bubble 0. */
3557
+ const reopenTour = () => {
4065
3558
  setSettingsOpen(false);
4066
- setOnboardingOpen(true);
3559
+ setTourRun((n) => n + 1);
3560
+ setTourOpen(true);
4067
3561
  };
4068
3562
  /** B1: a feature renders while the toolbar is EXPANDED or while it is pinned. */
4069
3563
  const featureVisible = (id) => toolbarExpanded || pinned.includes(id);
@@ -4385,11 +3879,20 @@ window.__ModuleLoader__.load({
4385
3879
  }
4386
3880
  }
4387
3881
  const meta = deriveTurnMeta(nodes, locations, mark.turn, trajectoryRequests);
3882
+ const summaryCount = collapsedSummaries.get(mark.key);
4388
3883
  return {
4389
3884
  mark,
4390
3885
  index,
4391
3886
  total: displayMarks.length,
4392
- turnLabel: mark.turn !== void 0 ? t("turn.label", { n: mark.turn }) : null,
3887
+ posLabel: summaryCount !== void 0 ? t("pos.range", {
3888
+ a: index - summaryCount + 2,
3889
+ b: index + 1,
3890
+ m: displayMarks.length
3891
+ }) : t("pos.of", {
3892
+ n: index + 1,
3893
+ m: displayMarks.length
3894
+ }),
3895
+ turnLabel: mark.turn !== void 0 ? t("turn.label", { n: displayTurns.get(mark.turn) ?? mark.turn }) : null,
4393
3896
  durationLabel,
4394
3897
  reasonLabel,
4395
3898
  ttftLabel,
@@ -5345,7 +4848,7 @@ window.__ModuleLoader__.load({
5345
4848
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5346
4849
  type: "button",
5347
4850
  "data-onboarding-reopen": true,
5348
- onClick: reopenOnboarding,
4851
+ onClick: reopenTour,
5349
4852
  style: {
5350
4853
  padding: "7px 16px",
5351
4854
  border: `1px solid rgba(255, 255, 255, 0.14)`,
@@ -5354,17 +4857,20 @@ window.__ModuleLoader__.load({
5354
4857
  color: "#b9c2d4",
5355
4858
  fontSize: 12.5
5356
4859
  },
5357
- children: t("onboarding.reopen")
4860
+ children: t("tour.reopen")
5358
4861
  })]
5359
4862
  })
5360
4863
  ]
5361
4864
  })
5362
4865
  }),
5363
- onboardingOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneOnboarding, {
4866
+ tourOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneTour, {
5364
4867
  t,
5365
- accent,
5366
- onClose: () => setOnboardingOpen(false)
5367
- }),
4868
+ side,
4869
+ toolbarExpanded,
4870
+ settingsOpen,
4871
+ onSetToolbarExpanded: setToolbarExpanded,
4872
+ onClose: () => setTourOpen(false)
4873
+ }, tourRun),
5368
4874
  updateOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5369
4875
  ref: updatePanelRef,
5370
4876
  "data-update-panel": true,
@@ -5401,7 +4907,7 @@ window.__ModuleLoader__.load({
5401
4907
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5402
4908
  style: { color: "#8b96ab" },
5403
4909
  children: [t("update.current"), ": "]
5404
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.6.4" })] }),
4910
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.6.6" })] }),
5405
4911
  updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5406
4912
  "data-update-latest": true,
5407
4913
  children: [
@@ -5506,6 +5012,8 @@ window.__ModuleLoader__.load({
5506
5012
  panelRight: panelRightFor(PANEL_WIDTH_STANDARD),
5507
5013
  marks,
5508
5014
  onJump: jump,
5015
+ onClose: () => setListOpen(false),
5016
+ loading: listDraining,
5509
5017
  t
5510
5018
  }),
5511
5019
  crossOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneSessionSearch, {
@@ -5589,7 +5097,7 @@ window.__ModuleLoader__.load({
5589
5097
  "aria-current": dotState === "active" ? "true" : void 0,
5590
5098
  "data-current": dotState === "current" ? "true" : void 0,
5591
5099
  "data-dimmed": dotState === "dimmed" ? "true" : void 0,
5592
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5100
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5593
5101
  style: {
5594
5102
  position: "relative",
5595
5103
  width: size,
@@ -5602,7 +5110,7 @@ window.__ModuleLoader__.load({
5602
5110
  opacity: isHovered || dotState !== "dimmed" ? 1 : .22
5603
5111
  },
5604
5112
  "data-bookmarked": bookmarked ? "true" : void 0,
5605
- children: ringStyle !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5113
+ children: [ringStyle !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5606
5114
  "data-badge": badge,
5607
5115
  style: {
5608
5116
  position: "absolute",
@@ -5613,7 +5121,31 @@ window.__ModuleLoader__.load({
5613
5121
  pointerEvents: "none",
5614
5122
  animation: ringStyle.pulse ? "milestone-badge-pulse 2s ease-in-out infinite" : void 0
5615
5123
  }
5616
- })
5124
+ }), summaryCount !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5125
+ "data-collapsed-badge": true,
5126
+ style: {
5127
+ position: "absolute",
5128
+ top: -5,
5129
+ right: -7,
5130
+ minWidth: 15,
5131
+ height: 15,
5132
+ padding: "0 3px",
5133
+ display: "flex",
5134
+ alignItems: "center",
5135
+ justifyContent: "center",
5136
+ borderRadius: 8,
5137
+ background: "rgba(20, 24, 32, 0.96)",
5138
+ border: `1px solid ${accentSoft}`,
5139
+ boxSizing: "border-box",
5140
+ color: "#e6e8ee",
5141
+ fontSize: 9,
5142
+ lineHeight: 1,
5143
+ fontWeight: 600,
5144
+ whiteSpace: "nowrap",
5145
+ pointerEvents: "none"
5146
+ },
5147
+ children: ["×", summaryCount]
5148
+ })]
5617
5149
  })
5618
5150
  }) }, mark.key);
5619
5151
  })