FossilRepo

Add copy-link popover on line number click (code + diff views) - Click a line number → popover appears with "Copy link" button - Copies full permalink URL to clipboard - Shows "Copied!" confirmation with green color, auto-dismisses - Click outside to dismiss - Works on both code file viewer and diff view - Uses Alpine.js for state, navigator.clipboard for copy

lmata 2026-04-06 14:40 trunk
Commit 2d4ff6f552c166990526024feda7b7d3289cad2f9e4e4db81f0b2ec0d4eed525
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -15,10 +15,24 @@
1515
.diff-gutter { width: 1%; user-select: none; color: #4b5563; text-align: right; padding: 0 6px; border-right: 1px solid #374151; cursor: pointer; }
1616
.diff-gutter:hover { color: #DC394C; }
1717
.diff-gutter a { color: inherit; text-decoration: none; display: block; }
1818
.line-row:target { background: rgba(220, 57, 76, 0.15) !important; }
1919
.line-row:target .diff-gutter { color: #DC394C; font-weight: 600; }
20
+ .line-popover {
21
+ position: absolute; left: 100%; top: 50%; transform: translateY(-50%);
22
+ margin-left: 4px; z-index: 20; white-space: nowrap;
23
+ background: #1f2937; border: 1px solid #374151; border-radius: 6px;
24
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4); padding: 2px;
25
+ display: flex; gap: 2px;
26
+ }
27
+ .line-popover button {
28
+ display: flex; align-items: center; gap: 4px; padding: 4px 8px;
29
+ font-size: 0.7rem; color: #d1d5db; background: transparent;
30
+ border: none; border-radius: 4px; cursor: pointer;
31
+ }
32
+ .line-popover button:hover { background: #374151; color: #fff; }
33
+ .line-popover button.copied { color: #22c55e; }
2034
</style>
2135
{% endblock %}
2236
2337
{% block content %}
2438
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
@@ -121,16 +135,36 @@
121135
<tbody>
122136
{% for dl in fd.diff_lines %}
123137
{% if dl.new_num %}
124138
<tr class="diff-line-{{ dl.type }} line-row" id="diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}">
125139
<td class="diff-gutter">{{ dl.old_num }}</td>
126
- <td class="diff-gutter"><a href="#diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}">{{ dl.new_num }}</a></td>
140
+ <td class="diff-gutter" style="position:relative"
141
+ x-data="{ pop: false, copied: false }"
142
+ @click.stop="pop = !pop; window.location.hash = 'diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}'"
143
+ @click.outside="pop = false">
144
+ {{ dl.new_num }}
145
+ <div class="line-popover" x-show="pop" x-transition @click.stop>
146
+ <button @click="navigator.clipboard.writeText(window.location.origin + window.location.pathname + '#diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}'); copied = true; setTimeout(() => { copied = false; pop = false }, 1000)" :class="copied && 'copied'">
147
+ <span x-show="!copied">Copy link</span><span x-show="copied">Copied!</span>
148
+ </button>
149
+ </div>
150
+ </td>
127151
<td>{{ dl.text }}</td>
128152
</tr>
129153
{% elif dl.old_num %}
130154
<tr class="diff-line-{{ dl.type }} line-row" id="diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}">
131
- <td class="diff-gutter"><a href="#diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}">{{ dl.old_num }}</a></td>
155
+ <td class="diff-gutter" style="position:relative"
156
+ x-data="{ pop: false, copied: false }"
157
+ @click.stop="pop = !pop; window.location.hash = 'diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}'"
158
+ @click.outside="pop = false">
159
+ {{ dl.old_num }}
160
+ <div class="line-popover" x-show="pop" x-transition @click.stop>
161
+ <button @click="navigator.clipboard.writeText(window.location.origin + window.location.pathname + '#diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}'); copied = true; setTimeout(() => { copied = false; pop = false }, 1000)" :class="copied && 'copied'">
162
+ <span x-show="!copied">Copy link</span><span x-show="copied">Copied!</span>
163
+ </button>
164
+ </div>
165
+ </td>
132166
<td class="diff-gutter"></td>
133167
<td>{{ dl.text }}</td>
134168
</tr>
135169
{% else %}
136170
<tr class="diff-line-{{ dl.type }}">
137171
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -15,10 +15,24 @@
15 .diff-gutter { width: 1%; user-select: none; color: #4b5563; text-align: right; padding: 0 6px; border-right: 1px solid #374151; cursor: pointer; }
16 .diff-gutter:hover { color: #DC394C; }
17 .diff-gutter a { color: inherit; text-decoration: none; display: block; }
18 .line-row:target { background: rgba(220, 57, 76, 0.15) !important; }
19 .line-row:target .diff-gutter { color: #DC394C; font-weight: 600; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20 </style>
21 {% endblock %}
22
23 {% block content %}
24 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
@@ -121,16 +135,36 @@
121 <tbody>
122 {% for dl in fd.diff_lines %}
123 {% if dl.new_num %}
124 <tr class="diff-line-{{ dl.type }} line-row" id="diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}">
125 <td class="diff-gutter">{{ dl.old_num }}</td>
126 <td class="diff-gutter"><a href="#diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}">{{ dl.new_num }}</a></td>
 
 
 
 
 
 
 
 
 
 
127 <td>{{ dl.text }}</td>
128 </tr>
129 {% elif dl.old_num %}
130 <tr class="diff-line-{{ dl.type }} line-row" id="diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}">
131 <td class="diff-gutter"><a href="#diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}">{{ dl.old_num }}</a></td>
 
 
 
 
 
 
 
 
 
 
132 <td class="diff-gutter"></td>
133 <td>{{ dl.text }}</td>
134 </tr>
135 {% else %}
136 <tr class="diff-line-{{ dl.type }}">
137
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -15,10 +15,24 @@
15 .diff-gutter { width: 1%; user-select: none; color: #4b5563; text-align: right; padding: 0 6px; border-right: 1px solid #374151; cursor: pointer; }
16 .diff-gutter:hover { color: #DC394C; }
17 .diff-gutter a { color: inherit; text-decoration: none; display: block; }
18 .line-row:target { background: rgba(220, 57, 76, 0.15) !important; }
19 .line-row:target .diff-gutter { color: #DC394C; font-weight: 600; }
20 .line-popover {
21 position: absolute; left: 100%; top: 50%; transform: translateY(-50%);
22 margin-left: 4px; z-index: 20; white-space: nowrap;
23 background: #1f2937; border: 1px solid #374151; border-radius: 6px;
24 box-shadow: 0 4px 12px rgba(0,0,0,0.4); padding: 2px;
25 display: flex; gap: 2px;
26 }
27 .line-popover button {
28 display: flex; align-items: center; gap: 4px; padding: 4px 8px;
29 font-size: 0.7rem; color: #d1d5db; background: transparent;
30 border: none; border-radius: 4px; cursor: pointer;
31 }
32 .line-popover button:hover { background: #374151; color: #fff; }
33 .line-popover button.copied { color: #22c55e; }
34 </style>
35 {% endblock %}
36
37 {% block content %}
38 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
@@ -121,16 +135,36 @@
135 <tbody>
136 {% for dl in fd.diff_lines %}
137 {% if dl.new_num %}
138 <tr class="diff-line-{{ dl.type }} line-row" id="diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}">
139 <td class="diff-gutter">{{ dl.old_num }}</td>
140 <td class="diff-gutter" style="position:relative"
141 x-data="{ pop: false, copied: false }"
142 @click.stop="pop = !pop; window.location.hash = 'diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}'"
143 @click.outside="pop = false">
144 {{ dl.new_num }}
145 <div class="line-popover" x-show="pop" x-transition @click.stop>
146 <button @click="navigator.clipboard.writeText(window.location.origin + window.location.pathname + '#diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}'); copied = true; setTimeout(() => { copied = false; pop = false }, 1000)" :class="copied && 'copied'">
147 <span x-show="!copied">Copy link</span><span x-show="copied">Copied!</span>
148 </button>
149 </div>
150 </td>
151 <td>{{ dl.text }}</td>
152 </tr>
153 {% elif dl.old_num %}
154 <tr class="diff-line-{{ dl.type }} line-row" id="diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}">
155 <td class="diff-gutter" style="position:relative"
156 x-data="{ pop: false, copied: false }"
157 @click.stop="pop = !pop; window.location.hash = 'diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}'"
158 @click.outside="pop = false">
159 {{ dl.old_num }}
160 <div class="line-popover" x-show="pop" x-transition @click.stop>
161 <button @click="navigator.clipboard.writeText(window.location.origin + window.location.pathname + '#diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}'); copied = true; setTimeout(() => { copied = false; pop = false }, 1000)" :class="copied && 'copied'">
162 <span x-show="!copied">Copy link</span><span x-show="copied">Copied!</span>
163 </button>
164 </div>
165 </td>
166 <td class="diff-gutter"></td>
167 <td>{{ dl.text }}</td>
168 </tr>
169 {% else %}
170 <tr class="diff-line-{{ dl.type }}">
171
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -19,10 +19,24 @@
1919
font-size: 0.8125rem; line-height: 1.5rem;
2020
}
2121
.line-row:hover { background: rgba(220, 57, 76, 0.05); }
2222
.line-row:target { background: rgba(220, 57, 76, 0.12); }
2323
.line-row:target .line-num { color: #DC394C; font-weight: 600; }
24
+ .line-popover {
25
+ position: absolute; left: 100%; top: 50%; transform: translateY(-50%);
26
+ margin-left: 4px; z-index: 20; white-space: nowrap;
27
+ background: #1f2937; border: 1px solid #374151; border-radius: 6px;
28
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4); padding: 2px;
29
+ display: flex; gap: 2px;
30
+ }
31
+ .line-popover button {
32
+ display: flex; align-items: center; gap: 4px; padding: 4px 8px;
33
+ font-size: 0.7rem; color: #d1d5db; background: transparent;
34
+ border: none; border-radius: 4px; cursor: pointer;
35
+ }
36
+ .line-popover button:hover { background: #374151; color: #fff; }
37
+ .line-popover button.copied { color: #22c55e; }
2438
</style>
2539
{% endblock %}
2640
2741
{% block content %}
2842
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
@@ -52,11 +66,21 @@
5266
{% else %}
5367
<table class="code-table">
5468
<tbody>
5569
{% for line in lines %}
5670
<tr class="line-row" id="L{{ line.num }}">
57
- <td class="line-num"><a href="#L{{ line.num }}">{{ line.num }}</a></td>
71
+ <td class="line-num" style="position:relative"
72
+ x-data="{ pop: false, copied: false }"
73
+ @click.stop="pop = !pop; window.location.hash = 'L{{ line.num }}'"
74
+ @click.outside="pop = false">
75
+ {{ line.num }}
76
+ <div class="line-popover" x-show="pop" x-transition @click.stop>
77
+ <button @click="navigator.clipboard.writeText(window.location.origin + window.location.pathname + '#L{{ line.num }}'); copied = true; setTimeout(() => { copied = false; pop = false }, 1000)" :class="copied && 'copied'">
78
+ <span x-show="!copied">Copy link</span><span x-show="copied">Copied!</span>
79
+ </button>
80
+ </div>
81
+ </td>
5882
<td class="line-code"><code class="language-{{ language }}">{{ line.text }}</code></td>
5983
</tr>
6084
{% endfor %}
6185
</tbody>
6286
</table>
6387
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -19,10 +19,24 @@
19 font-size: 0.8125rem; line-height: 1.5rem;
20 }
21 .line-row:hover { background: rgba(220, 57, 76, 0.05); }
22 .line-row:target { background: rgba(220, 57, 76, 0.12); }
23 .line-row:target .line-num { color: #DC394C; font-weight: 600; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24 </style>
25 {% endblock %}
26
27 {% block content %}
28 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
@@ -52,11 +66,21 @@
52 {% else %}
53 <table class="code-table">
54 <tbody>
55 {% for line in lines %}
56 <tr class="line-row" id="L{{ line.num }}">
57 <td class="line-num"><a href="#L{{ line.num }}">{{ line.num }}</a></td>
 
 
 
 
 
 
 
 
 
 
58 <td class="line-code"><code class="language-{{ language }}">{{ line.text }}</code></td>
59 </tr>
60 {% endfor %}
61 </tbody>
62 </table>
63
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -19,10 +19,24 @@
19 font-size: 0.8125rem; line-height: 1.5rem;
20 }
21 .line-row:hover { background: rgba(220, 57, 76, 0.05); }
22 .line-row:target { background: rgba(220, 57, 76, 0.12); }
23 .line-row:target .line-num { color: #DC394C; font-weight: 600; }
24 .line-popover {
25 position: absolute; left: 100%; top: 50%; transform: translateY(-50%);
26 margin-left: 4px; z-index: 20; white-space: nowrap;
27 background: #1f2937; border: 1px solid #374151; border-radius: 6px;
28 box-shadow: 0 4px 12px rgba(0,0,0,0.4); padding: 2px;
29 display: flex; gap: 2px;
30 }
31 .line-popover button {
32 display: flex; align-items: center; gap: 4px; padding: 4px 8px;
33 font-size: 0.7rem; color: #d1d5db; background: transparent;
34 border: none; border-radius: 4px; cursor: pointer;
35 }
36 .line-popover button:hover { background: #374151; color: #fff; }
37 .line-popover button.copied { color: #22c55e; }
38 </style>
39 {% endblock %}
40
41 {% block content %}
42 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
@@ -52,11 +66,21 @@
66 {% else %}
67 <table class="code-table">
68 <tbody>
69 {% for line in lines %}
70 <tr class="line-row" id="L{{ line.num }}">
71 <td class="line-num" style="position:relative"
72 x-data="{ pop: false, copied: false }"
73 @click.stop="pop = !pop; window.location.hash = 'L{{ line.num }}'"
74 @click.outside="pop = false">
75 {{ line.num }}
76 <div class="line-popover" x-show="pop" x-transition @click.stop>
77 <button @click="navigator.clipboard.writeText(window.location.origin + window.location.pathname + '#L{{ line.num }}'); copied = true; setTimeout(() => { copied = false; pop = false }, 1000)" :class="copied && 'copied'">
78 <span x-show="!copied">Copy link</span><span x-show="copied">Copied!</span>
79 </button>
80 </div>
81 </td>
82 <td class="line-code"><code class="language-{{ language }}">{{ line.text }}</code></td>
83 </tr>
84 {% endfor %}
85 </tbody>
86 </table>
87

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button