FossilRepo

fossilrepo / templates / base.html
1
<!DOCTYPE html>
2
<html lang="en" class="h-full dark">
3
<head>
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<meta name="csrf-token" content="{{ csrf_token }}">
7
<title>{% block title %}Fossilrepo{% endblock %}</title>
8
<script>
9
// Apply theme before anything renders to prevent flash
10
(function() {
11
var theme = localStorage.getItem('theme');
12
if (theme === 'light') {
13
document.documentElement.classList.remove('dark');
14
} else {
15
document.documentElement.classList.add('dark');
16
}
17
})();
18
</script>
19
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
20
{% block extra_head %}{% endblock %}
21
<script>
22
tailwind.config = {
23
darkMode: 'class',
24
theme: {
25
extend: {
26
colors: {
27
brand: {
28
DEFAULT: '#DC394C',
29
dark: '#8B3138',
30
hover: '#c42d3f',
31
light: '#e8677a',
32
}
33
}
34
}
35
}
36
}
37
</script>
38
<style type="text/tailwindcss">
39
@layer base {
40
input[type="text"], input[type="number"], input[type="email"],
41
input[type="password"], input[type="search"], input[type="url"],
42
textarea, select {
43
@apply bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
44
text-gray-900 dark:text-gray-100 rounded-md
45
px-3 py-2
46
shadow-sm dark:shadow-inner
47
focus:border-brand focus:ring-1 focus:ring-brand
48
placeholder-gray-500 dark:placeholder-gray-500
49
transition-colors duration-150
50
sm:text-sm;
51
}
52
}
53
</style>
54
<style>
55
/* Hide scrollbar on horizontal-scroll navs (tabs, filter pills) */
56
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
57
.scrollbar-hide::-webkit-scrollbar { display: none; }
58
/* Focus visible outlines for keyboard navigation */
59
a:focus-visible, button:focus-visible, [role="button"]:focus-visible {
60
outline: 2px solid #DC394C;
61
outline-offset: 2px;
62
border-radius: 4px;
63
}
64
/* HTMX loading indicator: hidden by default, shown when htmx is in flight */
65
.htmx-indicator { display: none; }
66
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { display: inline-flex; }
67
/* Spinner next to search inputs during HTMX requests */
68
.search-wrap { position: relative; display: inline-flex; align-items: center; }
69
.search-wrap .search-spinner {
70
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
71
display: none; color: #6b7280;
72
}
73
.search-wrap:has(input.htmx-request) .search-spinner,
74
.search-wrap.htmx-request .search-spinner { display: block; }
75
</style>
76
<style>
77
/*
78
* Light mode — matches Django admin dark_theme.css palette
79
* Brand: #DC394C red, #8B3138 crimson, #2B2D2C charcoal
80
* Nav bar stays dark. Only main content area switches.
81
*/
82
html:not(.dark) body { background-color: #f8f8f8; color: #1a1a1a; }
83
/* Surfaces */
84
html:not(.dark) main .bg-gray-950 { background-color: #f8f8f8 !important; }
85
html:not(.dark) main .bg-gray-900 { background-color: #eeeeee !important; }
86
html:not(.dark) main .bg-gray-800 { background-color: #ffffff !important; }
87
html:not(.dark) main .bg-gray-700 { background-color: #eeeeee !important; }
88
html:not(.dark) main .bg-gray-800\/50,
89
html:not(.dark) main .hover\:bg-gray-800\/50:hover { background-color: #eeeeee !important; }
90
html:not(.dark) main .hover\:bg-gray-800:hover { background-color: #eeeeee !important; }
91
html:not(.dark) main .hover\:bg-gray-700:hover { background-color: #e0e0e0 !important; }
92
html:not(.dark) main .hover\:bg-gray-700\/50:hover { background-color: #eeeeee !important; }
93
html:not(.dark) main .hover\:bg-gray-600:hover { background-color: #e0e0e0 !important; }
94
/* Text */
95
html:not(.dark) main .text-gray-100 { color: #1a1a1a !important; }
96
html:not(.dark) main .text-gray-200 { color: #1a1a1a !important; }
97
html:not(.dark) main .text-gray-300 { color: #666666 !important; }
98
html:not(.dark) main .text-gray-400 { color: #666666 !important; }
99
html:not(.dark) main .text-gray-500 { color: #a8aaa9 !important; }
100
html:not(.dark) main .hover\:text-white:hover { color: #000000 !important; }
101
html:not(.dark) main .hover\:text-gray-200:hover { color: #1a1a1a !important; }
102
/* Borders — matches --hairline-color / --border-color */
103
html:not(.dark) main .border-gray-700 { border-color: #e0e0e0 !important; }
104
html:not(.dark) main .border-gray-600 { border-color: #e0e0e0 !important; }
105
html:not(.dark) main .divide-gray-700 > :not([hidden]) ~ :not([hidden]) { border-color: #e0e0e0 !important; }
106
html:not(.dark) main .ring-gray-700 { --tw-ring-color: #e0e0e0 !important; }
107
html:not(.dark) main .ring-gray-600 { --tw-ring-color: #e0e0e0 !important; }
108
html:not(.dark) main .shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05) !important; }
109
/* Status badges — matches admin message colors */
110
html:not(.dark) main .bg-green-900\/50 { background-color: #d4edda !important; }
111
html:not(.dark) main .text-green-300 { color: #155724 !important; }
112
html:not(.dark) main .border-green-700 { border-color: #c3e6cb !important; }
113
html:not(.dark) main .bg-yellow-900\/50 { background-color: #fff3cd !important; }
114
html:not(.dark) main .text-yellow-300 { color: #856404 !important; }
115
html:not(.dark) main .text-yellow-400 { color: #856404 !important; }
116
html:not(.dark) main .bg-purple-900\/50 { background-color: #f3e8ff !important; }
117
html:not(.dark) main .text-purple-300 { color: #9333ea !important; }
118
html:not(.dark) main .bg-blue-900\/50 { background-color: #dbeafe !important; }
119
html:not(.dark) main .text-blue-300 { color: #2563eb !important; }
120
html:not(.dark) main .bg-red-900\/50 { background-color: #f8d7da !important; }
121
html:not(.dark) main .text-red-300 { color: #721c24 !important; }
122
html:not(.dark) main .border-red-700 { border-color: #f5c6cb !important; }
123
html:not(.dark) main .text-red-400 { color: #c0392b !important; }
124
html:not(.dark) main .hover\:text-red-300:hover { color: #721c24 !important; }
125
/* Mono text */
126
html:not(.dark) main .font-mono { color: #666666 !important; }
127
/* Prose — matches admin link/text colors */
128
html:not(.dark) main .prose-invert { --tw-prose-body: #1a1a1a; --tw-prose-headings: #000000; --tw-prose-links: #DC394C; --tw-prose-bold: #000000; --tw-prose-code: #1a1a1a; --tw-prose-th-borders: #e0e0e0; --tw-prose-td-borders: #e0e0e0; }
129
/* Brand links — matches admin --link-fg */
130
html:not(.dark) main .text-brand-light { color: #DC394C !important; }
131
html:not(.dark) main .hover\:text-brand:hover { color: #8B3138 !important; }
132
html:not(.dark) main .hover\:border-brand:hover { border-color: #DC394C !important; }
133
/* Selected/hover rows — matches admin --selected-bg */
134
html:not(.dark) main .group:hover { border-color: #DC394C !important; }
135
</style>
136
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
137
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
138
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.6/purify.min.js"></script>
139
<script src="https://unpkg.com/[email protected]"></script>
140
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
141
<script>
142
document.body.addEventListener('htmx:configRequest', function(event) {
143
var token = document.querySelector('meta[name="csrf-token"]');
144
if (token) { event.detail.headers['X-CSRFToken'] = token.content; }
145
});
146
// Keyboard shortcut: / to open search
147
document.addEventListener('keydown', function(e) {
148
if (e.key === '/' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
149
e.preventDefault();
150
document.querySelector('[x-ref="searchInput"]')?.closest('[x-data]')?.__x?.$data && (document.querySelector('[x-ref="searchInput"]').closest('[x-data]').__x.$data.open = true);
151
setTimeout(() => document.querySelector('[x-ref="searchInput"]')?.focus(), 100);
152
}
153
});
154
</script>
155
</head>
156
<body class="h-full bg-gray-950 text-gray-100" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
157
<div class="min-h-full flex flex-col" x-data="{ mobileSidebar: false }">
158
{% if user.is_authenticated %}
159
{% include "includes/nav.html" %}
160
{% else %}
161
{% include "includes/nav_public.html" %}
162
{% endif %}
163
164
<div class="flex flex-1 overflow-hidden">
165
{% if user.is_authenticated %}
166
<!-- Mobile sidebar overlay -->
167
<div x-show="mobileSidebar" x-transition:enter="transition-opacity ease-out duration-200" x-transition:leave="transition-opacity ease-in duration-150"
168
class="fixed inset-0 z-40 bg-black/50 lg:hidden" @click="mobileSidebar = false" style="display:none"></div>
169
<div x-show="mobileSidebar" x-transition:enter="transition-transform ease-out duration-200" x-transition:enter-start="-translate-x-full" x-transition:enter-end="translate-x-0"
170
x-transition:leave="transition-transform ease-in duration-150" x-transition:leave-start="translate-x-0" x-transition:leave-end="-translate-x-full"
171
class="fixed inset-y-0 left-0 z-50 w-64 lg:hidden" style="display:none">
172
{% include "includes/sidebar.html" %}
173
</div>
174
<!-- Desktop sidebar -->
175
{% include "includes/sidebar.html" %}
176
{% endif %}
177
178
<main class="flex-1 overflow-y-auto py-6">
179
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
180
{% if messages %}
181
<div id="messages" class="mb-4 space-y-2">
182
{% for message in messages %}
183
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 4000)"
184
x-transition role="status" class="rounded-md p-4 {% if message.tags == 'success' %}bg-green-900/50 text-green-300 border border-green-700{% elif message.tags == 'error' %}bg-red-900/50 text-red-300 border border-red-700{% else %}bg-gray-800 text-gray-300 border border-gray-700{% endif %}">
185
<div class="flex justify-between">
186
<p class="text-sm font-medium">{{ message }}</p>
187
<button @click="show = false" aria-label="Dismiss" class="ml-3 text-sm font-medium underline">&times;</button>
188
</div>
189
</div>
190
{% endfor %}
191
</div>
192
{% endif %}
193
194
{% block content %}{% endblock %}
195
</div>
196
</main>
197
</div>
198
</div>
199
{% include "fossil/_keyboard_help.html" %}
200
</body>
201
</html>
202

Keyboard Shortcuts

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