FossilRepo
HTMX infinite scroll, copy hash buttons, clone instructions Timeline: - HTMX infinite scroll — loads next page when scrolling near bottom - Loading spinner indicator - Replaces "Load more" button Copy hash: - Reusable _copy_hash.html component with clipboard icon - Click copies full hash, shows green checkmark confirmation - Used in timeline entries (replaces plain hash link) Project overview: - Clone instructions card with copy-to-clipboard - Shows `fossil clone /path/to/slug.fossil` command
Commit
b139e8be7e1d8b08ebc72fa9d11d3a2ee9d3ffe2940a0ae4950101ae59a95d1b
Parent
63b6ac26ee935d0…
4 files changed
+12
+1
-1
+13
-5
+13
| --- a/templates/fossil/_copy_hash.html | ||
| +++ b/templates/fossil/_copy_hash.html | ||
| @@ -0,0 +1,12 @@ | ||
| 1 | +{% comment %} | |
| 2 | +Usage: {% include "fossil/_copy_hash.html" with hash=some_uuid slug=project.slug %} | |
| 3 | +Shows a truncated hash with copy-to-clipboard button. | |
| 4 | +{% endcomment %} | |
| 5 | +<span class="inline-flex items-center gap-1" x-data="{ copied: false }"> | |
| 6 | + <a href="{% url 'fossil:checkin_detail' slug=slug checkin_uuid=hash %}" class="font-mono text-xs text-brand-light hover:text-brand">{{ hash|truncatechars:10 }}</a> | |
| 7 | + <button @click="navigator.clipboard.writeText('{{ hash }}'); copied = true; setTimeout(() => copied = false, 1500)" | |
| 8 | + class="text-gray-600 hover:text-brand-light" title="Copy full hash"> | |
| 9 | + <svg x-show="!copied" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /></svg> | |
| 10 | + <svg x-show="copied" class="h-3 w-3 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="display:none"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg> | |
| 11 | + </button> | |
| 12 | +</span> |
| --- a/templates/fossil/_copy_hash.html | |
| +++ b/templates/fossil/_copy_hash.html | |
| @@ -0,0 +1,12 @@ | |
| --- a/templates/fossil/_copy_hash.html | |
| +++ b/templates/fossil/_copy_hash.html | |
| @@ -0,0 +1,12 @@ | |
| 1 | {% comment %} |
| 2 | Usage: {% include "fossil/_copy_hash.html" with hash=some_uuid slug=project.slug %} |
| 3 | Shows a truncated hash with copy-to-clipboard button. |
| 4 | {% endcomment %} |
| 5 | <span class="inline-flex items-center gap-1" x-data="{ copied: false }"> |
| 6 | <a href="{% url 'fossil:checkin_detail' slug=slug checkin_uuid=hash %}" class="font-mono text-xs text-brand-light hover:text-brand">{{ hash|truncatechars:10 }}</a> |
| 7 | <button @click="navigator.clipboard.writeText('{{ hash }}'); copied = true; setTimeout(() => copied = false, 1500)" |
| 8 | class="text-gray-600 hover:text-brand-light" title="Copy full hash"> |
| 9 | <svg x-show="!copied" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /></svg> |
| 10 | <svg x-show="copied" class="h-3 w-3 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="display:none"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg> |
| 11 | </button> |
| 12 | </span> |
| --- templates/fossil/partials/timeline_entries.html | ||
| +++ templates/fossil/partials/timeline_entries.html | ||
| @@ -75,11 +75,11 @@ | ||
| 75 | 75 | </div> |
| 76 | 76 | |
| 77 | 77 | {# Meta: hash, user, branch #} |
| 78 | 78 | <div class="tl-meta"> |
| 79 | 79 | {% if e.event_type == "ci" %} |
| 80 | - <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=e.uuid %}" class="tl-hash">{{ e.uuid|truncatechars:10 }}</a> | |
| 80 | + {% include "fossil/_copy_hash.html" with hash=e.uuid slug=project.slug %} | |
| 81 | 81 | {% endif %} |
| 82 | 82 | <a href="{% url 'fossil:user_activity' slug=project.slug username=e.user %}" class="tl-user">{{ e.user }}</a> |
| 83 | 83 | {% if e.branch %}<span class="tl-branch">{{ e.branch }}</span>{% endif %} |
| 84 | 84 | </div> |
| 85 | 85 | </div> |
| 86 | 86 |
| --- templates/fossil/partials/timeline_entries.html | |
| +++ templates/fossil/partials/timeline_entries.html | |
| @@ -75,11 +75,11 @@ | |
| 75 | </div> |
| 76 | |
| 77 | {# Meta: hash, user, branch #} |
| 78 | <div class="tl-meta"> |
| 79 | {% if e.event_type == "ci" %} |
| 80 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=e.uuid %}" class="tl-hash">{{ e.uuid|truncatechars:10 }}</a> |
| 81 | {% endif %} |
| 82 | <a href="{% url 'fossil:user_activity' slug=project.slug username=e.user %}" class="tl-user">{{ e.user }}</a> |
| 83 | {% if e.branch %}<span class="tl-branch">{{ e.branch }}</span>{% endif %} |
| 84 | </div> |
| 85 | </div> |
| 86 |
| --- templates/fossil/partials/timeline_entries.html | |
| +++ templates/fossil/partials/timeline_entries.html | |
| @@ -75,11 +75,11 @@ | |
| 75 | </div> |
| 76 | |
| 77 | {# Meta: hash, user, branch #} |
| 78 | <div class="tl-meta"> |
| 79 | {% if e.event_type == "ci" %} |
| 80 | {% include "fossil/_copy_hash.html" with hash=e.uuid slug=project.slug %} |
| 81 | {% endif %} |
| 82 | <a href="{% url 'fossil:user_activity' slug=project.slug username=e.user %}" class="tl-user">{{ e.user }}</a> |
| 83 | {% if e.branch %}<span class="tl-branch">{{ e.branch }}</span>{% endif %} |
| 84 | </div> |
| 85 | </div> |
| 86 |
+13
-5
| --- templates/fossil/timeline.html | ||
| +++ templates/fossil/timeline.html | ||
| @@ -24,15 +24,23 @@ | ||
| 24 | 24 | </div> |
| 25 | 25 | |
| 26 | 26 | {% include "fossil/partials/timeline_entries.html" %} |
| 27 | 27 | |
| 28 | 28 | {% if entries|length == 50 %} |
| 29 | -<div class="mt-4 text-center"> | |
| 30 | - <a href="{% url 'fossil:timeline' slug=project.slug %}?page={{ page|add:1 }}{% if event_type %}&type={{ event_type }}{% endif %}" | |
| 31 | - class="inline-flex items-center rounded-md bg-gray-800 px-4 py-2 text-sm text-gray-400 hover:text-white border border-gray-700"> | |
| 32 | - Load more | |
| 33 | - </a> | |
| 29 | +<div id="load-more" class="mt-4 text-center" | |
| 30 | + hx-get="{% url 'fossil:timeline' slug=project.slug %}?page={{ page|add:1 }}{% if event_type %}&type={{ event_type }}{% endif %}" | |
| 31 | + hx-trigger="revealed" | |
| 32 | + hx-target="#timeline-entries" | |
| 33 | + hx-swap="beforeend" | |
| 34 | + hx-select="#timeline-entries > *" | |
| 35 | + hx-indicator="#load-spinner"> | |
| 36 | + <div id="load-spinner" class="htmx-indicator"> | |
| 37 | + <div class="inline-flex items-center gap-2 text-sm text-gray-500"> | |
| 38 | + <svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> | |
| 39 | + Loading more... | |
| 40 | + </div> | |
| 41 | + </div> | |
| 34 | 42 | </div> |
| 35 | 43 | {% endif %} |
| 36 | 44 | |
| 37 | 45 | <script> |
| 38 | 46 | // Keyboard navigation: j/k to move between timeline entries |
| 39 | 47 |
| --- templates/fossil/timeline.html | |
| +++ templates/fossil/timeline.html | |
| @@ -24,15 +24,23 @@ | |
| 24 | </div> |
| 25 | |
| 26 | {% include "fossil/partials/timeline_entries.html" %} |
| 27 | |
| 28 | {% if entries|length == 50 %} |
| 29 | <div class="mt-4 text-center"> |
| 30 | <a href="{% url 'fossil:timeline' slug=project.slug %}?page={{ page|add:1 }}{% if event_type %}&type={{ event_type }}{% endif %}" |
| 31 | class="inline-flex items-center rounded-md bg-gray-800 px-4 py-2 text-sm text-gray-400 hover:text-white border border-gray-700"> |
| 32 | Load more |
| 33 | </a> |
| 34 | </div> |
| 35 | {% endif %} |
| 36 | |
| 37 | <script> |
| 38 | // Keyboard navigation: j/k to move between timeline entries |
| 39 |
| --- templates/fossil/timeline.html | |
| +++ templates/fossil/timeline.html | |
| @@ -24,15 +24,23 @@ | |
| 24 | </div> |
| 25 | |
| 26 | {% include "fossil/partials/timeline_entries.html" %} |
| 27 | |
| 28 | {% if entries|length == 50 %} |
| 29 | <div id="load-more" class="mt-4 text-center" |
| 30 | hx-get="{% url 'fossil:timeline' slug=project.slug %}?page={{ page|add:1 }}{% if event_type %}&type={{ event_type }}{% endif %}" |
| 31 | hx-trigger="revealed" |
| 32 | hx-target="#timeline-entries" |
| 33 | hx-swap="beforeend" |
| 34 | hx-select="#timeline-entries > *" |
| 35 | hx-indicator="#load-spinner"> |
| 36 | <div id="load-spinner" class="htmx-indicator"> |
| 37 | <div class="inline-flex items-center gap-2 text-sm text-gray-500"> |
| 38 | <svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> |
| 39 | Loading more... |
| 40 | </div> |
| 41 | </div> |
| 42 | </div> |
| 43 | {% endif %} |
| 44 | |
| 45 | <script> |
| 46 | // Keyboard navigation: j/k to move between timeline entries |
| 47 |
| --- templates/projects/project_detail.html | ||
| +++ templates/projects/project_detail.html | ||
| @@ -120,10 +120,23 @@ | ||
| 120 | 120 | <a href="{% url 'fossil:wiki' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.wiki_page_count|default:"0" }}</a> |
| 121 | 121 | </div> |
| 122 | 122 | </div> |
| 123 | 123 | </div> |
| 124 | 124 | {% endif %} |
| 125 | + | |
| 126 | + <!-- Clone instructions --> | |
| 127 | + <div class="rounded-lg bg-gray-800 border border-gray-700 p-4" x-data="{ copied: false }"> | |
| 128 | + <h3 class="text-sm font-medium text-gray-300 mb-2">Clone</h3> | |
| 129 | + <div class="flex items-center gap-2"> | |
| 130 | + <code class="flex-1 text-xs font-mono text-gray-400 bg-gray-900 rounded px-3 py-2 truncate">fossil clone /path/to/{{ project.slug }}.fossil</code> | |
| 131 | + <button @click="navigator.clipboard.writeText('fossil clone /path/to/{{ project.slug }}.fossil'); copied = true; setTimeout(() => copied = false, 1500)" | |
| 132 | + class="flex-shrink-0 rounded px-2 py-2 text-gray-500 hover:text-brand-light hover:bg-gray-700"> | |
| 133 | + <svg x-show="!copied" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /></svg> | |
| 134 | + <svg x-show="copied" class="h-4 w-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="display:none"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg> | |
| 135 | + </button> | |
| 136 | + </div> | |
| 137 | + </div> | |
| 125 | 138 | |
| 126 | 139 | {% if top_contributors %} |
| 127 | 140 | <!-- Contributors --> |
| 128 | 141 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 129 | 142 | <h3 class="text-sm font-medium text-gray-300 mb-3">Top Contributors</h3> |
| 130 | 143 |
| --- templates/projects/project_detail.html | |
| +++ templates/projects/project_detail.html | |
| @@ -120,10 +120,23 @@ | |
| 120 | <a href="{% url 'fossil:wiki' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.wiki_page_count|default:"0" }}</a> |
| 121 | </div> |
| 122 | </div> |
| 123 | </div> |
| 124 | {% endif %} |
| 125 | |
| 126 | {% if top_contributors %} |
| 127 | <!-- Contributors --> |
| 128 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 129 | <h3 class="text-sm font-medium text-gray-300 mb-3">Top Contributors</h3> |
| 130 |
| --- templates/projects/project_detail.html | |
| +++ templates/projects/project_detail.html | |
| @@ -120,10 +120,23 @@ | |
| 120 | <a href="{% url 'fossil:wiki' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.wiki_page_count|default:"0" }}</a> |
| 121 | </div> |
| 122 | </div> |
| 123 | </div> |
| 124 | {% endif %} |
| 125 | |
| 126 | <!-- Clone instructions --> |
| 127 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4" x-data="{ copied: false }"> |
| 128 | <h3 class="text-sm font-medium text-gray-300 mb-2">Clone</h3> |
| 129 | <div class="flex items-center gap-2"> |
| 130 | <code class="flex-1 text-xs font-mono text-gray-400 bg-gray-900 rounded px-3 py-2 truncate">fossil clone /path/to/{{ project.slug }}.fossil</code> |
| 131 | <button @click="navigator.clipboard.writeText('fossil clone /path/to/{{ project.slug }}.fossil'); copied = true; setTimeout(() => copied = false, 1500)" |
| 132 | class="flex-shrink-0 rounded px-2 py-2 text-gray-500 hover:text-brand-light hover:bg-gray-700"> |
| 133 | <svg x-show="!copied" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /></svg> |
| 134 | <svg x-show="copied" class="h-4 w-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="display:none"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg> |
| 135 | </button> |
| 136 | </div> |
| 137 | </div> |
| 138 | |
| 139 | {% if top_contributors %} |
| 140 | <!-- Contributors --> |
| 141 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 142 | <h3 class="text-sm font-medium text-gray-300 mb-3">Top Contributors</h3> |
| 143 |