@youtyan/code-viewer 0.1.43 → 0.1.45

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.
@@ -633,6 +633,27 @@ function refCommits(cwd, query = "", max = DEFAULT_REF_COMMIT_LIMIT) {
633
633
  ]);
634
634
  return mergeCommitResults(limit, hashMatches, subjectMatches, authorMatches);
635
635
  }
636
+ function parseRemoteWebUrl(remote) {
637
+ const raw = (remote || "").trim();
638
+ if (!raw)
639
+ return null;
640
+ const sshShorthand = /^[\w.-]+@([\w.-]+):(.+?)(?:\.git)?\/?$/.exec(raw);
641
+ if (sshShorthand)
642
+ return `https://${sshShorthand[1]}/${sshShorthand[2]}`;
643
+ const sshUrl = /^ssh:\/\/(?:[\w.-]+@)?([\w.-]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/.exec(raw);
644
+ if (sshUrl)
645
+ return `https://${sshUrl[1]}/${sshUrl[2]}`;
646
+ const httpUrl = /^https?:\/\/([\w.-]+)\/(.+?)(?:\.git)?\/?$/.exec(raw);
647
+ if (httpUrl)
648
+ return `https://${httpUrl[1]}/${httpUrl[2]}`;
649
+ return null;
650
+ }
651
+ function remoteWebUrl(cwd) {
652
+ const res = run(["git", "remote", "get-url", "origin"], cwd);
653
+ if (res.code !== 0)
654
+ return null;
655
+ return parseRemoteWebUrl(res.stdout.trim());
656
+ }
636
657
  function parseHistoryLog(stdout) {
637
658
  const parts = stdout.split("\x00");
638
659
  const commits = [];
@@ -3104,6 +3125,7 @@ function handleTree(url) {
3104
3125
  function handleSettings() {
3105
3126
  return json({
3106
3127
  project: basename2(cwd),
3128
+ repo_web_url: remoteWebUrl(cwd),
3107
3129
  scope: {
3108
3130
  omit_dirs_effective: scopeOmitDirNames,
3109
3131
  omit_dirs_built_in: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -7698,6 +7698,114 @@ ${frontmatter.yaml}
7698
7698
  };
7699
7699
  }
7700
7700
 
7701
+ // web-src/views/diff-line-select.ts
7702
+ var SELECTED_CLASS = "gdp-diff-line-selected";
7703
+ function cardPath(el) {
7704
+ return el.closest(".gdp-file-shell[data-path]")?.dataset.path || "";
7705
+ }
7706
+ function afterLineFromCell(cell) {
7707
+ const sideCell = cell.closest("td.d2h-code-side-linenumber");
7708
+ if (sideCell) {
7709
+ const side = sideCell.closest(".d2h-file-side-diff");
7710
+ const wrapper = sideCell.closest(".d2h-file-wrapper");
7711
+ if (!side || !wrapper)
7712
+ return null;
7713
+ const sides = wrapper.querySelectorAll(".d2h-file-side-diff");
7714
+ if (sides.length < 2 || side !== sides[1])
7715
+ return null;
7716
+ const line2 = Number((sideCell.textContent || "").trim());
7717
+ return Number.isInteger(line2) && line2 > 0 ? line2 : null;
7718
+ }
7719
+ const numCell = cell.closest("td.d2h-code-linenumber");
7720
+ if (!numCell)
7721
+ return null;
7722
+ const raw = (numCell.querySelector(".line-num2")?.textContent || "").trim();
7723
+ const line = Number(raw);
7724
+ return Number.isInteger(line) && line > 0 ? line : null;
7725
+ }
7726
+ function rowsWithAfterLines(card) {
7727
+ const out = [];
7728
+ card.querySelectorAll("table.d2h-diff-table tr").forEach((row) => {
7729
+ const cell = row.querySelector("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
7730
+ if (!cell)
7731
+ return;
7732
+ const line = afterLineFromCell(cell);
7733
+ if (line !== null)
7734
+ out.push({ row, line });
7735
+ });
7736
+ return out;
7737
+ }
7738
+ function createDiffLineSelect(deps) {
7739
+ let drag = null;
7740
+ let selection = null;
7741
+ function clearHighlights() {
7742
+ document.querySelectorAll(`.${SELECTED_CLASS}`).forEach((row) => {
7743
+ row.classList.remove(SELECTED_CLASS);
7744
+ });
7745
+ }
7746
+ function applySelection(next) {
7747
+ selection = next;
7748
+ clearHighlights();
7749
+ if (!next) {
7750
+ deps.pill.hide();
7751
+ return;
7752
+ }
7753
+ const start = Math.min(next.start, next.end);
7754
+ const end = Math.max(next.start, next.end);
7755
+ const card = document.querySelector(`.gdp-file-shell[data-path="${CSS.escape(next.path)}"]`);
7756
+ if (card) {
7757
+ for (const item of rowsWithAfterLines(card)) {
7758
+ if (item.line >= start && item.line <= end)
7759
+ item.row.classList.add(SELECTED_CLASS);
7760
+ }
7761
+ }
7762
+ deps.pill.show(next.path, start, end);
7763
+ }
7764
+ function clear() {
7765
+ drag = null;
7766
+ applySelection(null);
7767
+ }
7768
+ const diff = document.querySelector("#diff");
7769
+ if (!diff)
7770
+ return { clear };
7771
+ diff.addEventListener("mousedown", (e2) => {
7772
+ const target = e2.target;
7773
+ const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
7774
+ if (!cell)
7775
+ return;
7776
+ const line = afterLineFromCell(cell);
7777
+ const path = cardPath(cell);
7778
+ if (line === null || !path) {
7779
+ if (selection)
7780
+ clear();
7781
+ return;
7782
+ }
7783
+ e2.preventDefault();
7784
+ drag = { path, start: line };
7785
+ applySelection({ path, start: line, end: line });
7786
+ });
7787
+ diff.addEventListener("mouseover", (e2) => {
7788
+ if (!drag)
7789
+ return;
7790
+ const target = e2.target;
7791
+ const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
7792
+ if (!cell || cardPath(cell) !== drag.path)
7793
+ return;
7794
+ const line = afterLineFromCell(cell);
7795
+ if (line === null)
7796
+ return;
7797
+ applySelection({ path: drag.path, start: drag.start, end: line });
7798
+ });
7799
+ document.addEventListener("mouseup", () => {
7800
+ drag = null;
7801
+ });
7802
+ document.addEventListener("keydown", (e2) => {
7803
+ if (e2.key === "Escape" && selection && !drag)
7804
+ clear();
7805
+ });
7806
+ return { clear };
7807
+ }
7808
+
7701
7809
  // web-src/core/file-path-copy.ts
7702
7810
  function filePathClipboardText(path) {
7703
7811
  return path || "";
@@ -7708,6 +7816,13 @@ ${frontmatter.yaml}
7708
7816
  const parts = path.split("/").filter(Boolean);
7709
7817
  return parts[parts.length - 1] || "";
7710
7818
  }
7819
+ function fileReferenceClipboardText(path, start, end) {
7820
+ if (!path)
7821
+ return "";
7822
+ const a2 = Math.max(1, Math.floor(Math.min(start, end)));
7823
+ const b2 = Math.max(1, Math.floor(Math.max(start, end)));
7824
+ return a2 === b2 ? `@${path}#${a2}` : `@${path}#${a2}-${b2}`;
7825
+ }
7711
7826
 
7712
7827
  // web-src/core/ws-highlight.ts
7713
7828
  function isWhitespaceOnlyInlineHighlight(text2) {
@@ -9769,6 +9884,76 @@ ${frontmatter.yaml}
9769
9884
  return { setupHunkExpand };
9770
9885
  }
9771
9886
 
9887
+ // web-src/views/line-ref-pill.ts
9888
+ var COPY_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="currentColor">' + '<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>' + '<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>' + "</svg>";
9889
+ var CHECK_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="currentColor">' + '<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>' + "</svg>";
9890
+ function escapeHtml2(value) {
9891
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
9892
+ }
9893
+ function createLineRefPill() {
9894
+ const pill = document.createElement("button");
9895
+ pill.id = "line-ref-pill";
9896
+ pill.type = "button";
9897
+ pill.title = "選択行の参照をコピー(Claude Code / Codex に貼り付け用)";
9898
+ pill.hidden = true;
9899
+ document.body.appendChild(pill);
9900
+ let refText = "";
9901
+ let feedbackTimer = null;
9902
+ function render(state) {
9903
+ pill.classList.toggle("copied", state === "copied");
9904
+ if (state === "copied") {
9905
+ pill.innerHTML = `${CHECK_ICON}<span class="lrp-label">Copied!</span>`;
9906
+ return;
9907
+ }
9908
+ if (state === "failed") {
9909
+ pill.innerHTML = `${COPY_ICON}<span class="lrp-label">copy failed</span>`;
9910
+ return;
9911
+ }
9912
+ pill.innerHTML = `${COPY_ICON}<span class="lrp-label">Copy</span>` + `<span class="lrp-ref">${escapeHtml2(refText)}</span>`;
9913
+ }
9914
+ pill.addEventListener("click", async () => {
9915
+ if (!refText)
9916
+ return;
9917
+ try {
9918
+ await navigator.clipboard.writeText(refText);
9919
+ render("copied");
9920
+ } catch {
9921
+ render("failed");
9922
+ }
9923
+ if (feedbackTimer)
9924
+ clearTimeout(feedbackTimer);
9925
+ feedbackTimer = setTimeout(() => {
9926
+ feedbackTimer = null;
9927
+ if (!pill.hidden)
9928
+ render("ready");
9929
+ }, 1200);
9930
+ });
9931
+ return {
9932
+ show(path, start, end) {
9933
+ const next = fileReferenceClipboardText(path, start, end);
9934
+ if (!next)
9935
+ return;
9936
+ const changed = next !== refText;
9937
+ refText = next;
9938
+ if (feedbackTimer) {
9939
+ clearTimeout(feedbackTimer);
9940
+ feedbackTimer = null;
9941
+ }
9942
+ render("ready");
9943
+ if (pill.hidden || changed) {
9944
+ pill.classList.remove("pop");
9945
+ pill.offsetWidth;
9946
+ pill.classList.add("pop");
9947
+ }
9948
+ pill.hidden = false;
9949
+ },
9950
+ hide() {
9951
+ refText = "";
9952
+ pill.hidden = true;
9953
+ }
9954
+ };
9955
+ }
9956
+
9772
9957
  // web-src/views/ref-picker.ts
9773
9958
  function createRefPicker(deps) {
9774
9959
  function wireRefSelectorInput(input, onPick) {
@@ -15126,6 +15311,11 @@ ${frontmatter.yaml}
15126
15311
  return null;
15127
15312
  const settings = await res.json();
15128
15313
  setProjectName(settings.project || "");
15314
+ const repoLink = document.querySelector("#repo-web-link");
15315
+ if (repoLink && settings.repo_web_url) {
15316
+ repoLink.href = settings.repo_web_url;
15317
+ repoLink.hidden = false;
15318
+ }
15129
15319
  SERVER_SCOPE_OMIT_DIRS_DEFAULT = normalizeScopeOmitDirs(settings.scope.omit_dirs_effective);
15130
15320
  SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = normalizeScopeExcludeNames(settings.scope.exclude_names_effective);
15131
15321
  return settings;
@@ -15164,6 +15354,19 @@ ${frontmatter.yaml}
15164
15354
  let highlightConfigured = false;
15165
15355
  let PROJECT_NAME = "";
15166
15356
  let REPO_SIDEBAR_REF = null;
15357
+ const LINE_REF_PILL = createLineRefPill();
15358
+ const DIFF_LINE_SELECT = createDiffLineSelect({ pill: LINE_REF_PILL });
15359
+ function syncLineRefPill() {
15360
+ const route = STATE.route;
15361
+ if (route.screen === "diff")
15362
+ return;
15363
+ DIFF_LINE_SELECT.clear();
15364
+ if (route.screen === "file" && route.line) {
15365
+ const start = typeof route.line === "number" ? route.line : route.line.start;
15366
+ const end = typeof route.line === "number" ? route.line : route.line.end;
15367
+ LINE_REF_PILL.show(route.path, start, end);
15368
+ }
15369
+ }
15167
15370
  const SIDEBAR = createSidebar({
15168
15371
  $,
15169
15372
  $$,
@@ -15553,7 +15756,7 @@ ${frontmatter.yaml}
15553
15756
  throw e2;
15554
15757
  });
15555
15758
  }
15556
- function escapeHtml2(s2) {
15759
+ function escapeHtml3(s2) {
15557
15760
  return String(s2 == null ? "" : s2).replace(/[&<>"']/g, (c2) => ({
15558
15761
  "&": "&amp;",
15559
15762
  "<": "&lt;",
@@ -15571,6 +15774,19 @@ ${frontmatter.yaml}
15571
15774
  to: STATE.to || DEFAULT_RANGE.to
15572
15775
  };
15573
15776
  }
15777
+ let preHistoryRange = null;
15778
+ function parkRangeForHistory() {
15779
+ if (preHistoryRange === null)
15780
+ preHistoryRange = { from: STATE.from, to: STATE.to };
15781
+ }
15782
+ function restoreRangeAfterHistory() {
15783
+ if (!preHistoryRange)
15784
+ return;
15785
+ STATE.from = preHistoryRange.from;
15786
+ STATE.to = preHistoryRange.to;
15787
+ preHistoryRange = null;
15788
+ syncRefInputs();
15789
+ }
15574
15790
  function repoFileTargetFromRoute() {
15575
15791
  return STATE.route.screen === "file" && STATE.route.view === "blob" ? STATE.route.ref : null;
15576
15792
  }
@@ -15601,6 +15817,7 @@ ${frontmatter.yaml}
15601
15817
  else
15602
15818
  history.pushState(state, "", url);
15603
15819
  syncHeaderMenu();
15820
+ syncLineRefPill();
15604
15821
  }
15605
15822
  function setPageMode() {
15606
15823
  document.body.classList.toggle("gdp-file-detail-page", STATE.route.screen === "file");
@@ -15633,7 +15850,10 @@ ${frontmatter.yaml}
15633
15850
  });
15634
15851
  }
15635
15852
  if (link2.dataset.route === "diff") {
15636
- link2.href = buildRoute({ screen: "diff", range: currentRange() });
15853
+ link2.href = buildRoute({
15854
+ screen: "diff",
15855
+ range: preHistoryRange ?? currentRange()
15856
+ });
15637
15857
  }
15638
15858
  if (link2.dataset.route === "history") {
15639
15859
  link2.href = buildRoute({
@@ -15763,7 +15983,7 @@ ${frontmatter.yaml}
15763
15983
  STATE,
15764
15984
  setRoute,
15765
15985
  currentRange,
15766
- escapeHtml: escapeHtml2,
15986
+ escapeHtml: escapeHtml3,
15767
15987
  trackLoad,
15768
15988
  diffCardSelector,
15769
15989
  getHljs,
@@ -16169,10 +16389,12 @@ ${frontmatter.yaml}
16169
16389
  setStatus("live");
16170
16390
  applySourceRouteToShell();
16171
16391
  } else if (STATE.route.screen === "history") {
16392
+ parkRangeForHistory();
16172
16393
  setStatus("live");
16173
16394
  HISTORY_VIEW.enterHistory();
16174
16395
  } else
16175
16396
  load();
16397
+ syncLineRefPill();
16176
16398
  });
16177
16399
  function syncRefInputs() {
16178
16400
  const fi = $("#ref-from"), ti = $("#ref-to");
@@ -16182,6 +16404,7 @@ ${frontmatter.yaml}
16182
16404
  ti.value = STATE.to;
16183
16405
  }
16184
16406
  function setRange(from, to) {
16407
+ preHistoryRange = null;
16185
16408
  STATE.from = from || "";
16186
16409
  STATE.to = to || "";
16187
16410
  localStorage.setItem("gdp:from", STATE.from);
@@ -16208,7 +16431,7 @@ ${frontmatter.yaml}
16208
16431
  syncHeaderMenu();
16209
16432
  const HISTORY_VIEW = createHistoryView({
16210
16433
  $,
16211
- escapeHtml: escapeHtml2,
16434
+ escapeHtml: escapeHtml3,
16212
16435
  getRoute: () => STATE.route,
16213
16436
  setRoute,
16214
16437
  applyCommitRange: (range) => {
@@ -16237,7 +16460,7 @@ ${frontmatter.yaml}
16237
16460
  });
16238
16461
  const REF_PICKER = createRefPicker({
16239
16462
  $,
16240
- escapeHtml: escapeHtml2,
16463
+ escapeHtml: escapeHtml3,
16241
16464
  currentRange,
16242
16465
  setRange,
16243
16466
  setRoute,
@@ -16256,6 +16479,9 @@ ${frontmatter.yaml}
16256
16479
  }
16257
16480
  $("#ref-reset").addEventListener("click", () => setRange("HEAD", "worktree"));
16258
16481
  function applyRouteFromLocation() {
16482
+ if (STATE.route.screen === "history" && window.location.pathname !== "/history") {
16483
+ restoreRangeAfterHistory();
16484
+ }
16259
16485
  const parsedRoute = parseRoute(window.location.pathname, window.location.search, currentRange());
16260
16486
  STATE.route = parsedRoute.screen === "unknown" ? { screen: "diff", range: parsedRoute.range } : parsedRoute;
16261
16487
  STATE.from = STATE.route.range.from;
@@ -16265,6 +16491,7 @@ ${frontmatter.yaml}
16265
16491
  ANNOTATIONS_UI?.restoreSessionFromUrl();
16266
16492
  syncRefInputs();
16267
16493
  syncHeaderMenu();
16494
+ syncLineRefPill();
16268
16495
  if (STATE.route.screen === "help") {
16269
16496
  cancelActiveSourceLoad("navigation");
16270
16497
  setPageMode();
@@ -16280,6 +16507,7 @@ ${frontmatter.yaml}
16280
16507
  return;
16281
16508
  }
16282
16509
  if (STATE.route.screen === "history") {
16510
+ parkRangeForHistory();
16283
16511
  cancelActiveSourceLoad("navigation");
16284
16512
  setPageMode();
16285
16513
  removeStandaloneSource();
@@ -16297,6 +16525,8 @@ ${frontmatter.yaml}
16297
16525
  }
16298
16526
  window.addEventListener("popstate", applyRouteFromLocation);
16299
16527
  document.querySelectorAll(".app-menu-item, .global-help-link").forEach((link2) => {
16528
+ if (link2.target === "_blank")
16529
+ return;
16300
16530
  link2.addEventListener("click", (e2) => {
16301
16531
  if (e2.metaKey || e2.ctrlKey || e2.shiftKey || e2.altKey || e2.button !== 0)
16302
16532
  return;
package/web/index.html CHANGED
@@ -33,6 +33,7 @@
33
33
  <button id="viewer-settings" class="global-icon-action" title="viewer settings" aria-label="viewer settings"></button>
34
34
  <button id="theme" title="toggle theme">🌗</button>
35
35
  <span id="status" class="status"></span>
36
+ <a id="repo-web-link" class="global-help-link" href="#" target="_blank" rel="noopener" hidden>GitHub</a>
36
37
  <a class="global-help-link" data-route="help" href="/help">Help</a>
37
38
  <span class="product-label" aria-hidden="true">code viewer</span>
38
39
  </div>
package/web/style.css CHANGED
@@ -926,6 +926,11 @@ body.gdp-history-page.gdp-sidebar-hidden main#content {
926
926
  body.gdp-history-page #sidebar-resizer {
927
927
  left: calc(var(--history-w) + var(--sidebar-w) - 4px);
928
928
  }
929
+ /* The history screen drives its diff range from the commit list; the topbar
930
+ from/to pickers belong to the Diff Viewer and are hidden here. */
931
+ body.gdp-history-page #topbar .ref-pickers {
932
+ display: none;
933
+ }
929
934
  #history-panel[hidden] {
930
935
  display: none !important;
931
936
  }
@@ -1801,7 +1806,10 @@ body.gdp-help-page #content {
1801
1806
 
1802
1807
  #diff > *:first-child { margin-top: 0; }
1803
1808
 
1804
- body:not(.gdp-file-detail-page) #diff::after {
1809
+ /* Scroll-past-end spacer so the sidebar can align the last diff file to the
1810
+ top of the viewport. Not wanted on the history screen, where small
1811
+ single-commit diffs would gain a screenful of dead scroll space. */
1812
+ body:not(.gdp-file-detail-page):not(.gdp-history-page) #diff::after {
1805
1813
  content: "";
1806
1814
  display: block;
1807
1815
  height: calc(100vh - var(--chrome-h) - 40px);
@@ -2071,6 +2079,86 @@ table.d2h-diff-table tr.gdp-diff-line-target .d2h-code-line-ctn {
2071
2079
  table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2072
2080
  box-shadow: inset 3px 0 0 var(--line-hit-border), inset -1px 0 0 var(--border-muted);
2073
2081
  }
2082
+
2083
+ /* After-side drag selection (diff screen) — same yellow as the file view. */
2084
+ table.d2h-diff-table tr.gdp-diff-line-selected > td,
2085
+ table.d2h-diff-table tr.gdp-diff-line-selected .d2h-code-line,
2086
+ table.d2h-diff-table tr.gdp-diff-line-selected .d2h-code-side-line,
2087
+ table.d2h-diff-table tr.gdp-diff-line-selected .d2h-code-line-ctn {
2088
+ background: var(--line-hit-bg) !important;
2089
+ background-color: var(--line-hit-bg) !important;
2090
+ }
2091
+ table.d2h-diff-table tr.gdp-diff-line-selected > td:first-child {
2092
+ box-shadow: inset 3px 0 0 var(--line-hit-border), inset -1px 0 0 var(--border-muted);
2093
+ }
2094
+ table.d2h-diff-table td.d2h-code-linenumber,
2095
+ table.d2h-diff-table td.d2h-code-side-linenumber {
2096
+ cursor: pointer;
2097
+ user-select: none;
2098
+ }
2099
+
2100
+ /* Floating "copy @path#start-end" pill shown while lines are selected. */
2101
+ #line-ref-pill {
2102
+ position: fixed;
2103
+ left: 50%;
2104
+ bottom: 24px;
2105
+ transform: translateX(-50%);
2106
+ z-index: 400;
2107
+ display: inline-flex;
2108
+ align-items: center;
2109
+ gap: 8px;
2110
+ max-width: min(680px, calc(100vw - 48px));
2111
+ white-space: nowrap;
2112
+ padding: 9px 16px;
2113
+ border: 1px solid var(--accent);
2114
+ border-radius: 999px;
2115
+ background: var(--accent);
2116
+ color: #fff;
2117
+ font: inherit;
2118
+ font-size: 13px;
2119
+ font-weight: 600;
2120
+ cursor: pointer;
2121
+ box-shadow: 0 8px 28px rgba(31, 35, 40, 0.35);
2122
+ }
2123
+ #line-ref-pill:hover {
2124
+ filter: brightness(1.1);
2125
+ }
2126
+ #line-ref-pill svg {
2127
+ flex: 0 0 auto;
2128
+ }
2129
+ #line-ref-pill .lrp-label {
2130
+ flex: 0 0 auto;
2131
+ }
2132
+ #line-ref-pill .lrp-ref {
2133
+ overflow: hidden;
2134
+ text-overflow: ellipsis;
2135
+ padding: 2px 8px;
2136
+ border-radius: 6px;
2137
+ background: rgba(255, 255, 255, 0.18);
2138
+ font-family: "Monaspace Neon", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
2139
+ font-size: 12px;
2140
+ font-weight: 400;
2141
+ }
2142
+ #line-ref-pill.copied {
2143
+ background: var(--success, #2ea043);
2144
+ border-color: var(--success, #2ea043);
2145
+ }
2146
+ #line-ref-pill.pop {
2147
+ animation: lrp-pop 0.22s ease-out;
2148
+ }
2149
+ @keyframes lrp-pop {
2150
+ 0% {
2151
+ transform: translateX(-50%) translateY(14px) scale(0.92);
2152
+ opacity: 0;
2153
+ }
2154
+ 100% {
2155
+ transform: translateX(-50%) translateY(0) scale(1);
2156
+ opacity: 1;
2157
+ }
2158
+ }
2159
+ #line-ref-pill[hidden] {
2160
+ display: none;
2161
+ }
2074
2162
  /* Stack height = number of buttons * 20px. With 1 button = 20px row,
2075
2163
  * with 2 (↑+↓) = 40px row, matching GitHub. */
2076
2164
  .gdp-expand-stack {