Skip to content

Commit fcfb82e

Browse files
committed
Compile Tailwind in a Docker build stage instead of loading the CDN
Drop the Tailwind play CDN and its in-browser JIT in favour of a real build: a dedicated tailwind stage in the Dockerfile downloads the official standalone binary (no Node), scans templates and the input.css source, and emits a minified tailwind.css that the runtime image picks up via COPY --from=tailwind. The compiled CSS lands at finance/static/finance/ where collectstatic processes it as any other static asset. The base template now loads the compiled tailwind.css; the component classes that lived in the inline <style type="text/tailwindcss"> block move into tailwind/input.css. The compiled output is gitignored as a build artefact. No more production-mode warning in the console, no more in-browser JIT cost on first render.
1 parent 9c8ef6c commit fcfb82e

5 files changed

Lines changed: 136 additions & 78 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ celerybeat.pid
130130
# Private finance seed rules (real account/vendor match strings)
131131
seed_rules.json
132132

133+
# Compiled Tailwind CSS (built by the Dockerfile's tailwind stage)
134+
finance/static/finance/tailwind.css
135+
133136
# Environments
134137
.env
135138
.env.*

Dockerfile

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
# Stage 1: compile Tailwind CSS with the standalone binary
2+
# (no Node in the final image; the binary is a self-contained executable)
3+
FROM debian:bookworm-slim AS tailwind
4+
5+
ARG TAILWIND_VERSION=v3.4.17
6+
7+
RUN apt-get update \
8+
&& apt-get install -y --no-install-recommends curl ca-certificates \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
WORKDIR /build
12+
13+
# Pick the right binary for the build architecture (amd64 or arm64)
14+
RUN ARCH=$(dpkg --print-architecture) \
15+
&& case "$ARCH" in \
16+
amd64) PLATFORM=linux-x64;; \
17+
arm64) PLATFORM=linux-arm64;; \
18+
*) echo "Unsupported architecture: $ARCH" >&2; exit 1;; \
19+
esac \
20+
&& curl -fsSL -o /usr/local/bin/tailwindcss \
21+
"https://github.com/tailwindlabs/tailwindcss/releases/download/${TAILWIND_VERSION}/tailwindcss-${PLATFORM}" \
22+
&& chmod +x /usr/local/bin/tailwindcss
23+
24+
COPY tailwind.config.js ./
25+
COPY tailwind/input.css ./input.css
26+
# The config scans the template tree, so the templates are part of the build input
27+
COPY finance/templates ./finance/templates
28+
COPY finance/static/finance ./finance/static/finance
29+
30+
RUN tailwindcss -c tailwind.config.js -i input.css -o tailwind.css --minify
31+
32+
# Stage 2: the runtime image
133
FROM python:3.14-slim
234

335
ENV PYTHONUNBUFFERED=1 \
@@ -16,6 +48,9 @@ RUN pip install -r requirements.txt
1648

1749
COPY . .
1850

51+
# Drop in the compiled Tailwind CSS from the build stage
52+
COPY --from=tailwind /build/tailwind.css /app/finance/static/finance/tailwind.css
53+
1954
COPY entrypoint.sh /entrypoint.sh
2055
RUN chmod +x /entrypoint.sh
2156

finance/templates/finance/base.html

Lines changed: 1 addition & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -10,84 +10,7 @@
1010
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1111
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
1212

13-
<script src="https://cdn.tailwindcss.com"></script>
14-
<script>
15-
tailwind.config = {
16-
theme: {
17-
extend: {
18-
fontFamily: {
19-
sans: ['Inter', 'system-ui', '-apple-system', 'Segoe UI', 'Roboto', 'sans-serif'],
20-
},
21-
},
22-
},
23-
};
24-
</script>
25-
<style type="text/tailwindcss">
26-
@layer base {
27-
body {
28-
background:
29-
radial-gradient(900px 600px at 100% -10%, rgba(99,102,241,0.07), transparent 60%),
30-
radial-gradient(800px 500px at -10% 110%, rgba(16,185,129,0.06), transparent 60%),
31-
theme(colors.slate.50);
32-
}
33-
}
34-
@layer components {
35-
.nav-link {
36-
@apply px-3 py-2 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 whitespace-nowrap transition;
37-
}
38-
.nav-link.is-active { @apply bg-indigo-50 text-indigo-700; }
39-
40-
.kpi-card {
41-
@apply bg-white border border-slate-200 rounded-xl px-5 py-4 transition hover:shadow-md hover:-translate-y-px;
42-
}
43-
.kpi-label { @apply text-xs font-semibold uppercase tracking-wider text-slate-500; }
44-
.kpi-value { @apply text-2xl font-bold tracking-tight tabular-nums mt-1 text-slate-900; }
45-
.kpi-sub { @apply text-xs text-slate-400 mt-1; }
46-
47-
.panel { @apply bg-white border border-slate-200 rounded-xl p-6 transition hover:shadow-md; }
48-
.panel h2 { @apply text-base font-semibold tracking-tight mb-4 text-slate-900; }
49-
50-
.chart-wrap { @apply relative; height: 320px; }
51-
.chart-wrap.tall { height: 440px; }
52-
53-
.table-data { @apply w-full border-collapse text-sm; }
54-
.table-data th {
55-
@apply text-left py-2 px-3 border-b border-slate-200 text-[0.7rem] font-semibold uppercase tracking-wider text-slate-500;
56-
}
57-
.table-data td {
58-
@apply py-3 px-3 border-b border-slate-100 align-top text-slate-900;
59-
}
60-
.table-data tbody tr:last-child td { @apply border-b-0; }
61-
.table-data tbody tr:hover td { @apply bg-slate-50; }
62-
.table-data .num { @apply text-right tabular-nums; }
63-
64-
.tag { @apply inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold; }
65-
.tag.income { @apply bg-emerald-50 text-emerald-700; }
66-
.tag.expense { @apply bg-rose-50 text-rose-700; }
67-
.tag.transfer { @apply bg-slate-200 text-slate-600; }
68-
.tag.investment { @apply bg-violet-50 text-violet-700; }
69-
.tag.tax { @apply bg-amber-50 text-amber-700; }
70-
.tag.other { @apply bg-indigo-50 text-indigo-700; }
71-
.tag.personal { @apply bg-sky-50 text-sky-700; }
72-
.tag.business { @apply bg-fuchsia-50 text-fuchsia-700; }
73-
74-
.filter-field { @apply text-xs font-medium text-slate-500 flex flex-col gap-1; }
75-
.filter-input,
76-
.filter-field input,
77-
.filter-field select {
78-
@apply px-3 py-1.5 border border-slate-200 rounded-md bg-white text-sm text-slate-900 transition focus:outline-none focus:ring-2 focus:ring-indigo-100 focus:border-indigo-500;
79-
}
80-
81-
.btn-primary {
82-
@apply px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-semibold transition hover:bg-indigo-500 active:scale-[0.98] shadow-md shadow-indigo-500/25;
83-
}
84-
.btn-ghost {
85-
@apply px-3 py-1.5 bg-white text-slate-700 border border-slate-200 rounded-md text-sm font-medium transition hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed;
86-
}
87-
88-
.empty { @apply text-center text-slate-400 py-10 text-sm; }
89-
}
90-
</style>
13+
<link rel="stylesheet" href="{% static 'finance/tailwind.css' %}">
9114

9215
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js" defer></script>
9316
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js" defer></script>

tailwind.config.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Tailwind CSS configuration for the FinancialAudit dashboards.
3+
*
4+
* The content globs tell Tailwind which files to scan for class names so the
5+
* generated CSS only contains utilities that are actually used.
6+
*/
7+
module.exports = {
8+
content: [
9+
"./finance/templates/**/*.html",
10+
"./finance/static/finance/**/*.js",
11+
],
12+
theme: {
13+
extend: {
14+
fontFamily: {
15+
sans: ["Inter", "system-ui", "-apple-system", "Segoe UI", "Roboto", "sans-serif"],
16+
},
17+
},
18+
},
19+
plugins: [],
20+
};

tailwind/input.css

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Author: xhico
3+
* Date: May 27, 2026
4+
* Tailwind source — compiled by the Docker tailwind stage into the static
5+
* directory as tailwind.css. Components defined here with @apply replace the
6+
* old hand-written stylesheet.
7+
*/
8+
9+
@tailwind base;
10+
@tailwind components;
11+
@tailwind utilities;
12+
13+
@layer base {
14+
body {
15+
background:
16+
radial-gradient(900px 600px at 100% -10%, rgba(99, 102, 241, 0.07), transparent 60%),
17+
radial-gradient(800px 500px at -10% 110%, rgba(16, 185, 129, 0.06), transparent 60%),
18+
theme(colors.slate.50);
19+
}
20+
}
21+
22+
@layer components {
23+
.nav-link {
24+
@apply px-3 py-2 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 whitespace-nowrap transition;
25+
}
26+
.nav-link.is-active { @apply bg-indigo-50 text-indigo-700; }
27+
28+
.kpi-card {
29+
@apply bg-white border border-slate-200 rounded-xl px-5 py-4 transition hover:shadow-md hover:-translate-y-px;
30+
}
31+
.kpi-label { @apply text-xs font-semibold uppercase tracking-wider text-slate-500; }
32+
.kpi-value { @apply text-2xl font-bold tracking-tight tabular-nums mt-1 text-slate-900; }
33+
.kpi-sub { @apply text-xs text-slate-400 mt-1; }
34+
35+
.panel { @apply bg-white border border-slate-200 rounded-xl p-6 transition hover:shadow-md; }
36+
.panel h2 { @apply text-base font-semibold tracking-tight mb-4 text-slate-900; }
37+
38+
.chart-wrap { @apply relative; height: 320px; }
39+
.chart-wrap.tall { height: 440px; }
40+
41+
.table-data { @apply w-full border-collapse text-sm; }
42+
.table-data th {
43+
@apply text-left py-2 px-3 border-b border-slate-200 text-[0.7rem] font-semibold uppercase tracking-wider text-slate-500;
44+
}
45+
.table-data td {
46+
@apply py-3 px-3 border-b border-slate-100 align-top text-slate-900;
47+
}
48+
.table-data tbody tr:last-child td { @apply border-b-0; }
49+
.table-data tbody tr:hover td { @apply bg-slate-50; }
50+
.table-data .num { @apply text-right tabular-nums; }
51+
52+
.tag { @apply inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold; }
53+
.tag.income { @apply bg-emerald-50 text-emerald-700; }
54+
.tag.expense { @apply bg-rose-50 text-rose-700; }
55+
.tag.transfer { @apply bg-slate-200 text-slate-600; }
56+
.tag.investment { @apply bg-violet-50 text-violet-700; }
57+
.tag.tax { @apply bg-amber-50 text-amber-700; }
58+
.tag.other { @apply bg-indigo-50 text-indigo-700; }
59+
.tag.personal { @apply bg-sky-50 text-sky-700; }
60+
.tag.business { @apply bg-fuchsia-50 text-fuchsia-700; }
61+
62+
.filter-field { @apply text-xs font-medium text-slate-500 flex flex-col gap-1; }
63+
.filter-input,
64+
.filter-field input,
65+
.filter-field select {
66+
@apply px-3 py-1.5 border border-slate-200 rounded-md bg-white text-sm text-slate-900 transition focus:outline-none focus:ring-2 focus:ring-indigo-100 focus:border-indigo-500;
67+
}
68+
69+
.btn-primary {
70+
@apply px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-semibold transition hover:bg-indigo-500 active:scale-[0.98] shadow-md shadow-indigo-500/25;
71+
}
72+
.btn-ghost {
73+
@apply px-3 py-1.5 bg-white text-slate-700 border border-slate-200 rounded-md text-sm font-medium transition hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed;
74+
}
75+
76+
.empty { @apply text-center text-slate-400 py-10 text-sm; }
77+
}

0 commit comments

Comments
 (0)