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
Commit
2d4ff6f552c166990526024feda7b7d3289cad2f9e4e4db81f0b2ec0d4eed525
Parent
dc6f6e1bab6ebcd…
2 files changed
+36
-2
+25
-1
+36
-2
| --- templates/fossil/checkin_detail.html | ||
| +++ templates/fossil/checkin_detail.html | ||
| @@ -15,10 +15,24 @@ | ||
| 15 | 15 | .diff-gutter { width: 1%; user-select: none; color: #4b5563; text-align: right; padding: 0 6px; border-right: 1px solid #374151; cursor: pointer; } |
| 16 | 16 | .diff-gutter:hover { color: #DC394C; } |
| 17 | 17 | .diff-gutter a { color: inherit; text-decoration: none; display: block; } |
| 18 | 18 | .line-row:target { background: rgba(220, 57, 76, 0.15) !important; } |
| 19 | 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; } | |
| 20 | 34 | </style> |
| 21 | 35 | {% endblock %} |
| 22 | 36 | |
| 23 | 37 | {% block content %} |
| 24 | 38 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| @@ -121,16 +135,36 @@ | ||
| 121 | 135 | <tbody> |
| 122 | 136 | {% for dl in fd.diff_lines %} |
| 123 | 137 | {% if dl.new_num %} |
| 124 | 138 | <tr class="diff-line-{{ dl.type }} line-row" id="diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}"> |
| 125 | 139 | <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> | |
| 127 | 151 | <td>{{ dl.text }}</td> |
| 128 | 152 | </tr> |
| 129 | 153 | {% elif dl.old_num %} |
| 130 | 154 | <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> | |
| 132 | 166 | <td class="diff-gutter"></td> |
| 133 | 167 | <td>{{ dl.text }}</td> |
| 134 | 168 | </tr> |
| 135 | 169 | {% else %} |
| 136 | 170 | <tr class="diff-line-{{ dl.type }}"> |
| 137 | 171 |
| --- 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 |
+25
-1
| --- templates/fossil/code_file.html | ||
| +++ templates/fossil/code_file.html | ||
| @@ -19,10 +19,24 @@ | ||
| 19 | 19 | font-size: 0.8125rem; line-height: 1.5rem; |
| 20 | 20 | } |
| 21 | 21 | .line-row:hover { background: rgba(220, 57, 76, 0.05); } |
| 22 | 22 | .line-row:target { background: rgba(220, 57, 76, 0.12); } |
| 23 | 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; } | |
| 24 | 38 | </style> |
| 25 | 39 | {% endblock %} |
| 26 | 40 | |
| 27 | 41 | {% block content %} |
| 28 | 42 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| @@ -52,11 +66,21 @@ | ||
| 52 | 66 | {% else %} |
| 53 | 67 | <table class="code-table"> |
| 54 | 68 | <tbody> |
| 55 | 69 | {% for line in lines %} |
| 56 | 70 | <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> | |
| 58 | 82 | <td class="line-code"><code class="language-{{ language }}">{{ line.text }}</code></td> |
| 59 | 83 | </tr> |
| 60 | 84 | {% endfor %} |
| 61 | 85 | </tbody> |
| 62 | 86 | </table> |
| 63 | 87 |
| --- 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 |