@davmash/bygone 0.8.6 → 0.8.7

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.
@@ -8772,6 +8772,20 @@ var require_present = __commonJS({
8772
8772
  let presenterIndexTemplate;
8773
8773
  const server = createServer((request, response) => {
8774
8774
  const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
8775
+ if (requestUrl.pathname === "/narration/claim") {
8776
+ if (request.method !== "POST") {
8777
+ respond(response, 405, "Method not allowed");
8778
+ return;
8779
+ }
8780
+ if (!isSameOriginLoopbackRequest(request)) {
8781
+ respond(response, 403, "Forbidden");
8782
+ return;
8783
+ }
8784
+ options.onNarrationClaim?.();
8785
+ response.writeHead(204, { "Cache-Control": "no-store" });
8786
+ response.end();
8787
+ return;
8788
+ }
8775
8789
  if (requestUrl.pathname === "/tour.json") {
8776
8790
  response.writeHead(200, {
8777
8791
  "Content-Type": "application/json; charset=utf-8",
@@ -8897,6 +8911,17 @@ var require_present = __commonJS({
8897
8911
  }
8898
8912
  response.end(message);
8899
8913
  }
8914
+ function isSameOriginLoopbackRequest(request) {
8915
+ const origin = request.headers.origin;
8916
+ const host = request.headers.host;
8917
+ if (typeof origin !== "string" || typeof host !== "string") return false;
8918
+ try {
8919
+ const parsed = new URL(origin);
8920
+ return parsed.protocol === "http:" && parsed.hostname === "127.0.0.1" && parsed.host === host;
8921
+ } catch {
8922
+ return false;
8923
+ }
8924
+ }
8900
8925
  function escapeHtml(text) {
8901
8926
  return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
8902
8927
  }
@@ -8951,6 +8976,116 @@ var require_launchArgs = __commonJS({
8951
8976
  }
8952
8977
  });
8953
8978
 
8979
+ // standalone/windowState.js
8980
+ var require_windowState = __commonJS({
8981
+ "standalone/windowState.js"(exports2, module2) {
8982
+ "use strict";
8983
+ var fs8 = require("fs");
8984
+ var path9 = require("path");
8985
+ var WINDOW_STATE_VERSION = 1;
8986
+ function readWindowState2(filePath) {
8987
+ try {
8988
+ return normalizeWindowState(JSON.parse(fs8.readFileSync(filePath, "utf8")));
8989
+ } catch {
8990
+ return null;
8991
+ }
8992
+ }
8993
+ function writeWindowState2(filePath, state) {
8994
+ const normalized = normalizeWindowState(state);
8995
+ if (!normalized) {
8996
+ throw new Error("Cannot save invalid Bygone window state.");
8997
+ }
8998
+ fs8.mkdirSync(path9.dirname(filePath), { recursive: true });
8999
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
9000
+ try {
9001
+ fs8.writeFileSync(temporaryPath, `${JSON.stringify(normalized, null, 2)}
9002
+ `, "utf8");
9003
+ fs8.renameSync(temporaryPath, filePath);
9004
+ } finally {
9005
+ try {
9006
+ fs8.unlinkSync(temporaryPath);
9007
+ } catch {
9008
+ }
9009
+ }
9010
+ }
9011
+ function normalizeWindowState(value) {
9012
+ if (!value || typeof value !== "object" || value.version !== WINDOW_STATE_VERSION) {
9013
+ return null;
9014
+ }
9015
+ const mainSource = value.main === null ? null : normalizeSessionSource(value.main?.source);
9016
+ if (value.main !== null && !mainSource) {
9017
+ return null;
9018
+ }
9019
+ if (!Array.isArray(value.tours)) {
9020
+ return null;
9021
+ }
9022
+ const tours = value.tours.map(normalizeTourLaunch).filter(Boolean);
9023
+ if (tours.length !== value.tours.length) {
9024
+ return null;
9025
+ }
9026
+ return {
9027
+ version: WINDOW_STATE_VERSION,
9028
+ main: mainSource ? { source: mainSource } : null,
9029
+ tours
9030
+ };
9031
+ }
9032
+ function normalizeSessionSource(source) {
9033
+ if (!source || typeof source !== "object" || typeof source.kind !== "string") {
9034
+ return null;
9035
+ }
9036
+ if (source.kind === "blank") {
9037
+ return { kind: "blank" };
9038
+ }
9039
+ if (source.kind === "files" || source.kind === "directories") {
9040
+ if (!isStringArray(source.paths) || source.paths.length === 0) return null;
9041
+ return {
9042
+ kind: source.kind,
9043
+ paths: [...source.paths],
9044
+ ...source.kind === "directories" && isStringArray(source.labels) ? { labels: [...source.labels] } : {}
9045
+ };
9046
+ }
9047
+ if (source.kind === "file-history" || source.kind === "directory-history") {
9048
+ if (typeof source.path !== "string" || source.path.length === 0) return null;
9049
+ return {
9050
+ kind: source.kind,
9051
+ path: source.path,
9052
+ includeStaged: Boolean(source.includeStaged),
9053
+ skipUnchanged: Boolean(source.skipUnchanged)
9054
+ };
9055
+ }
9056
+ if (source.kind === "git-refs") {
9057
+ if (typeof source.repoRoot !== "string" || !isStringArray(source.refs) || source.refs.length < 2) return null;
9058
+ return { kind: source.kind, repoRoot: source.repoRoot, refs: [...source.refs] };
9059
+ }
9060
+ if (source.kind === "branch-review") {
9061
+ if (typeof source.repoRoot !== "string" || typeof source.headRef !== "string") return null;
9062
+ return {
9063
+ kind: source.kind,
9064
+ repoRoot: source.repoRoot,
9065
+ headRef: source.headRef,
9066
+ ...typeof source.baseRef === "string" ? { baseRef: source.baseRef } : {}
9067
+ };
9068
+ }
9069
+ return null;
9070
+ }
9071
+ function normalizeTourLaunch(value) {
9072
+ if (!value || typeof value !== "object" || !isStringArray(value.args) || typeof value.cwd !== "string") {
9073
+ return null;
9074
+ }
9075
+ return { args: [...value.args], cwd: value.cwd };
9076
+ }
9077
+ function isStringArray(value) {
9078
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
9079
+ }
9080
+ module2.exports = {
9081
+ WINDOW_STATE_VERSION,
9082
+ normalizeWindowState,
9083
+ readWindowState: readWindowState2,
9084
+ writeWindowState: writeWindowState2
9085
+ };
9086
+ }
9087
+ });
9088
+
8954
9089
  // src/windowTitle.ts
8955
9090
  var windowTitle_exports = {};
8956
9091
  __export(windowTitle_exports, {
@@ -9075,7 +9210,7 @@ var require_package = __commonJS({
9075
9210
  displayName: "Bygone",
9076
9211
  desktopName: "com.davidmashburn.bygone.desktop",
9077
9212
  description: "See how change happened. Visual diff and file history for VS Code.",
9078
- version: "0.8.6",
9213
+ version: "0.8.7",
9079
9214
  author: "David Mashburn",
9080
9215
  license: "MIT",
9081
9216
  private: false,
@@ -22324,6 +22459,7 @@ var { tokenMatches, tokensFor } = require_commandSpec();
22324
22459
  var { startPresentation } = require_present();
22325
22460
  var { resolveWorkingDirectory } = require_workingDirectory();
22326
22461
  var { getCliArgsFromArgv, getForwardedLaunchArgs } = require_launchArgs();
22462
+ var { readWindowState, writeWindowState } = require_windowState();
22327
22463
  var {
22328
22464
  buildHistoryTitle: buildHistoryTitle2,
22329
22465
  buildStandaloneSessionTitle: buildStandaloneSessionTitle2,
@@ -22343,13 +22479,15 @@ var parsedDirectoryHistoryCacheSize = Number.parseInt(process.env.BYGONE_DIR_HIS
22343
22479
  var DIRECTORY_HISTORY_CACHE_SIZE = Number.isFinite(parsedDirectoryHistoryCacheSize) && parsedDirectoryHistoryCacheSize > 0 ? parsedDirectoryHistoryCacheSize : DEFAULT_DIRECTORY_HISTORY_CACHE_SIZE;
22344
22480
  var commandLineToolPath = process.platform === "win32" ? path8.join(process.env.LOCALAPPDATA || os.homedir(), "Microsoft", "WindowsApps", "bygone.cmd") : "/usr/local/bin/bygone";
22345
22481
  var gitHistoryService = new GitHistoryService2();
22346
- var launchArguments = parseLaunchArgs(getCliArgs());
22482
+ var initialCliArgs = getCliArgs();
22483
+ var launchArguments = parseLaunchArgs(initialCliArgs);
22347
22484
  var smokeTestMode = launchArguments.kind === "smoke" || launchArguments.kind === "smoke-multi" || launchArguments.kind === "smoke-directory";
22348
22485
  var captureOutputPath = launchArguments.capturePath ? path8.resolve(launchArguments.capturePath) : null;
22349
22486
  var captureMode = Boolean(captureOutputPath);
22350
22487
  var launchWindowWidth = Number.isFinite(launchArguments.windowWidth) ? launchArguments.windowWidth : 1500;
22351
22488
  var launchWindowHeight = Number.isFinite(launchArguments.windowHeight) ? launchArguments.windowHeight : 960;
22352
22489
  var shouldUseSingleInstanceLock = app.isPackaged && !smokeTestMode && !captureMode;
22490
+ var shouldRestoreWindowState = initialCliArgs.length === 0 && !smokeTestMode && !captureMode;
22353
22491
  app.setName(APP_NAME);
22354
22492
  if (typeof app.setAppUserModelId === "function") {
22355
22493
  app.setAppUserModelId("com.davidmashburn.bygone");
@@ -22383,6 +22521,8 @@ var wordWrapEnabled = false;
22383
22521
  var wordWrapAvailable = false;
22384
22522
  var repositorySearch = null;
22385
22523
  var lastRepositoryReplacement = null;
22524
+ var appIsQuitting = false;
22525
+ var restoringWindowState = false;
22386
22526
  if (!singleInstanceLock) {
22387
22527
  app.quit();
22388
22528
  }
@@ -22399,6 +22539,10 @@ app.on("window-all-closed", () => {
22399
22539
  app.quit();
22400
22540
  }
22401
22541
  });
22542
+ app.on("before-quit", () => {
22543
+ appIsQuitting = true;
22544
+ persistOpenWindowState();
22545
+ });
22402
22546
  app.on("will-quit", () => {
22403
22547
  repositorySearch?.handle?.cancel();
22404
22548
  for (const server of [...activeTourServers]) {
@@ -22548,6 +22692,9 @@ function createMainWindow({ show = !smokeTestMode } = {}) {
22548
22692
  smokeTimeout = void 0;
22549
22693
  captureScheduled = false;
22550
22694
  captureRenderReady = false;
22695
+ if (!appIsQuitting && (process.platform === "darwin" || BrowserWindow.getAllWindows().length > 0)) {
22696
+ persistOpenWindowState();
22697
+ }
22551
22698
  });
22552
22699
  }
22553
22700
  function ensureMainWindow() {
@@ -22557,13 +22704,18 @@ function ensureMainWindow() {
22557
22704
  }
22558
22705
  }
22559
22706
  async function openTourPresentation(args, cwd) {
22707
+ let ownerWindow = null;
22560
22708
  const presentation = await startPresentation(args, cwd, packageRoot, {
22561
22709
  announce: false,
22562
- open: false
22710
+ open: false,
22711
+ onNarrationClaim: () => claimTourNarration(ownerWindow)
22563
22712
  });
22564
22713
  activeTourServers.add(presentation.server);
22565
22714
  try {
22566
- await showTourWindow(presentation.url, presentation.server, presentation.manifest);
22715
+ ownerWindow = await showTourWindow(presentation.url, presentation.server, presentation.manifest, {
22716
+ args: [...args],
22717
+ cwd: path8.resolve(cwd)
22718
+ });
22567
22719
  } catch (error) {
22568
22720
  closeTourServer(presentation.server);
22569
22721
  throw error;
@@ -22573,7 +22725,7 @@ async function openAuthoredTourDocument(sourcePath) {
22573
22725
  const document2 = discoverAuthoredTourDocument2(sourcePath);
22574
22726
  await openTourPresentation(["--tour", document2.documentPath], document2.repoRoot);
22575
22727
  }
22576
- async function showTourWindow(url, server, manifest) {
22728
+ async function showTourWindow(url, server, manifest, launch) {
22577
22729
  const parsedUrl = new URL(url);
22578
22730
  if (parsedUrl.protocol !== "http:" || parsedUrl.hostname !== "127.0.0.1") {
22579
22731
  throw new Error("Tour presenter URL must use the local Bygone server.");
@@ -22595,7 +22747,8 @@ async function showTourWindow(url, server, manifest) {
22595
22747
  }
22596
22748
  });
22597
22749
  latestTourWindow = tourWindow;
22598
- tourPresentations.set(tourWindow, { server, origin: tourOrigin });
22750
+ tourPresentations.set(tourWindow, { server, origin: tourOrigin, launch });
22751
+ installApplicationMenu();
22599
22752
  tourWindow.webContents.setWindowOpenHandler(({ url: externalUrl }) => {
22600
22753
  if (/^https?:\/\//i.test(externalUrl)) {
22601
22754
  void shell.openExternal(externalUrl);
@@ -22622,6 +22775,10 @@ async function showTourWindow(url, server, manifest) {
22622
22775
  if (latestTourWindow === tourWindow) {
22623
22776
  latestTourWindow = getLatestTourWindow();
22624
22777
  }
22778
+ installApplicationMenu();
22779
+ if (!appIsQuitting && (process.platform === "darwin" || BrowserWindow.getAllWindows().length > 0)) {
22780
+ persistOpenWindowState();
22781
+ }
22625
22782
  });
22626
22783
  const tourWindowTitle = buildTourWindowTitle2(manifest, APP_NAME);
22627
22784
  const applyTourWindowTitle = () => {
@@ -22643,11 +22800,30 @@ async function showTourWindow(url, server, manifest) {
22643
22800
  applyTourWindowTitle();
22644
22801
  tourWindow.show();
22645
22802
  tourWindow.focus();
22803
+ persistOpenWindowState();
22646
22804
  return tourWindow;
22647
22805
  }
22648
22806
  function getLatestTourWindow() {
22649
22807
  return [...tourPresentations.keys()].reverse().find((window2) => !window2.isDestroyed());
22650
22808
  }
22809
+ function claimTourNarration(ownerWindow) {
22810
+ if (!ownerWindow || ownerWindow.isDestroyed()) return;
22811
+ for (const tourWindow of tourPresentations.keys()) {
22812
+ if (tourWindow !== ownerWindow && !tourWindow.isDestroyed()) {
22813
+ dispatchTourPresenterCommand(tourWindow, "pauseNarration");
22814
+ }
22815
+ }
22816
+ }
22817
+ function dispatchTourPresenterCommand(tourWindow, command) {
22818
+ if (!tourWindow || tourWindow.isDestroyed()) return;
22819
+ const script = `window.dispatchEvent(new CustomEvent('bygone:tour-command', { detail: ${JSON.stringify(command)} }))`;
22820
+ void tourWindow.webContents.executeJavaScript(script).catch(() => {
22821
+ });
22822
+ }
22823
+ function toggleLatestTourNarration() {
22824
+ const tourWindow = getLatestTourWindow();
22825
+ if (tourWindow) dispatchTourPresenterCommand(tourWindow, "toggleNarration");
22826
+ }
22651
22827
  function closeTourServer(server) {
22652
22828
  if (!server || !activeTourServers.delete(server)) {
22653
22829
  return;
@@ -22962,6 +23138,12 @@ ${HELP_URL}`,
22962
23138
  click: () => {
22963
23139
  void openAuthoredTourDialog().catch((error) => showError(`Could not open Bygone presentation: ${getErrorMessage(error)}`));
22964
23140
  }
23141
+ },
23142
+ { type: "separator" },
23143
+ {
23144
+ label: "Listen to Tour",
23145
+ enabled: Boolean(getLatestTourWindow()),
23146
+ click: toggleLatestTourNarration
22965
23147
  }
22966
23148
  ]
22967
23149
  },
@@ -23320,8 +23502,73 @@ async function openInitialLaunchTarget() {
23320
23502
  await routePendingOpenPaths();
23321
23503
  return;
23322
23504
  }
23505
+ if (shouldRestoreWindowState && await restoreOpenWindowState()) {
23506
+ return;
23507
+ }
23323
23508
  await routeLaunchTarget(launchArguments);
23324
23509
  }
23510
+ async function restoreOpenWindowState() {
23511
+ const savedState = readWindowState(getWindowStatePath());
23512
+ if (!savedState || !savedState.main && savedState.tours.length === 0) {
23513
+ return false;
23514
+ }
23515
+ let restoredAnyWindow = false;
23516
+ restoringWindowState = true;
23517
+ try {
23518
+ if (savedState.main) {
23519
+ ensureMainWindow();
23520
+ try {
23521
+ await restoreMainSession(savedState.main.source);
23522
+ } catch (error) {
23523
+ console.error(`Could not restore the previous Bygone session: ${getErrorMessage(error)}`);
23524
+ await openBlankDiff();
23525
+ }
23526
+ restoredAnyWindow = true;
23527
+ }
23528
+ for (const tour of savedState.tours) {
23529
+ try {
23530
+ await openTourPresentation(tour.args, tour.cwd);
23531
+ restoredAnyWindow = true;
23532
+ } catch (error) {
23533
+ console.error(`Could not restore a previous Bygone tour: ${getErrorMessage(error)}`);
23534
+ }
23535
+ }
23536
+ } finally {
23537
+ restoringWindowState = false;
23538
+ persistOpenWindowState();
23539
+ }
23540
+ return restoredAnyWindow;
23541
+ }
23542
+ async function restoreMainSession(source) {
23543
+ if (source.kind === "blank") {
23544
+ await openBlankDiff();
23545
+ return;
23546
+ }
23547
+ session = buildSessionFromSource(source);
23548
+ historyIncludeStagedPreference = Boolean(source.includeStaged ?? historyIncludeStagedPreference);
23549
+ historySkipUnchangedPreference = Boolean(source.skipUnchanged ?? historySkipUnchangedPreference);
23550
+ clearWatchers();
23551
+ updateWatchers();
23552
+ await sendCurrentSession();
23553
+ }
23554
+ function persistOpenWindowState() {
23555
+ if (restoringWindowState || !app.isReady()) return;
23556
+ const hasMainWindow = Boolean(mainWindow && !mainWindow.isDestroyed());
23557
+ const mainSource = hasMainWindow && (isRefreshableSource(session.source) || session.source?.kind === "blank") ? cloneSessionSource(session.source) : hasMainWindow ? { kind: "blank" } : null;
23558
+ const tours = [...tourPresentations.entries()].filter(([window2, presentation]) => !window2.isDestroyed() && presentation.launch).map(([, presentation]) => presentation.launch);
23559
+ try {
23560
+ writeWindowState(getWindowStatePath(), {
23561
+ version: 1,
23562
+ main: mainSource ? { source: mainSource } : null,
23563
+ tours
23564
+ });
23565
+ } catch (error) {
23566
+ console.error(`Could not save Bygone window state: ${getErrorMessage(error)}`);
23567
+ }
23568
+ }
23569
+ function getWindowStatePath() {
23570
+ return path8.join(app.getPath("userData"), "open-windows.json");
23571
+ }
23325
23572
  async function routeLaunchTarget(launchTarget) {
23326
23573
  if (launchTarget.kind === "unsupported-launch-intent") {
23327
23574
  ensureMainWindow();
@@ -26928,6 +27175,7 @@ async function refreshSession(options = {}) {
26928
27175
  function postOrQueue(message) {
26929
27176
  installApplicationMenu();
26930
27177
  if (message && typeof message === "object" && typeof message.type === "string" && message.type.startsWith("show")) {
27178
+ persistOpenWindowState();
26931
27179
  if (!refreshInProgress) {
26932
27180
  refreshFailure = null;
26933
27181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davmash/bygone",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "See how change happened. Visual diff and file history.",
5
5
  "author": "David Mashburn",
6
6
  "license": "MIT",
@@ -30,13 +30,13 @@
30
30
  <div id="change-toolbar" class="change-toolbar header-align-diff" hidden>
31
31
  <div class="change-toolbar-main">
32
32
  <div class="change-toolbar-nav">
33
- <button id="previous-file" class="change-button icon-button" type="button" title="Open previous file" aria-label="Previous file">
33
+ <button id="previous-file" class="change-button icon-button" type="button" title="Open previous file" data-tooltip="Open previous file" aria-label="Previous file">
34
34
  <svg viewBox="0 0 24 24" aria-hidden="true">
35
35
  <path d="M17 11l-5-5-5 5"></path>
36
36
  <path d="M17 18l-5-5-5 5" opacity="0.7"></path>
37
37
  </svg>
38
38
  </button>
39
- <button id="next-file" class="change-button change-button-primary icon-button" type="button" title="Open next file" aria-label="Next file">
39
+ <button id="next-file" class="change-button change-button-primary icon-button" type="button" title="Open next file" data-tooltip="Open next file" aria-label="Next file">
40
40
  <svg viewBox="0 0 24 24" aria-hidden="true">
41
41
  <path d="M7 13l5 5 5-5"></path>
42
42
  <path d="M7 6l5 5 5-5" opacity="0.7"></path>
@@ -72,8 +72,15 @@
72
72
  </svg>
73
73
  </button>
74
74
  </div>
75
+ <div class="change-toolbar-actions">
75
76
  <button id="toggle-word-wrap" class="change-button icon-button" type="button" title="Enable line wrapping (Alt+Z)" aria-label="Enable line wrapping" aria-pressed="false" hidden>
76
- <svg viewBox="0 0 24 24" aria-hidden="true">
77
+ <svg class="word-wrap-icon-off" viewBox="0 0 24 24" aria-hidden="true">
78
+ <path d="M4 6h13a3 3 0 0 1 0 6H9"></path>
79
+ <path d="M12 9l-3 3 3 3"></path>
80
+ <path d="M4 18h8"></path>
81
+ <path d="M5 4l14 16"></path>
82
+ </svg>
83
+ <svg class="word-wrap-icon-on" viewBox="0 0 24 24" aria-hidden="true">
77
84
  <path d="M4 6h13a3 3 0 0 1 0 6H9"></path>
78
85
  <path d="M12 9l-3 3 3 3"></path>
79
86
  <path d="M4 18h8"></path>
@@ -85,6 +92,7 @@
85
92
  <path d="M20 5v6h-6"></path>
86
93
  </svg>
87
94
  </button>
95
+ </div>
88
96
  </div>
89
97
  <div class="change-hint">F7 / Shift+F7 · Cmd/Ctrl+Shift+Up/Down to jump.</div>
90
98
  </div>
@@ -94,14 +102,14 @@
94
102
  <div id="history-left-time" class="history-time"></div>
95
103
  </div>
96
104
  <div class="history-nav">
97
- <button id="history-back" class="history-button icon-button" type="button" title="Open older commit (Alt+Left)" aria-label="Older commit">
105
+ <button id="history-back" class="history-button icon-button" type="button" title="Open older commit (Alt+Left)" data-tooltip="Open older commit" aria-label="Older commit">
98
106
  <svg viewBox="0 0 24 24" aria-hidden="true">
99
107
  <path d="M15 18l-6-6 6-6"></path>
100
108
  <path d="M10 18l-6-6 6-6" opacity="0.7"></path>
101
109
  </svg>
102
110
  </button>
103
111
  <div id="history-position" class="history-position" role="status" aria-live="polite" aria-atomic="true"></div>
104
- <button id="history-forward" class="history-button icon-button" type="button" title="Open newer commit (Alt+Right)" aria-label="Newer commit">
112
+ <button id="history-forward" class="history-button icon-button" type="button" title="Open newer commit (Alt+Right)" data-tooltip="Open newer commit" aria-label="Newer commit">
105
113
  <svg viewBox="0 0 24 24" aria-hidden="true">
106
114
  <path d="M9 18l6-6-6-6"></path>
107
115
  <path d="M14 18l6-6-6-6" opacity="0.7"></path>
package/web/index.html CHANGED
@@ -60,6 +60,34 @@
60
60
  <span id="tour-breadcrumb" class="tour-breadcrumb"></span>
61
61
  <button id="tour-next" type="button" title="Next tour item (Right or Page Down)">Next →</button>
62
62
  </nav>
63
+ <div id="tour-narration-controls" class="tour-narration-controls" role="group" aria-label="Tour narration">
64
+ <div class="tour-narration-transport" role="group" aria-label="Narration playback">
65
+ <button id="tour-narration-skip-back" type="button" title="Previous sentence" aria-label="Previous sentence" disabled>
66
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>
67
+ </button>
68
+ <button id="tour-listen" type="button" title="Listen from the current tour item" aria-label="Listen from the current tour item">
69
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="tour-icon-fill tour-play-pause-icon" d="m8 5 11 7-11 7Z"/></svg>
70
+ </button>
71
+ <button id="tour-stop" type="button" title="Stop narration" aria-label="Stop narration" disabled>
72
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="tour-icon-fill" d="M6 6h12v12H6z"/></svg>
73
+ </button>
74
+ <button id="tour-narration-skip-ahead" type="button" title="Next sentence" aria-label="Next sentence" disabled>
75
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>
76
+ </button>
77
+ </div>
78
+ <div class="tour-narration-settings">
79
+ <select id="tour-narration-voice" aria-label="Narration voice" title="Narration voice" disabled>
80
+ <option value="">System default voice</option>
81
+ </select>
82
+ <select id="tour-narration-rate" aria-label="Narration speed" title="Narration speed">
83
+ <option value="0.75">0.75×</option>
84
+ <option value="1">1×</option>
85
+ <option value="1.25">1.25×</option>
86
+ <option value="1.5">1.5×</option>
87
+ </select>
88
+ </div>
89
+ <span id="tour-narration-status" class="tour-narration-status" role="status" aria-live="polite" aria-atomic="true">Device voice ready</span>
90
+ </div>
63
91
  <div class="tour-narrative-layout">
64
92
  <div class="tour-narrative-prose">
65
93
  <div id="tour-narrative-chapter" class="tour-narrative-chapter"></div>
@@ -113,13 +141,13 @@
113
141
  <div id="change-toolbar" class="change-toolbar header-align-diff" hidden>
114
142
  <div class="change-toolbar-main">
115
143
  <div class="change-toolbar-nav">
116
- <button id="previous-file" class="change-button icon-button" type="button" title="Open previous file" aria-label="Previous file">
144
+ <button id="previous-file" class="change-button icon-button" type="button" title="Open previous file" data-tooltip="Open previous file" aria-label="Previous file">
117
145
  <svg viewBox="0 0 24 24" aria-hidden="true">
118
146
  <path d="M17 11l-5-5-5 5"></path>
119
147
  <path d="M17 18l-5-5-5 5"></path>
120
148
  </svg>
121
149
  </button>
122
- <button id="next-file" class="change-button icon-button" type="button" title="Open next file" aria-label="Next file">
150
+ <button id="next-file" class="change-button icon-button" type="button" title="Open next file" data-tooltip="Open next file" aria-label="Next file">
123
151
  <svg viewBox="0 0 24 24" aria-hidden="true">
124
152
  <path d="M7 13l5 5 5-5"></path>
125
153
  <path d="M7 6l5 5 5-5"></path>
@@ -155,8 +183,15 @@
155
183
  </svg>
156
184
  </button>
157
185
  </div>
186
+ <div class="change-toolbar-actions">
158
187
  <button id="toggle-word-wrap" class="change-button icon-button" type="button" title="Enable line wrapping (Alt+Z)" aria-label="Enable line wrapping" aria-pressed="false" hidden>
159
- <svg viewBox="0 0 24 24" aria-hidden="true">
188
+ <svg class="word-wrap-icon-off" viewBox="0 0 24 24" aria-hidden="true">
189
+ <path d="M4 6h13a3 3 0 0 1 0 6H9"></path>
190
+ <path d="M12 9l-3 3 3 3"></path>
191
+ <path d="M4 18h8"></path>
192
+ <path d="M5 4l14 16"></path>
193
+ </svg>
194
+ <svg class="word-wrap-icon-on" viewBox="0 0 24 24" aria-hidden="true">
160
195
  <path d="M4 6h13a3 3 0 0 1 0 6H9"></path>
161
196
  <path d="M12 9l-3 3 3 3"></path>
162
197
  <path d="M4 18h8"></path>
@@ -168,6 +203,7 @@
168
203
  <path d="M20 5v6h-6"></path>
169
204
  </svg>
170
205
  </button>
206
+ </div>
171
207
  </div>
172
208
  <div class="change-hint">F7 / Shift+F7 · Cmd/Ctrl+Shift+Up/Down to jump.</div>
173
209
  </div>
@@ -177,14 +213,14 @@
177
213
  <div id="history-left-time" class="history-time"></div>
178
214
  </div>
179
215
  <div class="history-nav">
180
- <button id="history-back" class="history-button icon-button" type="button" title="Open older commit" aria-label="Older commit">
216
+ <button id="history-back" class="history-button icon-button" type="button" title="Open older commit" data-tooltip="Open older commit" aria-label="Older commit">
181
217
  <svg viewBox="0 0 24 24" aria-hidden="true">
182
218
  <path d="M15 18l-6-6 6-6"></path>
183
219
  <path d="M10 18l-6-6 6-6" opacity="0.7"></path>
184
220
  </svg>
185
221
  </button>
186
222
  <div id="history-position" class="history-position" role="status" aria-live="polite" aria-atomic="true"></div>
187
- <button id="history-forward" class="history-button icon-button" type="button" title="Open newer commit" aria-label="Newer commit">
223
+ <button id="history-forward" class="history-button icon-button" type="button" title="Open newer commit" data-tooltip="Open newer commit" aria-label="Newer commit">
188
224
  <svg viewBox="0 0 24 24" aria-hidden="true">
189
225
  <path d="M9 18l6-6-6-6"></path>
190
226
  <path d="M14 18l6-6-6-6" opacity="0.7"></path>