Containerise: Dockerfile, Compose stack and wildcard TLS
One server now, the same image and env vars for many later: point DJANGO_DATABASE_URL / DJANGO_REDIS_URL at central services, set a bucket, drop the db and redis services, run several web containers behind a load balancer. No code changes. The wildcard certificate is what shapes this. Subdomain tenancy needs *.rosterchief.app, and Let's Encrypt will not issue a wildcard over HTTP-01 -- only DNS-01 -- so Caddy is built with a DNS provider plugin and needs an API token. That single constraint is why the proxy is Caddy rather than the usual nginx+certbot. The image apt-installs libpango and friends, which is what WeasyPrint binds to. The PDF invoices that cannot render on a Mac without Homebrew work in the container by construction. Migrations are NOT run by the entrypoint: with more than one web container they would race, and a starting gunicorn worker is a bad place to discover a failed migration. Deploy runs them once, explicitly. Two things the local build check caught, either of which would have failed the image build at collectstatic (manifest storage treats a missing referenced file as fatal): - chart.js ended with a sourceMappingURL pointing at a .map we never vendored. Stripped, with an npm script so re-vendoring cannot bring it back. - The Tailwind INPUT file lived at static/src/app.css, inside the served static tree, so collectstatic collected it and then choked on its @import "tailwindcss". It belongs outside: it is a build input, not an asset. Now assets/app.css. Verified locally under gunicorn + WhiteNoise + manifest storage: pages serve and the CSS comes back hashed. The image itself is unverified -- there is no container runtime on this machine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
.git
|
||||
.venv
|
||||
node_modules
|
||||
staticfiles
|
||||
media
|
||||
db.sqlite3
|
||||
.env
|
||||
*.pyc
|
||||
__pycache__
|
||||
.coverage
|
||||
.idea
|
||||
.ruff_cache
|
||||
ARCHITECTURE.pdf
|
||||
10
.env.compose.example
Normal file
10
.env.compose.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# Copy to .env — read by docker compose itself (not by Django).
|
||||
ROSTERCHIEF_BASE_DOMAIN=rosterchief.app
|
||||
ACME_EMAIL=you@example.com
|
||||
|
||||
# DNS-01 is the only way to get the *.rosterchief.app wildcard. Token needs DNS:Edit on the zone.
|
||||
CLOUDFLARE_API_TOKEN=
|
||||
|
||||
POSTGRES_DB=rosterchief
|
||||
POSTGRES_USER=rosterchief
|
||||
POSTGRES_PASSWORD=
|
||||
38
.env.production.example
Normal file
38
.env.production.example
Normal file
@@ -0,0 +1,38 @@
|
||||
# Copy to .env.production and fill in. Everything here is read by python-decouple.
|
||||
|
||||
# --- Django ---
|
||||
DJANGO_SECRET_KEY= # python -c "import secrets; print(secrets.token_urlsafe(64))"
|
||||
DJANGO_DEBUG=False
|
||||
# The leading dot matches every club subdomain.
|
||||
DJANGO_ALLOWED_HOSTS=.rosterchief.app
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://rosterchief.app,https://*.rosterchief.app
|
||||
DJANGO_TIME_ZONE=Europe/Brussels
|
||||
|
||||
# --- Tenancy ---
|
||||
# Drives subdomain resolution, the shared session cookie, and the WebAuthn RP ID (one passkey
|
||||
# across every club).
|
||||
ROSTERCHIEF_BASE_DOMAIN=rosterchief.app
|
||||
ROSTERCHIEF_RP_NAME=RosterChief
|
||||
|
||||
# --- Services ---
|
||||
DJANGO_DATABASE_URL=postgres://rosterchief:CHANGEME@db:5432/rosterchief
|
||||
DJANGO_REDIS_URL=redis://redis:6379/0
|
||||
|
||||
# --- HTTPS (off by default in code; the deploy is what turns them on) ---
|
||||
DJANGO_SECURE_SSL_REDIRECT=True
|
||||
DJANGO_SESSION_COOKIE_SECURE=True
|
||||
DJANGO_CSRF_COOKIE_SECURE=True
|
||||
DJANGO_SECURE_HSTS_SECONDS=31536000
|
||||
DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=True
|
||||
# Preload is a one-way door — turn it on only once the wildcard cert has proven itself.
|
||||
DJANGO_SECURE_HSTS_PRELOAD=False
|
||||
|
||||
# --- Static ---
|
||||
DJANGO_STATICFILES_BACKEND=whitenoise.storage.CompressedManifestStaticFilesStorage
|
||||
|
||||
# --- Uploads: set these and club logos move off local disk (required for >1 app server) ---
|
||||
# AWS_STORAGE_BUCKET_NAME=rosterchief-media
|
||||
# AWS_S3_ENDPOINT_URL=https://fsn1.your-objectstorage.com
|
||||
# AWS_S3_REGION_NAME=fsn1
|
||||
# AWS_ACCESS_KEY_ID=
|
||||
# AWS_SECRET_ACCESS_KEY=
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -379,3 +379,4 @@ pyrightconfig.json
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,django%
|
||||
# Node
|
||||
node_modules/
|
||||
staticfiles/
|
||||
|
||||
73
Dockerfile
Normal file
73
Dockerfile
Normal file
@@ -0,0 +1,73 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# --- 1. the stylesheet -------------------------------------------------------
|
||||
# Tailwind is a build-time concern: the CSS it emits is committed, but building it here means
|
||||
# the image never depends on someone having remembered to run `npm run build`.
|
||||
FROM node:22-slim AS css
|
||||
|
||||
WORKDIR /build
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY assets ./assets
|
||||
COPY templates ./templates
|
||||
COPY controlpanel ./controlpanel
|
||||
COPY billing ./billing
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# --- 2. the runtime ----------------------------------------------------------
|
||||
FROM python:3.14-slim AS app
|
||||
|
||||
# WeasyPrint binds to these at import: no pango, no invoices. This is also why building the
|
||||
# PDF path in a container is easier than on a Mac — apt has what Homebrew would have to.
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
libpango-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
libharfbuzz0b \
|
||||
libffi8 \
|
||||
libjpeg62-turbo \
|
||||
libopenjp2-7 \
|
||||
shared-mime-info \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependencies first: they change far less often than the code, so this layer caches.
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --no-install-project
|
||||
|
||||
COPY . .
|
||||
COPY --from=css /build/static/css/app.css ./static/css/app.css
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev
|
||||
|
||||
# collectstatic needs a settings module that imports: a throwaway key, never used at runtime.
|
||||
RUN DJANGO_SECRET_KEY=build-only-not-a-secret \
|
||||
DJANGO_STATICFILES_BACKEND=whitenoise.storage.CompressedManifestStaticFilesStorage \
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
RUN useradd --system --uid 1000 rosterchief && chown -R rosterchief /app
|
||||
USER rosterchief
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Migrations are NOT run here. With more than one app container they would race, and a failed
|
||||
# migration inside a starting web process is a bad place to find out — deploy runs them once,
|
||||
# explicitly (see DEPLOYMENT.md).
|
||||
CMD ["gunicorn", "rosterchief.wsgi:application", \
|
||||
"--bind", "0.0.0.0:8000", \
|
||||
"--workers", "3", \
|
||||
"--threads", "4", \
|
||||
"--timeout", "60", \
|
||||
"--access-logfile", "-", \
|
||||
"--error-logfile", "-"]
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
/* Scan Django templates for utility classes (Tailwind's auto-detection doesn't
|
||||
know about our template dirs). */
|
||||
@source "../../templates";
|
||||
@source "../../controlpanel";
|
||||
@source "../templates";
|
||||
@source "../controlpanel";
|
||||
@source "../billing";
|
||||
|
||||
/* daisyUI: light is the default, dark applies automatically when the OS asks
|
||||
for it. An explicit data-theme on <html> (set by the toggle) overrides both. */
|
||||
72
compose.yaml
Normal file
72
compose.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
# One server. The same image and the same environment variables run a multi-server
|
||||
# deployment: point DJANGO_DATABASE_URL / DJANGO_REDIS_URL at your central services, set a
|
||||
# bucket, drop the `db` and `redis` services, and run several `web` containers behind a load
|
||||
# balancer. Nothing in the code changes.
|
||||
|
||||
name: rosterchief
|
||||
|
||||
services:
|
||||
caddy:
|
||||
build:
|
||||
context: ./deploy/caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
environment:
|
||||
# A wildcard certificate for *.rosterchief.app cannot be issued over HTTP-01 — Let's
|
||||
# Encrypt only does wildcards via DNS-01. That is why Caddy needs a DNS API token, and
|
||||
# why this image is built with the provider's DNS plugin rather than pulled as-is.
|
||||
ROSTERCHIEF_BASE_DOMAIN: ${ROSTERCHIEF_BASE_DOMAIN:?set the base domain, e.g. rosterchief.app}
|
||||
ACME_EMAIL: ${ACME_EMAIL:?set an email for Let's Encrypt}
|
||||
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN:?DNS-01 needs an API token with DNS:Edit on the zone}
|
||||
volumes:
|
||||
- ./deploy/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
web:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
env_file: .env.production
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:8000/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-rosterchief}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-rosterchief}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set a database password}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rosterchief}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
||||
# Cache only, so nothing here needs to survive a restart. It is not optional though: it
|
||||
# is what keeps every gunicorn worker agreeing about which feature flags are on.
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
24
deploy/caddy/Caddyfile
Normal file
24
deploy/caddy/Caddyfile
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
# The bare domain (control panel, admin, auth) and every club subdomain, on one certificate.
|
||||
{$ROSTERCHIEF_BASE_DOMAIN}, *.{$ROSTERCHIEF_BASE_DOMAIN} {
|
||||
tls {
|
||||
dns cloudflare {$CLOUDFLARE_API_TOKEN}
|
||||
}
|
||||
|
||||
encode zstd gzip
|
||||
|
||||
# X-Forwarded-Proto is what SECURE_PROXY_SSL_HEADER reads. Without it Django believes every
|
||||
# request is plain HTTP: request.is_secure() goes false, WebAuthn disagrees with the browser
|
||||
# about the origin, and the SSL redirect becomes a loop.
|
||||
reverse_proxy web:8000 {
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
|
||||
log {
|
||||
output stdout
|
||||
}
|
||||
}
|
||||
9
deploy/caddy/Dockerfile
Normal file
9
deploy/caddy/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
# Caddy with a DNS plugin. The stock image cannot solve a DNS-01 challenge, and DNS-01 is the
|
||||
# only way Let's Encrypt issues the *.rosterchief.app wildcard that subdomain tenancy needs.
|
||||
#
|
||||
# Swap the module if your DNS lives elsewhere: caddy-dns/route53, caddy-dns/digitalocean, ...
|
||||
FROM caddy:2-builder AS builder
|
||||
RUN xcaddy build --with github.com/caddy-dns/cloudflare
|
||||
|
||||
FROM caddy:2
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
@@ -3,8 +3,9 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tailwindcss -i ./static/src/app.css -o ./static/css/app.css --minify",
|
||||
"watch": "tailwindcss -i ./static/src/app.css -o ./static/css/app.css --watch"
|
||||
"build": "tailwindcss -i ./assets/app.css -o ./static/css/app.css --minify",
|
||||
"watch": "tailwindcss -i ./assets/app.css -o ./static/css/app.css --watch",
|
||||
"vendor:chart": "node -e \"const fs=require('fs');const s=fs.readFileSync('node_modules/chart.js/dist/chart.umd.js','utf8').split('\\n').filter(l=>!l.startsWith('//# sourceMappingURL=')).join('\\n');fs.writeFileSync('static/js/chart.js',s)\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
--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);
|
||||
--font-ubuntu: "Ubuntu", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-roboto: "Roboto", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-tourney: "Tourney", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
@@ -189,6 +191,67 @@
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
.diff {
|
||||
@layer daisyui.l1.l2 {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
webkit-user-select: none;
|
||||
user-select: none;
|
||||
align-items: normal;
|
||||
grid-template-rows: 1fr 1.8rem 1fr;
|
||||
direction: ltr;
|
||||
container-type: inline-size;
|
||||
grid-template-columns: auto 1fr;
|
||||
&:focus-visible, &:has(.diff-item-1:focus-visible) {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 2px;
|
||||
outline-offset: 1px;
|
||||
outline-color: var(--color-base-content);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 2px;
|
||||
outline-offset: 1px;
|
||||
outline-color: var(--color-base-content);
|
||||
.diff-resizer {
|
||||
min-width: 95cqi;
|
||||
max-width: 95cqi;
|
||||
}
|
||||
}
|
||||
&:has(.diff-item-1:focus-visible) {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 2px;
|
||||
outline-offset: 1px;
|
||||
.diff-resizer {
|
||||
min-width: 5cqi;
|
||||
max-width: 5cqi;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.diff-item-2 {
|
||||
&::after {
|
||||
height: 2.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@supports (-webkit-overflow-scrolling: touch) and (overflow: -webkit-paged-x) {
|
||||
&:focus {
|
||||
.diff-resizer {
|
||||
min-width: 5cqi;
|
||||
max-width: 5cqi;
|
||||
}
|
||||
}
|
||||
&:has(.diff-item-1:focus) {
|
||||
.diff-resizer {
|
||||
min-width: 95cqi;
|
||||
max-width: 95cqi;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tooltip {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
position: relative;
|
||||
@@ -726,6 +789,57 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.diff-item-2 {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
position: relative;
|
||||
grid-column-start: 1;
|
||||
grid-row: span 3 / span 3;
|
||||
grid-row-start: 1;
|
||||
&:after {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: calc(1 / 2 * 100%);
|
||||
right: 1px;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
border-radius: calc(infinity * 1px);
|
||||
background-color: var(--color-base-100);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-base-100) 98%, transparent);
|
||||
}
|
||||
width: 1.2rem;
|
||||
height: 1.8rem;
|
||||
border: 2px solid var(--color-base-100);
|
||||
content: "";
|
||||
box-shadow: 0 0 0 2px #0000002a;
|
||||
outline: 2px solid var(--color-base-content);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
outline: 2px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);
|
||||
}
|
||||
outline-offset: -3px;
|
||||
translate: 50% -50%;
|
||||
transition: height 0.3s linear(0, 0.931 13.8%, 1.196 21.4%, 1.343 29.8%, 1.378 36%, 1.365 43.2%, 1.059 78%, 1);
|
||||
}
|
||||
> * {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100cqi;
|
||||
max-width: none;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
@supports (-webkit-overflow-scrolling: touch) and (overflow: -webkit-paged-x) {
|
||||
&:after {
|
||||
--tw-content: none;
|
||||
content: var(--tw-content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.dropdown {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
position: relative;
|
||||
@@ -974,6 +1088,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.loading {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
pointer-events: none;
|
||||
display: inline-block;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-color: currentcolor;
|
||||
vertical-align: middle;
|
||||
width: calc(var(--size-selector, 0.25rem) * 6);
|
||||
mask-size: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='8s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='6s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='6s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
}
|
||||
.collapse {
|
||||
&:not(td, tr, colgroup) {
|
||||
visibility: revert-layer;
|
||||
@@ -1141,6 +1272,44 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.\!filter {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
[type="radio"] {
|
||||
width: auto !important;
|
||||
}
|
||||
input {
|
||||
overflow: hidden !important;
|
||||
opacity: 100% !important;
|
||||
scale: 1 !important;
|
||||
transition: visibility 0.1s allow-discrete, margin 0.1s, opacity 0.3s, padding 0.3s, border-width 0.1s !important;
|
||||
&.filter-reset {
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
&::after {
|
||||
--tw-content: "×" !important;
|
||||
content: var(--tw-content) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
> input:not(:last-child), > :not(:last-child) input {
|
||||
margin-inline-end: 0.25rem !important;
|
||||
}
|
||||
}
|
||||
@layer daisyui.l1 {
|
||||
&:not(:has(:checked:not(.filter-reset))) :is(.filter-reset, [type="reset"]):not(:focus-visible) {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
&:not(:has(:checked:not(.filter-reset))) :is(.filter-reset, [type="reset"]):not(:focus-visible), &:not(:has(:focus-visible)):has(:checked:not(.filter-reset, [type="checkbox"])) :is(input, button):not(:checked, .filter-reset, [type="reset"]) {
|
||||
margin-inline: 0 !important;
|
||||
width: 0 !important;
|
||||
padding-inline: 0 !important;
|
||||
opacity: 0% !important;
|
||||
scale: 0 !important;
|
||||
border-width: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: flex;
|
||||
@@ -1179,6 +1348,36 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.validator-hint {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
visibility: hidden;
|
||||
margin-top: calc(0.25rem * 2);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
.validator {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
&:user-valid, &:has(:user-valid) {
|
||||
&, &:focus, &:checked, &[aria-checked="true"], &:focus-within {
|
||||
--input-color: var(--color-success);
|
||||
}
|
||||
}
|
||||
&:user-invalid, &:has(:user-invalid), &[aria-invalid]:not([aria-invalid="false"]), &:has([aria-invalid]:not([aria-invalid="false"])) {
|
||||
&, &:focus, &:checked, &[aria-checked="true"], &:focus-within {
|
||||
--input-color: var(--color-error);
|
||||
}
|
||||
& ~ .validator-hint {
|
||||
visibility: visible;
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:user-invalid, &:has(:user-invalid), &[aria-invalid]:not([aria-invalid="false"]), &:has([aria-invalid]:not([aria-invalid="false"])) {
|
||||
& ~ .validator-hint {
|
||||
display: revert-layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
.collapse-open {
|
||||
@layer daisyui.l1.l2 {
|
||||
grid-template-rows: max-content 1fr;
|
||||
@@ -1550,6 +1749,105 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.range {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
appearance: none;
|
||||
webkit-appearance: none;
|
||||
--range-thumb: var(--color-base-100);
|
||||
--range-thumb-size: calc(var(--size-selector, 0.25rem) * 6);
|
||||
--range-progress: currentColor;
|
||||
--range-fill: 1;
|
||||
--range-p: 0.25rem;
|
||||
--range-bg: currentColor;
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
--range-bg: color-mix(in oklab, currentColor 10%, #0000);
|
||||
}
|
||||
--range-fill-x: calc(
|
||||
(var(--range-dir, 1) * -100cqw) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)
|
||||
);
|
||||
--range-fill-y: 0;
|
||||
--range-fill-spread: calc(100cqw * var(--range-fill));
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
vertical-align: middle;
|
||||
width: clamp(3rem, 20rem, 100%);
|
||||
--radius-selector-max: calc(
|
||||
var(--radius-selector) + var(--radius-selector) + var(--radius-selector)
|
||||
);
|
||||
border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));
|
||||
border: none;
|
||||
height: var(--range-thumb-size);
|
||||
[dir="rtl"] & {
|
||||
--range-dir: -1;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
&::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
background-color: var(--range-bg);
|
||||
border-radius: var(--radius-selector);
|
||||
height: calc(var(--range-thumb-size) * 0.5);
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
&::-webkit-slider-runnable-track {
|
||||
border: 1px solid;
|
||||
}
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
&::-moz-range-track {
|
||||
border: 1px solid;
|
||||
}
|
||||
}
|
||||
&::-webkit-slider-thumb {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));
|
||||
background-color: var(--range-thumb);
|
||||
height: var(--range-thumb-size);
|
||||
width: var(--range-thumb-size);
|
||||
border: var(--range-p) solid;
|
||||
appearance: none;
|
||||
webkit-appearance: none;
|
||||
inset-block-start: 50%;
|
||||
color: var(--range-progress);
|
||||
transform: translateY(-50%);
|
||||
box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, var(--range-fill-x) var(--range-fill-y) 0 var(--range-fill-spread);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, var(--range-fill-x) var(--range-fill-y) 0 var(--range-fill-spread);
|
||||
}
|
||||
}
|
||||
&::-moz-range-track {
|
||||
width: 100%;
|
||||
background-color: var(--range-bg);
|
||||
border-radius: var(--radius-selector);
|
||||
height: calc(var(--range-thumb-size) * 0.5);
|
||||
}
|
||||
&::-moz-range-thumb {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));
|
||||
background-color: currentColor;
|
||||
height: var(--range-thumb-size);
|
||||
width: var(--range-thumb-size);
|
||||
border: var(--range-p) solid;
|
||||
color: var(--range-progress);
|
||||
box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, var(--range-fill-x) var(--range-fill-y) 0 var(--range-fill-spread);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, var(--range-fill-x) var(--range-fill-y) 0 var(--range-fill-spread);
|
||||
}
|
||||
}
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.indicator {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
position: relative;
|
||||
@@ -1791,6 +2089,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.diff-resizer {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
z-index: 2;
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 2;
|
||||
height: calc(0.25rem * 3);
|
||||
width: 50cqi;
|
||||
max-width: calc(100cqi - 1rem);
|
||||
min-width: 1rem;
|
||||
resize: horizontal;
|
||||
overflow: hidden;
|
||||
opacity: 0%;
|
||||
transform: scaleY(5) translate(0.32rem, 50%);
|
||||
cursor: ew-resize;
|
||||
transform-origin: 100% 100%;
|
||||
clip-path: inset(calc(100% - 0.75rem) 0 0 calc(100% - 0.75rem));
|
||||
transition: min-width 0.3s ease-out, max-width 0.3s ease-out;
|
||||
}
|
||||
}
|
||||
.select {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
position: relative;
|
||||
@@ -1977,6 +2296,42 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.swap {
|
||||
@layer daisyui.l1.l2 {
|
||||
position: relative;
|
||||
display: inline-grid;
|
||||
cursor: pointer;
|
||||
place-content: center;
|
||||
vertical-align: middle;
|
||||
webkit-user-select: none;
|
||||
user-select: none;
|
||||
input {
|
||||
appearance: none;
|
||||
border: none;
|
||||
}
|
||||
> * {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition-property: transform, rotate, opacity;
|
||||
transition-duration: 0.2s;
|
||||
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
.swap-on, .swap-indeterminate, input:indeterminate ~ .swap-on {
|
||||
opacity: 0%;
|
||||
}
|
||||
input:is(:checked, :indeterminate) {
|
||||
& ~ .swap-off {
|
||||
opacity: 0%;
|
||||
}
|
||||
}
|
||||
input:checked ~ .swap-on, input:indeterminate ~ .swap-indeterminate {
|
||||
opacity: 100%;
|
||||
backface-visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
.collapse-title {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
grid-column-start: 1;
|
||||
@@ -2333,12 +2688,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
.sticky {
|
||||
position: sticky;
|
||||
}
|
||||
.dropdown-right {
|
||||
@layer daisyui.l1.l2 {
|
||||
--anchor-h: right;
|
||||
@@ -2873,6 +3237,62 @@
|
||||
.my-6 {
|
||||
margin-block: calc(var(--spacing) * 6);
|
||||
}
|
||||
.breadcrumbs {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
padding-block: calc(0.25rem * 2);
|
||||
> menu, > ul, > ol {
|
||||
display: flex;
|
||||
min-height: min-content;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
> li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
> * {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
gap: calc(0.25rem * 2);
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
@media (forced-colors: active) {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
& + *:before {
|
||||
content: "";
|
||||
margin-inline-start: calc(0.25rem * 2);
|
||||
margin-inline-end: calc(0.25rem * 3);
|
||||
display: block;
|
||||
height: calc(0.25rem * 1.5);
|
||||
width: calc(0.25rem * 1.5);
|
||||
opacity: 40%;
|
||||
rotate: 45deg;
|
||||
border-top: 1px solid;
|
||||
border-right: 1px solid;
|
||||
background-color: #0000;
|
||||
}
|
||||
[dir="rtl"] & + *:before {
|
||||
rotate: -135deg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.label {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: inline-flex;
|
||||
@@ -3101,6 +3521,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.fieldset {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: grid;
|
||||
gap: calc(0.25rem * 1.5);
|
||||
padding-block: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-rows: max-content;
|
||||
}
|
||||
}
|
||||
.card-actions {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: flex;
|
||||
@@ -3183,9 +3613,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.\!hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
.contents {
|
||||
display: contents;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -3210,6 +3646,12 @@
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
.table-caption {
|
||||
display: table-caption;
|
||||
}
|
||||
.table-cell {
|
||||
display: table-cell;
|
||||
}
|
||||
.h-16 {
|
||||
height: calc(var(--spacing) * 16);
|
||||
}
|
||||
@@ -3708,6 +4150,12 @@
|
||||
.text-warning {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
.lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tabular-nums {
|
||||
--tw-numeric-spacing: tabular-nums;
|
||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
||||
@@ -3755,6 +4203,17 @@
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
.blur {
|
||||
--tw-blur: blur(8px);
|
||||
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,);
|
||||
}
|
||||
.invert {
|
||||
--tw-invert: invert(100%);
|
||||
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 {
|
||||
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,) !important;
|
||||
}
|
||||
.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,);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user