@youtyan/code-viewer 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/index.html CHANGED
@@ -26,6 +26,7 @@
26
26
  <nav class="app-menu" aria-label="Views">
27
27
  <a class="app-menu-item active" data-route="repo" href="/">Repository</a>
28
28
  <a class="app-menu-item active" data-route="diff" href="/todif?from=HEAD&to=worktree">Diff Viewer</a>
29
+ <a class="app-menu-item" data-route="help" href="/help">Help</a>
29
30
  </nav>
30
31
  <div class="global-actions">
31
32
  <button id="theme" title="toggle theme">🌗</button>
package/web/style.css CHANGED
@@ -22,6 +22,7 @@
22
22
  --accent-emph: #0550ae;
23
23
  --accent-subtle: #ddf4ff;
24
24
  --accent-muted: #54aeff66;
25
+ --scope-accent: #bf8700;
25
26
  /* success / danger / attention / done */
26
27
  --success: #1a7f37;
27
28
  --success-emph: #1f883d;
@@ -29,6 +30,8 @@
29
30
  --danger: #cf222e;
30
31
  --danger-emph: #cf222e;
31
32
  --attn: #9a6700;
33
+ --line-hit-bg: #fff8c5;
34
+ --line-hit-border:var(--accent);
32
35
  --done: #8250df;
33
36
  /* diff colors (GitHub exact) */
34
37
  --diff-add-bg: #dafbe1;
@@ -68,12 +71,15 @@
68
71
  --accent-emph: #1f6feb;
69
72
  --accent-subtle: rgba(56,139,253,0.10);
70
73
  --accent-muted: rgba(56,139,253,0.40);
74
+ --scope-accent: #d29922;
71
75
  --success: #3fb950;
72
76
  --success-emph: #238636;
73
77
  --success-subtle: rgba(46,160,67,0.15);
74
78
  --danger: #f85149;
75
79
  --danger-emph: #da3633;
76
80
  --attn: #d29922;
81
+ --line-hit-bg: rgba(187,128,9,0.18);
82
+ --line-hit-border:var(--accent);
77
83
  --done: #a371f7;
78
84
  --diff-add-bg: rgba(46,160,67,0.15);
79
85
  --diff-add-num-bg: rgba(46,160,67,0.30);
@@ -106,6 +112,140 @@ html, body {
106
112
  /* Window scrolls; global header + topbar + sidebar are fixed */
107
113
  #app { display: block; }
108
114
 
115
+ .gdp-palette-backdrop {
116
+ position: fixed;
117
+ inset: 0;
118
+ z-index: 500;
119
+ background: rgba(31, 35, 40, 0.18);
120
+ display: flex;
121
+ align-items: flex-start;
122
+ justify-content: center;
123
+ padding-top: min(12vh, 96px);
124
+ }
125
+
126
+ [data-theme="dark"] .gdp-palette-backdrop {
127
+ background: rgba(1, 4, 9, 0.45);
128
+ }
129
+
130
+ .gdp-palette {
131
+ width: min(760px, calc(100vw - 32px));
132
+ max-height: min(620px, calc(100vh - 48px));
133
+ display: grid;
134
+ grid-template-rows: auto auto auto auto 1fr;
135
+ overflow: hidden;
136
+ background: var(--bg);
137
+ border: 1px solid var(--border);
138
+ border-radius: 8px;
139
+ box-shadow: 0 16px 48px rgba(31, 35, 40, 0.28);
140
+ }
141
+
142
+ .gdp-palette-label {
143
+ padding: 12px 16px 0;
144
+ color: var(--fg-muted);
145
+ font-size: 12px;
146
+ font-weight: 600;
147
+ text-transform: uppercase;
148
+ }
149
+
150
+ .gdp-palette-input {
151
+ width: 100%;
152
+ border: 0;
153
+ border-bottom: 1px solid var(--border);
154
+ padding: 10px 16px 14px;
155
+ background: transparent;
156
+ color: var(--fg);
157
+ font-size: 24px;
158
+ line-height: 1.25;
159
+ outline: none;
160
+ }
161
+
162
+ .gdp-palette-status {
163
+ min-height: 28px;
164
+ padding: 6px 16px;
165
+ color: var(--fg-muted);
166
+ font-size: 12px;
167
+ border-bottom: 1px solid var(--border-muted);
168
+ }
169
+
170
+ .gdp-palette-controls {
171
+ min-height: 34px;
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 6px;
175
+ padding: 6px 16px 0;
176
+ }
177
+
178
+ .gdp-palette-mode-button {
179
+ height: 24px;
180
+ padding: 0 8px;
181
+ border: 1px solid var(--border);
182
+ border-radius: 6px;
183
+ background: var(--bg);
184
+ color: var(--fg-muted);
185
+ font-size: 12px;
186
+ font-weight: 600;
187
+ cursor: pointer;
188
+ }
189
+
190
+ .gdp-palette-mode-button[aria-pressed="true"] {
191
+ border-color: var(--accent);
192
+ background: var(--accent-subtle);
193
+ color: var(--accent);
194
+ }
195
+
196
+ .gdp-palette-mode-hint {
197
+ color: var(--fg-muted);
198
+ font-size: 12px;
199
+ }
200
+
201
+ .gdp-palette-list {
202
+ overflow: auto;
203
+ padding: 6px;
204
+ }
205
+
206
+ .gdp-palette-row {
207
+ width: 100%;
208
+ min-height: 52px;
209
+ display: grid;
210
+ grid-template-rows: auto auto;
211
+ gap: 2px;
212
+ padding: 8px 10px;
213
+ border: 0;
214
+ border-radius: 6px;
215
+ background: transparent;
216
+ color: var(--fg);
217
+ text-align: left;
218
+ cursor: pointer;
219
+ }
220
+
221
+ .gdp-palette-row[aria-selected="true"] {
222
+ background: var(--accent-subtle);
223
+ }
224
+
225
+ .gdp-palette-row-title {
226
+ overflow: hidden;
227
+ text-overflow: ellipsis;
228
+ white-space: nowrap;
229
+ font-size: 14px;
230
+ font-weight: 600;
231
+ }
232
+
233
+ .gdp-palette-row-detail {
234
+ overflow: hidden;
235
+ text-overflow: ellipsis;
236
+ white-space: nowrap;
237
+ color: var(--fg-muted);
238
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
239
+ font-size: 12px;
240
+ }
241
+
242
+ .gdp-palette-row-detail mark {
243
+ background: var(--attn);
244
+ color: var(--fg-onemphasis);
245
+ border-radius: 2px;
246
+ padding: 0 1px;
247
+ }
248
+
109
249
  /* ===== Global header ===== */
110
250
  #global-header {
111
251
  position: fixed;
@@ -666,6 +806,18 @@ html, body {
666
806
  padding: 0 0 32px;
667
807
  z-index: 30;
668
808
  }
809
+ body[data-focus-scope="sidebar"] #sidebar {
810
+ background: color-mix(in oklab, var(--bg-soft) 97%, var(--scope-accent) 3%);
811
+ outline: 1px solid color-mix(in oklab, var(--scope-accent) 58%, transparent);
812
+ outline-offset: -1px;
813
+ box-shadow: inset -3px 0 0 var(--scope-accent);
814
+ }
815
+ body[data-focus-scope="main"] #content {
816
+ background: color-mix(in oklab, var(--bg) 98%, var(--scope-accent) 2%);
817
+ outline: 1px solid color-mix(in oklab, var(--scope-accent) 58%, transparent);
818
+ outline-offset: -1px;
819
+ box-shadow: inset 3px 0 0 var(--scope-accent);
820
+ }
669
821
  .sb-head {
670
822
  display: flex; justify-content: space-between; align-items: center;
671
823
  padding: 14px 16px 10px;
@@ -788,6 +940,140 @@ body.gdp-resizing #sidebar-resizer {
788
940
  body.gdp-resizing { cursor: col-resize !important; user-select: none; }
789
941
  body.gdp-resizing * { user-select: none !important; }
790
942
 
943
+ body.gdp-help-page #topbar,
944
+ body.gdp-help-page #sidebar,
945
+ body.gdp-help-page #sidebar-resizer {
946
+ display: none;
947
+ }
948
+ body.gdp-help-page #content {
949
+ margin-left: 0;
950
+ padding-top: calc(var(--global-header-h) + 28px);
951
+ }
952
+ .gdp-help-shell {
953
+ width: 100%;
954
+ max-width: none;
955
+ margin: 0;
956
+ }
957
+ .gdp-help-header {
958
+ display: flex;
959
+ align-items: center;
960
+ justify-content: space-between;
961
+ gap: 16px;
962
+ margin-bottom: 22px;
963
+ }
964
+ .gdp-help-header h1 {
965
+ margin: 0;
966
+ font-size: 28px;
967
+ }
968
+ .gdp-help-language {
969
+ appearance: none;
970
+ min-width: 92px;
971
+ height: 34px;
972
+ border: 1px solid var(--border);
973
+ border-radius: 6px;
974
+ background:
975
+ linear-gradient(45deg, transparent 50%, var(--fg-muted) 50%) right 15px center / 5px 5px no-repeat,
976
+ linear-gradient(135deg, var(--fg-muted) 50%, transparent 50%) right 10px center / 5px 5px no-repeat,
977
+ var(--bg);
978
+ color: var(--fg);
979
+ padding: 0 34px 0 12px;
980
+ font: inherit;
981
+ font-size: 13px;
982
+ line-height: 32px;
983
+ cursor: pointer;
984
+ }
985
+ .gdp-help-language:hover {
986
+ border-color: var(--border-strong);
987
+ }
988
+ .gdp-help-language:focus {
989
+ border-color: var(--accent);
990
+ outline: none;
991
+ box-shadow: 0 0 0 3px var(--accent-muted);
992
+ }
993
+ .gdp-help-layout {
994
+ display: grid;
995
+ grid-template-columns: 220px minmax(0, 1fr);
996
+ gap: 28px;
997
+ align-items: start;
998
+ }
999
+ .gdp-help-nav {
1000
+ position: sticky;
1001
+ top: calc(var(--global-header-h) + 20px);
1002
+ display: grid;
1003
+ gap: 4px;
1004
+ padding-right: 16px;
1005
+ border-right: 1px solid var(--border-muted);
1006
+ }
1007
+ .gdp-help-nav button {
1008
+ appearance: none;
1009
+ border: 0;
1010
+ border-left: 3px solid transparent;
1011
+ border-radius: 0 6px 6px 0;
1012
+ background: transparent;
1013
+ color: var(--fg-muted);
1014
+ padding: 8px 10px;
1015
+ text-align: left;
1016
+ font: inherit;
1017
+ cursor: pointer;
1018
+ }
1019
+ .gdp-help-nav button:hover {
1020
+ background: var(--bg-mute);
1021
+ color: var(--fg);
1022
+ }
1023
+ .gdp-help-nav button.active {
1024
+ border-left-color: var(--accent);
1025
+ background: var(--accent-subtle);
1026
+ color: var(--accent-emph);
1027
+ font-weight: 700;
1028
+ }
1029
+ .gdp-help-content h2 {
1030
+ margin: 0 0 8px;
1031
+ font-size: 24px;
1032
+ }
1033
+ .gdp-help-content > p {
1034
+ margin: 0 0 24px;
1035
+ color: var(--fg-muted);
1036
+ }
1037
+ .gdp-help-group {
1038
+ margin: 0 0 28px;
1039
+ }
1040
+ .gdp-help-group h3 {
1041
+ margin: 0 0 10px;
1042
+ font-size: 16px;
1043
+ }
1044
+ .gdp-help-group table {
1045
+ width: 100%;
1046
+ border-collapse: collapse;
1047
+ border-top: 1px solid var(--border-muted);
1048
+ }
1049
+ .gdp-help-group th,
1050
+ .gdp-help-group td {
1051
+ padding: 11px 8px;
1052
+ border-bottom: 1px solid var(--border-muted);
1053
+ vertical-align: top;
1054
+ }
1055
+ .gdp-help-group th {
1056
+ width: 230px;
1057
+ color: var(--fg);
1058
+ font-weight: 600;
1059
+ text-align: left;
1060
+ }
1061
+ .gdp-help-group td {
1062
+ color: var(--fg-muted);
1063
+ }
1064
+ .gdp-help-group kbd {
1065
+ display: inline-block;
1066
+ min-width: 24px;
1067
+ padding: 2px 6px;
1068
+ border: 1px solid var(--border);
1069
+ border-bottom-color: var(--border-strong);
1070
+ border-radius: 5px;
1071
+ background: var(--bg-soft);
1072
+ color: var(--fg);
1073
+ font: 12px "Monaspace Neon", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
1074
+ text-align: center;
1075
+ }
1076
+
791
1077
  /* ===== Tree view ===== */
792
1078
  #filelist.tree {
793
1079
  padding: 4px 0 0;
@@ -1286,6 +1572,17 @@ table.d2h-diff-table .d2h-code-line-prefix {
1286
1572
  width: 1.5em;
1287
1573
  padding-left: 4px;
1288
1574
  }
1575
+
1576
+ table.d2h-diff-table tr.gdp-diff-line-target > td,
1577
+ table.d2h-diff-table tr.gdp-diff-line-target .d2h-code-line,
1578
+ table.d2h-diff-table tr.gdp-diff-line-target .d2h-code-side-line,
1579
+ table.d2h-diff-table tr.gdp-diff-line-target .d2h-code-line-ctn {
1580
+ background: var(--line-hit-bg) !important;
1581
+ background-color: var(--line-hit-bg) !important;
1582
+ }
1583
+ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
1584
+ box-shadow: inset 3px 0 0 var(--line-hit-border), inset -1px 0 0 var(--border-muted);
1585
+ }
1289
1586
  /* Stack height = number of buttons * 20px. With 1 button = 20px row,
1290
1587
  * with 2 (↑+↓) = 40px row, matching GitHub. */
1291
1588
  .gdp-expand-stack {
@@ -1370,6 +1667,8 @@ table.d2h-diff-table .d2h-code-line-prefix {
1370
1667
  }
1371
1668
  .gdp-copy-path.copied { color: var(--success); }
1372
1669
  .gdp-copy-path.failed { color: var(--danger); }
1670
+ .gdp-copy-source.copied { color: var(--success); }
1671
+ .gdp-copy-source.failed { color: var(--danger); }
1373
1672
  .gdp-open-path.opened { color: var(--success); }
1374
1673
  .gdp-open-path.failed { color: var(--danger); }
1375
1674
  .gdp-file-unfold[aria-pressed="true"] {
@@ -1589,6 +1888,23 @@ table.d2h-diff-table .d2h-code-line-prefix {
1589
1888
  color: var(--fg);
1590
1889
  border-bottom-color: var(--blob-tab-active);
1591
1890
  }
1891
+ .gdp-source-tabs .gdp-copy-source {
1892
+ margin-left: auto;
1893
+ align-self: center;
1894
+ width: 28px;
1895
+ height: 28px;
1896
+ padding: 0;
1897
+ border: 1px solid transparent;
1898
+ border-radius: 6px;
1899
+ }
1900
+ .gdp-source-virtual-copy {
1901
+ flex: 0 0 auto;
1902
+ width: 28px;
1903
+ height: 28px;
1904
+ padding: 0;
1905
+ border: 1px solid transparent;
1906
+ border-radius: 6px;
1907
+ }
1592
1908
  .gdp-source-viewer > [hidden],
1593
1909
  .gdp-markdown-layout[hidden],
1594
1910
  .gdp-source-table[hidden] {
@@ -1602,6 +1918,52 @@ table.d2h-diff-table .d2h-code-line-prefix {
1602
1918
  font-size: 12px;
1603
1919
  line-height: 20px;
1604
1920
  }
1921
+ .gdp-source-table tr.gdp-source-line-target {
1922
+ background: var(--line-hit-bg);
1923
+ }
1924
+ .gdp-source-table tr.gdp-source-cursor .gdp-source-line-number,
1925
+ .gdp-source-virtual-row.gdp-source-cursor .gdp-source-virtual-line-number {
1926
+ color: var(--fg);
1927
+ box-shadow: inset 3px 0 0 var(--scope-accent), inset -1px 0 0 var(--border-muted);
1928
+ }
1929
+ .gdp-source-table tr.gdp-source-cursor .gdp-source-line-code,
1930
+ .gdp-source-virtual-row.gdp-source-cursor .gdp-source-virtual-line-code {
1931
+ background: color-mix(in oklab, var(--bg) 90%, var(--scope-accent) 10%) !important;
1932
+ }
1933
+ .gdp-source-table tr.gdp-source-cursor .gdp-source-line-code::before,
1934
+ .gdp-source-virtual-row.gdp-source-cursor .gdp-source-virtual-line-code::before {
1935
+ content: "";
1936
+ display: inline-block;
1937
+ width: 2px;
1938
+ height: 1.05em;
1939
+ margin: 0 8px 0 -8px;
1940
+ vertical-align: -0.16em;
1941
+ background: var(--scope-accent);
1942
+ }
1943
+ .gdp-source-table tr.gdp-source-line-target .gdp-source-line-number,
1944
+ .gdp-source-table tr.gdp-source-line-target .gdp-source-line-code,
1945
+ .gdp-source-virtual-row.gdp-source-line-target {
1946
+ background: var(--line-hit-bg) !important;
1947
+ background-color: var(--line-hit-bg) !important;
1948
+ }
1949
+ .gdp-source-table tr.gdp-source-line-target > td.gdp-source-line-number,
1950
+ .gdp-source-virtual-row.gdp-source-line-target > .gdp-source-virtual-line-number {
1951
+ background: var(--line-hit-bg) !important;
1952
+ background-color: var(--line-hit-bg) !important;
1953
+ }
1954
+ .gdp-source-table tr.gdp-source-line-target .gdp-source-line-number,
1955
+ .gdp-source-virtual-row.gdp-source-line-target .gdp-source-virtual-line-number {
1956
+ box-shadow: inset 3px 0 0 var(--line-hit-border), inset -1px 0 0 var(--border-muted);
1957
+ }
1958
+ .gdp-source-line-target .gdp-source-line-code,
1959
+ .gdp-source-line-target .gdp-source-virtual-line-code {
1960
+ background: var(--line-hit-bg) !important;
1961
+ background-color: var(--line-hit-bg) !important;
1962
+ }
1963
+ .gdp-source-line-target .gdp-source-line-code.shiki span,
1964
+ .gdp-source-line-target .gdp-source-virtual-line-code.hljs span {
1965
+ background: transparent !important;
1966
+ }
1605
1967
  .gdp-standalone-source .gdp-source-table {
1606
1968
  display: block;
1607
1969
  overflow: auto;
@@ -1660,6 +2022,11 @@ table.d2h-diff-table .d2h-code-line-prefix {
1660
2022
  .gdp-source-line-code.hljs {
1661
2023
  background: var(--bg);
1662
2024
  }
2025
+ .gdp-source-line-target .gdp-source-line-code.hljs,
2026
+ .gdp-source-line-target .gdp-source-line-code.shiki,
2027
+ .gdp-source-line-target .gdp-source-virtual-line-code.hljs {
2028
+ background: var(--line-hit-bg);
2029
+ }
1663
2030
  .gdp-source-line-code.shiki,
1664
2031
  .gdp-source-line-code.shiki span {
1665
2032
  color: var(--shiki-light) !important;
@@ -1726,6 +2093,9 @@ table.d2h-diff-table .d2h-code-line-prefix {
1726
2093
  border-color: var(--accent);
1727
2094
  color: var(--accent);
1728
2095
  }
2096
+ .gdp-source-virtual-copy:hover {
2097
+ border-color: var(--border);
2098
+ }
1729
2099
  .gdp-source-virtual-scroller {
1730
2100
  position: relative;
1731
2101
  overflow: auto;
@@ -1733,6 +2103,8 @@ table.d2h-diff-table .d2h-code-line-prefix {
1733
2103
  font-family: "Monaspace Neon", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
1734
2104
  font-size: 12px;
1735
2105
  line-height: 20px;
2106
+ cursor: pointer;
2107
+ user-select: none;
1736
2108
  }
1737
2109
  .gdp-source-virtual-spacer {
1738
2110
  position: relative;
@@ -1760,6 +2132,7 @@ table.d2h-diff-table .d2h-code-line-prefix {
1760
2132
  background: var(--bg);
1761
2133
  box-shadow: inset -1px 0 0 var(--border-muted);
1762
2134
  font-variant-numeric: tabular-nums;
2135
+ cursor: pointer;
1763
2136
  user-select: none;
1764
2137
  }
1765
2138
  .gdp-source-virtual-line-code {
package/web-src/routes.ts CHANGED
@@ -3,6 +3,13 @@ export type DiffRange = {
3
3
  to: string;
4
4
  };
5
5
 
6
+ export type SourceLineRange = {
7
+ start: number;
8
+ end: number;
9
+ };
10
+
11
+ export type SourceLineTarget = number | SourceLineRange;
12
+
6
13
  export type SourceFileTarget = {
7
14
  path: string;
8
15
  ref: string;
@@ -10,11 +17,12 @@ export type SourceFileTarget = {
10
17
 
11
18
  export type AppRoute =
12
19
  | { screen: 'repo'; ref: string; path: string; range: DiffRange }
13
- | { screen: 'diff'; range: DiffRange }
14
- | { screen: 'file'; path: string; ref: string; range: DiffRange; view?: 'blob' | 'detail' }
20
+ | { screen: 'diff'; range: DiffRange; path?: string; line?: SourceLineTarget }
21
+ | { screen: 'file'; path: string; ref: string; range: DiffRange; view?: 'blob' | 'detail'; line?: SourceLineTarget }
22
+ | { screen: 'help'; range: DiffRange; lang: string; section: string }
15
23
  | { screen: 'unknown'; reason: 'unknown-pathname' | 'missing-path'; rawPathname: string; rawSearch: string; range: DiffRange };
16
24
 
17
- export const SPA_PATHS = ['/todif', '/todiff', '/file'] as const;
25
+ export const SPA_PATHS = ['/todif', '/todiff', '/file', '/help'] as const;
18
26
  export const APP_ENTRY_PATHS = ['/', '/index.html'] as const;
19
27
 
20
28
  export function assertNever(value: never): never {
@@ -31,6 +39,25 @@ function parseLegacyRange(value: string | null | undefined, fallback: DiffRange)
31
39
  };
32
40
  }
33
41
 
42
+ function parseLineTarget(value: string | null | undefined): SourceLineTarget | undefined {
43
+ const raw = value || '';
44
+ const range = /^(\d+)-(\d+)$/.exec(raw);
45
+ if (range) {
46
+ const a = Number(range[1]);
47
+ const b = Number(range[2]);
48
+ const start = Math.min(a, b);
49
+ const end = Math.max(a, b);
50
+ if (start > 0) return { start, end };
51
+ return undefined;
52
+ }
53
+ const line = Number(raw);
54
+ return Number.isInteger(line) && line > 0 ? line : undefined;
55
+ }
56
+
57
+ function formatLineTarget(line: SourceLineTarget): string {
58
+ return typeof line === 'number' ? String(line) : line.start + '-' + line.end;
59
+ }
60
+
34
61
  export function parseRoute(pathname: string, search: string, fallbackRange: DiffRange): AppRoute {
35
62
  const params = new URLSearchParams(search);
36
63
  const legacyRange = parseLegacyRange(params.get('range'), fallbackRange);
@@ -49,14 +76,27 @@ export function parseRoute(pathname: string, search: string, fallbackRange: Diff
49
76
  };
50
77
  case '/todif':
51
78
  case '/todiff':
52
- return { screen: 'diff', range };
79
+ return {
80
+ screen: 'diff',
81
+ range,
82
+ ...(params.get('path') ? { path: params.get('path') || '' } : {}),
83
+ ...(parseLineTarget(params.get('line')) ? { line: parseLineTarget(params.get('line')) } : {}),
84
+ };
53
85
  case '/file': {
54
86
  const path = params.get('path') || '';
55
87
  const target = params.get('target') || '';
56
88
  const ref = target || params.get('ref') || 'worktree';
89
+ const line = parseLineTarget(params.get('line'));
57
90
  if (!path) return { screen: 'unknown', reason: 'missing-path', rawPathname: pathname, rawSearch: search, range };
58
- return { screen: 'file', path, ref, range, view: target ? 'blob' : 'detail' };
91
+ return { screen: 'file', path, ref, range, view: target ? 'blob' : 'detail', ...(line ? { line } : {}) };
59
92
  }
93
+ case '/help':
94
+ return {
95
+ screen: 'help',
96
+ range,
97
+ lang: params.get('lang') || 'en',
98
+ section: params.get('section') || 'keybindings',
99
+ };
60
100
  default:
61
101
  return { screen: 'unknown', reason: 'unknown-pathname', rawPathname: pathname, rawSearch: search, range };
62
102
  }
@@ -74,15 +114,26 @@ export function buildRoute(route: AppRoute): string {
74
114
  case 'file':
75
115
  if (route.view === 'blob') {
76
116
  return '/file?path=' + encodeURIComponent(route.path) +
77
- '&target=' + encodeURIComponent(route.ref || 'worktree');
117
+ '&target=' + encodeURIComponent(route.ref || 'worktree') +
118
+ (route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
78
119
  }
79
120
  return '/file?path=' + encodeURIComponent(route.path) +
80
121
  '&ref=' + encodeURIComponent(route.ref || 'worktree') +
81
122
  '&from=' + encodeURIComponent(route.range.from || '') +
82
- '&to=' + encodeURIComponent(route.range.to || 'worktree');
123
+ '&to=' + encodeURIComponent(route.range.to || 'worktree') +
124
+ (route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
83
125
  case 'diff':
84
126
  return '/todif?from=' + encodeURIComponent(route.range.from || '') +
85
- '&to=' + encodeURIComponent(route.range.to || 'worktree');
127
+ '&to=' + encodeURIComponent(route.range.to || 'worktree') +
128
+ (route.path ? '&path=' + encodeURIComponent(route.path) : '') +
129
+ (route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
130
+ case 'help': {
131
+ const params = new URLSearchParams();
132
+ if (route.lang && route.lang !== 'en') params.set('lang', route.lang);
133
+ if (route.section && route.section !== 'keybindings') params.set('section', route.section);
134
+ const qs = params.toString();
135
+ return '/help' + (qs ? '?' + qs : '');
136
+ }
86
137
  case 'unknown':
87
138
  return '/todif?from=' + encodeURIComponent(route.range.from || '') +
88
139
  '&to=' + encodeURIComponent(route.range.to || 'worktree');