From 334c706aec9c5f8995a4d482622d8ce7285df399 Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Mon, 13 Jul 2026 18:32:19 +0200 Subject: [PATCH] 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 --- .DS_Store | Bin 0 -> 6148 bytes authentication/middleware.py | 4 +- pyproject.toml | 1 + rosterchief/settings.py | 7 + rosterchief/tests.py | 30 ++ rosterchief/urls.py | 4 + static/.DS_Store | Bin 0 -> 6148 bytes static/css/app.css | 358 ++++++++++++++++++ static/images/rosterchief-dark.svg | 6 + static/images/rosterchief-white.svg | 6 + static/src/app.css | 10 +- templates/_controlpanel_base.html | 33 ++ .../allauth/controlpanel/services/__init__.py | 0 .../controlpanel/templatetags/__init__.py | 0 templates/allauth/layouts/base.html | 2 +- uv.lock | 15 + 16 files changed, 473 insertions(+), 3 deletions(-) create mode 100644 .DS_Store create mode 100644 rosterchief/tests.py create mode 100644 static/.DS_Store create mode 100644 static/images/rosterchief-dark.svg create mode 100644 static/images/rosterchief-white.svg create mode 100644 templates/_controlpanel_base.html delete mode 100644 templates/allauth/controlpanel/services/__init__.py delete mode 100644 templates/allauth/controlpanel/templatetags/__init__.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3ba24ff755e2d6d4124ab235b082f25de93b7339 GIT binary patch literal 6148 zcmeHK&2G~`5T0p6Nn1*tG$|Euh{&ZA zILAnYK{Q>z#k7gq3q0zwhqgHW#C_9fZyE(NFW7|bn5-Z51nBcB+$MawGV>ybtcVsBzS~wC%&T8^Z z9`E@wYQ$n3h~ri&UZu_8HHC^lCG3mtlT+8h$&kjBP918}d$ADFqkbH8>%Cqg($)`l zo2&V;48w*@f;5m(`%qO+#b7V$Nvgx3-91Pe1y;N6~5sLuH!Y_!s~bwZ=s7jxQ`F9g`Uoyqpz=$s|F3=b26sx z?zkC8p^W icB~BEiZ@7=P^`iRp=&VHNFEaVBcN!ojb-4kGVmSMmXTKg literal 0 HcmV?d00001 diff --git a/authentication/middleware.py b/authentication/middleware.py index 4cfe4af..8cbe9ac 100644 --- a/authentication/middleware.py +++ b/authentication/middleware.py @@ -17,7 +17,9 @@ from django.urls import reverse from club.models import ClubRole #: 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) diff --git a/pyproject.toml b/pyproject.toml index fbfc6cc..7fa16ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ [dependency-groups] dev = [ "coverage>=7.15.0", + "django-browser-reload>=1.21.0", "ruff>=0.15.17", ] diff --git a/rosterchief/settings.py b/rosterchief/settings.py index fb1411c..fdd3375 100644 --- a/rosterchief/settings.py +++ b/rosterchief/settings.py @@ -81,6 +81,13 @@ MIDDLEWARE = [ "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 = [ "django.contrib.auth.backends.ModelBackend", "allauth.account.auth_backends.AuthenticationBackend", diff --git a/rosterchief/tests.py b/rosterchief/tests.py new file mode 100644 index 0000000..2dcf681 --- /dev/null +++ b/rosterchief/tests.py @@ -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/") diff --git a/rosterchief/urls.py b/rosterchief/urls.py index 15fb04f..c743dff 100644 --- a/rosterchief/urls.py +++ b/rosterchief/urls.py @@ -7,6 +7,7 @@ second factors. ``RequireMFAMiddleware`` then blocks any staff user who has not enrolled. """ +from django.conf import settings from django.contrib import admin from django.urls import include, path from django.views.generic import RedirectView @@ -17,3 +18,6 @@ urlpatterns = [ path("accounts/", include("allauth.urls")), path("controlpanel/", include("controlpanel.urls")), ] + +if settings.DEBUG: + urlpatterns += [path("__reload__/", include("django_browser_reload.urls"))] diff --git a/static/.DS_Store b/static/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..bff4bec45682bb98a49c4f1d2813076da976b4fa GIT binary patch literal 6148 zcmeHKPfrs;6n_JiE+Dc%i}>fTu@@6atRfiV!KGjf@c>~7lmOPOEGU@cya(RsNirB>}GfC&HwHjbGg z?9LGG=dDNjl%*e$h%qR{UOR4j0rPsIwLvqW8Ti{6;NR{5_|O0s>d^iDzVn)aA1B4) zC(_rSIX93svf1J6p7VsYoy1LAamkH#_|pbsftTv8x9QV%#mp_SFme5mMk2wlQ4HDL z^1~Wymsl&T#Uj^{4r9Y>_{$cMD3&!(A{H8?bUe&oWaIW|5qmK)3G^OG0zQ{$Jf zRLlv_tsc}PzT71iu&5NYXvG|jvFMd53)dogh{cu@`aE&`7w8)QC1IV`9-W#tj+z*g zJ2i+wA4HUfJN3w~t?uu~G%5e2Z}Zuz5etGf7W;|M!tGr#dqNv4;XcC|`3hE>+!b%q zC`RnbDQ~+`~giMlaWSY#7IdX^C zWRa|sEm9_~%ijOjb1a3n!j44rpb(jgC{u~LVi1{*c1y+C3Of>I zIuLdFAZli!ZYV^|j^kS*9EdGZ_nHCCKsN*Y-yg-~=l}iR-~UCB?lc3Mfq#<$l38|^ zOIVvaTYIYHXRU?pF*Ytdt|L*TV5g2_q41-43Y!r0SzJN16?P<|1x5b|2pV*!8Tg|N F`~Yf<$u9r^ literal 0 HcmV?d00001 diff --git a/static/css/app.css b/static/css/app.css index b421c9f..55fecd1 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -27,6 +27,10 @@ --font-weight-medium: 500; --font-weight-semibold: 600; --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-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 { @layer daisyui.l1.l2.l3 { 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 { 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 { @layer daisyui.l1.l2.l3 { 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 { @layer daisyui.l1.l2.l3 { 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 { @layer daisyui.l1.l2.l3 { display: flex; @@ -1734,9 +1881,89 @@ 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 { 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 { @layer daisyui.l1.l2 { --anchor-h: span-left; @@ -1931,6 +2158,26 @@ .z-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 { @layer daisyui.l1.l2.l3 { display: grid; @@ -2328,6 +2575,46 @@ .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 { @layer daisyui.l1.l2.l3 { cursor: pointer; @@ -2349,6 +2636,9 @@ .cursor-pointer { cursor: pointer; } + .resize { + resize: both; + } .flex-wrap { flex-wrap: wrap; } @@ -2435,6 +2725,9 @@ .bg-base-200 { background-color: var(--color-base-200); } + .bg-base-300 { + background-color: var(--color-base-300); + } .btn-link { @layer daisyui { text-decoration-line: underline; @@ -2553,6 +2846,9 @@ --alert-color: var(--color-warning); } } + .text-base-content { + color: var(--color-base-content); + } .text-base-content\/70 { color: var(--color-base-content); @supports (color: color-mix(in lab, red, red)) { @@ -2587,6 +2883,9 @@ text-decoration-line: none; } } + .underline { + text-decoration-line: underline; + } .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)); 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: 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 { @layer daisyui.l1.l2 { --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 { :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] { color-scheme: light; @@ -2969,6 +3291,26 @@ 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 { syntax: "*"; inherits: false; @@ -3073,6 +3415,11 @@ inherits: false; initial-value: 0 0 #0000; } +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} @property --tw-blur { syntax: "*"; inherits: false; @@ -3126,9 +3473,18 @@ syntax: "*"; inherits: false; } +@property --tw-ease { + syntax: "*"; + inherits: false; +} @layer properties { @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 { + --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-divide-y-reverse: 0; --tw-border-style: solid; @@ -3152,6 +3508,7 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; --tw-contrast: initial; @@ -3165,6 +3522,7 @@ --tw-drop-shadow-color: initial; --tw-drop-shadow-alpha: 100%; --tw-drop-shadow-size: initial; + --tw-ease: initial; } } } diff --git a/static/images/rosterchief-dark.svg b/static/images/rosterchief-dark.svg new file mode 100644 index 0000000..d58f99f --- /dev/null +++ b/static/images/rosterchief-dark.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/static/images/rosterchief-white.svg b/static/images/rosterchief-white.svg new file mode 100644 index 0000000..12f0bfb --- /dev/null +++ b/static/images/rosterchief-white.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/static/src/app.css b/static/src/app.css index bc76a63..3389bfe 100644 --- a/static/src/app.css +++ b/static/src/app.css @@ -8,5 +8,13 @@ /* daisyUI: light is the default, dark applies automatically when the OS asks for it. An explicit data-theme on (set by the toggle) overrides both. */ @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); } diff --git a/templates/_controlpanel_base.html b/templates/_controlpanel_base.html new file mode 100644 index 0000000..02581df --- /dev/null +++ b/templates/_controlpanel_base.html @@ -0,0 +1,33 @@ +{% load static lucide %} + + + + + + + + RosterChief - Control Panel + + + + + + + + {% block extra %}{% endblock extra %} + + + + {% block main %}{% endblock main %} + + \ No newline at end of file diff --git a/templates/allauth/controlpanel/services/__init__.py b/templates/allauth/controlpanel/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/templates/allauth/controlpanel/templatetags/__init__.py b/templates/allauth/controlpanel/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/templates/allauth/layouts/base.html b/templates/allauth/layouts/base.html index dbef0b3..07dfacd 100644 --- a/templates/allauth/layouts/base.html +++ b/templates/allauth/layouts/base.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "_controlpanel_base.html" %} {% block title %} {% block head_title %}{% endblock head_title %} · RosterChief diff --git a/uv.lock b/uv.lock index 476e066..cecb875 100644 --- a/uv.lock +++ b/uv.lock @@ -213,6 +213,19 @@ mfa = [ { 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]] name = "django-lucide" version = "1.3.1" @@ -379,6 +392,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "coverage" }, + { name = "django-browser-reload" }, { name = "ruff" }, ] @@ -398,6 +412,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "coverage", specifier = ">=7.15.0" }, + { name = "django-browser-reload", specifier = ">=1.21.0" }, { name = "ruff", specifier = ">=0.15.17" }, ]