Reload the browser on template and static changes in dev

Adds django-browser-reload: runserver already restarts on Python changes, but
the browser had to be refreshed by hand for every template or CSS edit. It also
watches static/, so a Tailwind rebuild now refreshes the page on its own.

Mounted only under DEBUG -- it injects a script into every HTML response and
serves an open event stream, neither of which belongs in production; a test
holds that line. Its endpoint is exempt from RequireMFAMiddleware, otherwise a
not-yet-enrolled staff user has the stream redirected away and live reload dies
on the MFA enrolment page, which is exactly a page we are restyling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 18:32:19 +02:00
parent 268cbe1e06
commit 334c706aec
16 changed files with 473 additions and 3 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -17,7 +17,9 @@ from django.urls import reverse
from club.models import ClubRole from club.models import ClubRole
#: Paths a not-yet-enrolled user must still reach (to enrol, or to log out). #: Paths a not-yet-enrolled user must still reach (to enrol, or to log out).
EXEMPT_PREFIXES = ("/accounts/", "/static/", "/media/") #: ``/__reload__/`` is django-browser-reload's event stream, which only exists
#: under DEBUG — without it, live reload dies on the enrolment page itself.
EXEMPT_PREFIXES = ("/accounts/", "/static/", "/media/", "/__reload__/")
ELEVATED_ROLES = (ClubRole.Roles.ADMIN, ClubRole.Roles.EDITOR) ELEVATED_ROLES = (ClubRole.Roles.ADMIN, ClubRole.Roles.EDITOR)

View File

@@ -17,6 +17,7 @@ dependencies = [
[dependency-groups] [dependency-groups]
dev = [ dev = [
"coverage>=7.15.0", "coverage>=7.15.0",
"django-browser-reload>=1.21.0",
"ruff>=0.15.17", "ruff>=0.15.17",
] ]

View File

@@ -81,6 +81,13 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
# Reload the browser when templates, static files or Python change. Dev only: it
# injects a script tag into every HTML response and serves an open event stream,
# neither of which belongs in production.
if DEBUG:
INSTALLED_APPS += ["django_browser_reload"]
MIDDLEWARE += ["django_browser_reload.middleware.BrowserReloadMiddleware"]
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend", "django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend", "allauth.account.auth_backends.AuthenticationBackend",

30
rosterchief/tests.py Normal file
View File

@@ -0,0 +1,30 @@
import importlib
from django.test import SimpleTestCase, override_settings
from django.urls import Resolver404, clear_url_caches, resolve
from . import urls
class BrowserReloadUrlTests(SimpleTestCase):
"""django-browser-reload serves an open event stream and injects a script into
every HTML response, so it must never be mounted outside DEBUG."""
def reload_urlconf(self):
importlib.reload(urls)
clear_url_caches()
def tearDown(self):
self.reload_urlconf() # restore the real (DEBUG=False) urlconf
def test_the_reload_endpoint_is_mounted_in_debug(self):
with override_settings(DEBUG=True):
self.reload_urlconf()
self.assertEqual(resolve("/__reload__/events/").view_name, "django_browser_reload:events")
def test_the_reload_endpoint_is_absent_without_debug(self):
self.reload_urlconf()
with self.assertRaises(Resolver404):
resolve("/__reload__/events/")

View File

@@ -7,6 +7,7 @@ second factors. ``RequireMFAMiddleware`` then blocks any staff user who has not
enrolled. enrolled.
""" """
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView from django.views.generic import RedirectView
@@ -17,3 +18,6 @@ urlpatterns = [
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
path("controlpanel/", include("controlpanel.urls")), path("controlpanel/", include("controlpanel.urls")),
] ]
if settings.DEBUG:
urlpatterns += [path("__reload__/", include("django_browser_reload.urls"))]

BIN
static/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -27,6 +27,10 @@
--font-weight-medium: 500; --font-weight-medium: 500;
--font-weight-semibold: 600; --font-weight-semibold: 600;
--font-weight-bold: 700; --font-weight-bold: 700;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans); --default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono); --default-mono-font-family: var(--font-mono);
} }
@@ -464,6 +468,26 @@
} }
} }
} }
.collapse-plus {
@layer daisyui.l1.l2 {
> .collapse-title:after {
position: absolute;
display: block;
height: 0.5rem;
width: 0.5rem;
@media (prefers-reduced-motion: no-preference) {
transition-property: all;
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
top: 0.9rem;
inset-inline-end: 1.4rem;
--tw-content: "+";
content: var(--tw-content);
pointer-events: none;
}
}
}
.dropdown { .dropdown {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
position: relative; position: relative;
@@ -917,6 +941,21 @@
} }
} }
} }
.collapse-open {
@layer daisyui.l1.l2 {
grid-template-rows: max-content 1fr;
> .collapse-content {
--overflow-delay: 0.2s;
overflow: revert-layer;
content-visibility: visible;
min-height: fit-content;
padding-bottom: 1rem;
@supports not (content-visibility: visible) {
visibility: visible;
}
}
}
}
.collapse { .collapse {
visibility: collapse; visibility: collapse;
} }
@@ -982,6 +1021,27 @@
} }
} }
} }
.toast {
@layer daisyui.l1.l2.l3 {
position: fixed;
inset-inline-start: auto;
inset-inline-end: calc(0.25rem * 4);
top: auto;
bottom: calc(0.25rem * 4);
display: flex;
flex-direction: column;
gap: calc(0.25rem * 2);
background-color: transparent;
translate: var(--toast-x, 0) var(--toast-y, 0);
width: max-content;
max-width: calc(100vw - 2rem);
& > * {
@media (prefers-reduced-motion: no-preference) {
animation: toast 0.25s ease-out;
}
}
}
}
.toggle { .toggle {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
border: var(--border) solid currentColor; border: var(--border) solid currentColor;
@@ -1318,6 +1378,51 @@
} }
} }
} }
.aura {
@layer daisyui.l1.l2.l3 {
position: relative;
display: inline-block;
--aura-padding: 0.125rem;
padding: var(--aura-padding);
border-radius: calc(var(--aura-padding) + var(--aura-radius, var(--radius-box)));
animation: aura var(--tw-duration, 6s) linear infinite;
background-image: conic-gradient(from var(--aura-angle), transparent 225deg, currentColor);
&:has( > .card, > .alert) {
--aura-radius: var(--radius-box);
}
&:has( > .btn, > .input, > .select) {
--aura-radius: var(--radius-field);
}
&:has( > .checkbox, > .toggle, > .badge) {
--aura-radius: var(--radius-selector);
}
&:before, &:after {
animation: inherit;
background-color: inherit;
background-image: inherit;
border-radius: inherit;
position: absolute;
top: calc(1 / 2 * 100%);
left: calc(1 / 2 * 100%);
z-index: 0;
display: block;
opacity: 70%;
filter: blur(0.25rem);
translate: -50% -50%;
width: 100%;
height: 100%;
content: "";
}
&:after {
opacity: 30%;
filter: blur(1rem);
}
& > * {
position: relative;
z-index: 1;
}
}
}
.select { .select {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
position: relative; position: relative;
@@ -1666,6 +1771,48 @@
} }
} }
} }
.rating {
@layer daisyui.l1.l2.l3 {
position: relative;
display: inline-flex;
vertical-align: middle;
--size: var(--size-selector, 0.25rem) * 6;
input {
cursor: pointer;
appearance: none;
}
* {
border-radius: 0;
background-color: var(--color-base-content);
opacity: 20%;
width: calc(var(--size) * 1);
height: calc(var(--size));
@media (prefers-reduced-motion: no-preference) {
animation: rating 0.25s ease-out;
}
}
.rating-hidden {
width: calc(0.25rem * 2);
background-color: transparent;
}
:checked, [aria-checked="true"], [aria-current="true"], :has( ~ :checked, ~ [aria-checked="true"], ~ [aria-current="true"]) {
opacity: 100%;
}
:focus-visible {
scale: 1.1;
@media (prefers-reduced-motion: no-preference) {
transition: scale 0.2s ease-out;
}
}
:active:focus {
animation: none;
scale: 1.1;
}
}
@layer daisyui.l1.l2 {
--size: var(--size-selector, 0.25rem) * 6;
}
}
.navbar { .navbar {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
display: flex; display: flex;
@@ -1734,9 +1881,89 @@
border-radius: var(--radius-box); border-radius: var(--radius-box);
} }
} }
.progress {
@layer daisyui.l1.l2.l3 {
position: relative;
height: calc(0.25rem * 2);
width: 100%;
appearance: none;
overflow: hidden;
border-radius: var(--radius-box);
background-color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, currentcolor 20%, transparent);
}
color: var(--color-base-content);
&:indeterminate {
background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% );
background-size: 200%;
background-position-x: 15%;
@media (prefers-reduced-motion: no-preference) {
animation: progress 5s ease-in-out infinite;
}
@supports (-moz-appearance: none) {
&::-moz-progress-bar {
background-color: transparent;
@media (prefers-reduced-motion: no-preference) {
animation: progress 5s ease-in-out infinite;
background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% );
background-size: 200%;
background-position-x: 15%;
}
}
}
}
@supports (-moz-appearance: none) {
&::-moz-progress-bar {
border-radius: var(--radius-box);
background-color: currentcolor;
@media (prefers-reduced-motion: no-preference) {
transition: inline-size 0.3s ease;
}
}
}
@supports (-webkit-appearance: none) {
&::-webkit-progress-bar {
border-radius: var(--radius-box);
background-color: transparent;
}
&::-webkit-progress-value {
border-radius: var(--radius-box);
background-color: currentColor;
@media (prefers-reduced-motion: no-preference) {
transition: inline-size 0.3s ease;
}
}
}
}
}
.static { .static {
position: static; position: static;
} }
.dropdown-right {
@layer daisyui.l1.l2 {
--anchor-h: right;
--anchor-v: span-bottom;
.dropdown-content {
inset-inline-start: 100%;
top: 0;
bottom: auto;
transform-origin: 0;
}
}
}
.dropdown-left {
@layer daisyui.l1.l2 {
--anchor-h: left;
--anchor-v: span-bottom;
.dropdown-content {
inset-inline-end: 100%;
top: 0;
bottom: auto;
transform-origin: 100%;
}
}
}
.dropdown-end { .dropdown-end {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
--anchor-h: span-left; --anchor-h: span-left;
@@ -1931,6 +2158,26 @@
.z-10 { .z-10 {
z-index: 10; z-index: 10;
} }
.tab-content {
@layer daisyui.l1.l2.l3 {
order: var(--tabcontent-order);
display: none;
border-color: transparent;
--tabcontent-radius-ss: var(--radius-box);
--tabcontent-radius-se: var(--radius-box);
--tabcontent-radius-es: var(--radius-box);
--tabcontent-radius-ee: var(--radius-box);
--tabcontent-order: 1;
width: 100%;
height: calc(100% - var(--tab-height) + var(--border));
margin: var(--tabcontent-margin);
border-width: var(--border);
border-start-start-radius: var(--tabcontent-radius-ss);
border-start-end-radius: var(--tabcontent-radius-se);
border-end-start-radius: var(--tabcontent-radius-es);
border-end-end-radius: var(--tabcontent-radius-ee);
}
}
.hero { .hero {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
display: grid; display: grid;
@@ -2328,6 +2575,46 @@
.flex-none { .flex-none {
flex: none; flex: none;
} }
.flex-shrink {
flex-shrink: 1;
}
.flex-grow {
flex-grow: 1;
}
.border-collapse {
border-collapse: collapse;
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.skeleton {
@layer daisyui.l1.l2.l3 {
border-radius: var(--radius-box);
background-color: var(--color-base-300);
@media (prefers-reduced-motion: reduce) {
transition-duration: 15s;
}
will-change: background-position;
background-image: linear-gradient( 105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100% );
background-size: 200% auto;
background-position-x: -50%;
@media (prefers-reduced-motion: no-preference) {
animation: skeleton 1.8s ease-in-out infinite;
}
}
}
.aura-glow {
@layer daisyui.l1.l2 {
animation: none;
background-image: radial-gradient(closest-corner at center, currentColor 0%, transparent 90%);
&:before {
animation: aura-glow var(--tw-duration, 6s) ease-out infinite;
}
&:after {
animation: aura-glow-after var(--tw-duration, 6s) ease-out infinite;
}
}
}
.link { .link {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
cursor: pointer; cursor: pointer;
@@ -2349,6 +2636,9 @@
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.resize {
resize: both;
}
.flex-wrap { .flex-wrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -2435,6 +2725,9 @@
.bg-base-200 { .bg-base-200 {
background-color: var(--color-base-200); background-color: var(--color-base-200);
} }
.bg-base-300 {
background-color: var(--color-base-300);
}
.btn-link { .btn-link {
@layer daisyui { @layer daisyui {
text-decoration-line: underline; text-decoration-line: underline;
@@ -2553,6 +2846,9 @@
--alert-color: var(--color-warning); --alert-color: var(--color-warning);
} }
} }
.text-base-content {
color: var(--color-base-content);
}
.text-base-content\/70 { .text-base-content\/70 {
color: var(--color-base-content); color: var(--color-base-content);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -2587,6 +2883,9 @@
text-decoration-line: none; text-decoration-line: none;
} }
} }
.underline {
text-decoration-line: underline;
}
.opacity-60 { .opacity-60 {
opacity: 60%; opacity: 60%;
} }
@@ -2601,9 +2900,26 @@
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.filter { .filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
} }
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.ease-in-out {
--tw-ease: var(--ease-in-out);
transition-timing-function: var(--ease-in-out);
}
.ease-out {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.btn-error { .btn-error {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
--btn-color: var(--color-error); --btn-color: var(--color-error);
@@ -2683,6 +2999,12 @@
} }
} }
} }
[data-theme="light"] .logo {
content: var(--logo-dark);
}
[data-theme="dark"] .logo {
content: var(--logo-light);
}
@layer base { @layer base {
:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] { :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] {
color-scheme: light; color-scheme: light;
@@ -2969,6 +3291,26 @@
opacity: 0; opacity: 0;
} }
} }
@property --tw-rotate-x {
syntax: "*";
inherits: false;
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
}
@property --tw-space-y-reverse { @property --tw-space-y-reverse {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -3073,6 +3415,11 @@
inherits: false; inherits: false;
initial-value: 0 0 #0000; initial-value: 0 0 #0000;
} }
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur { @property --tw-blur {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -3126,9 +3473,18 @@
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
} }
@property --tw-ease {
syntax: "*";
inherits: false;
}
@layer properties { @layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop { *, ::before, ::after, ::backdrop {
--tw-rotate-x: initial;
--tw-rotate-y: initial;
--tw-rotate-z: initial;
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
--tw-divide-y-reverse: 0; --tw-divide-y-reverse: 0;
--tw-border-style: solid; --tw-border-style: solid;
@@ -3152,6 +3508,7 @@
--tw-ring-offset-width: 0px; --tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff; --tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000; --tw-ring-offset-shadow: 0 0 #0000;
--tw-outline-style: solid;
--tw-blur: initial; --tw-blur: initial;
--tw-brightness: initial; --tw-brightness: initial;
--tw-contrast: initial; --tw-contrast: initial;
@@ -3165,6 +3522,7 @@
--tw-drop-shadow-color: initial; --tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%; --tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial; --tw-drop-shadow-size: initial;
--tw-ease: initial;
} }
} }
} }

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="200" height="200">
<path d="M50 4 L90 18 V50 C90 76 72 92 50 98 C28 92 10 76 10 50 V18 Z" fill="#0F1A2E"></path>
<rect x="27" y="30" width="46" height="8" rx="4" fill="#FFFFFF"></rect>
<rect x="27" y="44" width="34" height="8" rx="4" fill="#FFFFFF"></rect>
<path d="M27 80 L50 62 L73 80 L66 85 L50 72 L34 85 Z" fill="#FFFFFF"></path>
</svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="200" height="200">
<path d="M50 4 L90 18 V50 C90 76 72 92 50 98 C28 92 10 76 10 50 V18 Z" fill="#FFFFFF"></path>
<rect x="27" y="30" width="46" height="8" rx="4" fill="#0F1A2E"></rect>
<rect x="27" y="44" width="34" height="8" rx="4" fill="#0F1A2E"></rect>
<path d="M27 80 L50 62 L73 80 L66 85 L50 72 L34 85 Z" fill="#0F1A2E"></path>
</svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -10,3 +10,11 @@
@plugin "daisyui" { @plugin "daisyui" {
themes: light --default, dark --prefersdark; themes: light --default, dark --prefersdark;
} }
[data-theme="light"] .logo {
content: var(--logo-dark);
}
[data-theme="dark"] .logo {
content: var(--logo-light);
}

View File

@@ -0,0 +1,33 @@
{% load static lucide %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RosterChief - Control Panel</title>
<script>
(() => {
const stored = localStorage.getItem('theme');
if (stored) document.documentElement.setAttribute("data-theme", stored);
})();
</script>
<style>
:root {
--logo-light: url("{% static "images/rosterchief-white.svg" %}");
--logo-dark: url("{% static "images/rosterchief-black.svg" %}");
}
</style>
<link rel="stylesheet" href="{% static 'css/app.css' %}"/>
{% block extra %}{% endblock extra %}
</head>
<body class="min-h-screen bg-base-200">
{% block main %}{% endblock main %}
</body>
</html>

View File

@@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "_controlpanel_base.html" %}
{% block title %} {% block title %}
{% block head_title %}{% endblock head_title %} · RosterChief {% block head_title %}{% endblock head_title %} · RosterChief

15
uv.lock generated
View File

@@ -213,6 +213,19 @@ mfa = [
{ name = "qrcode" }, { name = "qrcode" },
] ]
[[package]]
name = "django-browser-reload"
version = "1.21.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/ef/ab407c1a2f14e13c75a6419a2d124954aa0364f62580ad95a3d31dc6c73d/django_browser_reload-1.21.0.tar.gz", hash = "sha256:3335ad3d107eb657f623d1a8e680dfbcab8a83ae1f94df1895e069dddf5604ba", size = 15800, upload-time = "2025-09-22T17:00:35.199Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/61/1b4a8c589652859995bcab87682286443eb9fdf2d7fd584975b9ffc1db33/django_browser_reload-1.21.0-py3-none-any.whl", hash = "sha256:0b2a86ab460774fa9bb142a121c70e75a72f18109f51a4f6de409cd633d3a70d", size = 12852, upload-time = "2025-09-22T17:00:33.479Z" },
]
[[package]] [[package]]
name = "django-lucide" name = "django-lucide"
version = "1.3.1" version = "1.3.1"
@@ -379,6 +392,7 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "coverage" }, { name = "coverage" },
{ name = "django-browser-reload" },
{ name = "ruff" }, { name = "ruff" },
] ]
@@ -398,6 +412,7 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "coverage", specifier = ">=7.15.0" }, { name = "coverage", specifier = ">=7.15.0" },
{ name = "django-browser-reload", specifier = ">=1.21.0" },
{ name = "ruff", specifier = ">=0.15.17" }, { name = "ruff", specifier = ">=0.15.17" },
] ]