mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
build(web): generate the UI's Tailwind CSS with the pinned standalone CLI (#685)
Replaces the hand-written Tailwind subset in app.css with a real, purged Tailwind build: scripts/build_css.py runs the pinned, SHA-256-checked standalone Tailwind CLI (no Node), the generated tailwind.css and plugin-frame.css are committed, and CI fails when they are stale. The Pi never builds anything. The login page (#683) now links tailwind.css too, and the load-order test covers every template that links app.css. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Input for static/v3/tailwind.css. Don't edit the output; edit this file or
|
||||
* tailwind.config.js and run `python3 scripts/build_css.py`.
|
||||
*
|
||||
* Custom CSS belongs in static/v3/app.css, which loads after this file, so
|
||||
* its rules win over utilities of equal specificity.
|
||||
*/
|
||||
|
||||
@tailwind base;
|
||||
|
||||
@layer base {
|
||||
/* The one piece of Tailwind's preflight the utilities depend on: border
|
||||
utilities set only a width (`border`, `border-t`, `divide-y`), so every
|
||||
element starts from zero width and a solid style. app.css sets the
|
||||
default border colour. */
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/* The reset above would erase these; keep them visible. */
|
||||
hr {
|
||||
height: 0;
|
||||
color: inherit;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
:where(
|
||||
input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"], [type="hidden"]),
|
||||
select,
|
||||
textarea
|
||||
) {
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Tailwind config for static/v3/plugin-frame.css: the stylesheet
|
||||
* /v3/plugin-ui/<plugin>/web-ui/<file> links for a plugin's web_ui/ fragment
|
||||
* (shown in an iframe on the plugin's config tab).
|
||||
*
|
||||
* Fragments used to load Tailwind v2.2.19 from a CDN, which fails when the Pi
|
||||
* is in AP mode with no internet. Their markup lives in plugin repos and
|
||||
* can't be scanned here, so instead of content globs this file safelists the
|
||||
* common utility families at the values the v2 CDN had. A fragment that
|
||||
* needs something rarer should ship it in its own <style>.
|
||||
*
|
||||
* Patterns are regex literals, so the colour families (gray red yellow green
|
||||
* blue indigo purple pink) and the spacing scale are spelled out in each
|
||||
* pattern that uses them; change every copy together.
|
||||
*
|
||||
* Rebuild with `python3 scripts/build_css.py` after changing this file.
|
||||
*/
|
||||
const colors = require('tailwindcss/colors');
|
||||
|
||||
module.exports = {
|
||||
content: {
|
||||
relative: true,
|
||||
// The route's own wrapper markup; everything else is safelisted.
|
||||
files: ['../blueprints/pages_v3.py'],
|
||||
},
|
||||
safelist: [
|
||||
// Colour
|
||||
{
|
||||
pattern: /^(bg|text|border)-(gray|red|yellow|green|blue|indigo|purple|pink)-(50|100|200|300|400|500|600|700|800|900)$/,
|
||||
variants: ['hover'],
|
||||
},
|
||||
{ pattern: /^(bg|text|border)-(white|black|transparent|current)$/, variants: ['hover'] },
|
||||
{ pattern: /^(ring|placeholder)-(gray|red|yellow|green|blue|indigo|purple|pink)-(300|400|500|600)$/, variants: ['focus'] },
|
||||
{ pattern: /^border-(gray|red|yellow|green|blue|indigo|purple|pink)-(300|400|500|600)$/, variants: ['focus'] },
|
||||
{ pattern: /^bg-opacity-(0|25|50|75|100)$/ },
|
||||
{ pattern: /^opacity-(0|25|50|75|100)$/, variants: ['hover', 'disabled'] },
|
||||
|
||||
// Spacing and sizing
|
||||
{ pattern: /^(p|px|py|pt|pr|pb|pl|m|mx|my|mt|mr|mb|ml)-(0|0\.5|1|1\.5|2|2\.5|3|4|5|6|8|10|12|16|20|24)$/ },
|
||||
{ pattern: /^(m|mx|my|mt|mr|mb|ml)-auto$/ },
|
||||
{ pattern: /^(space-x|space-y|gap|gap-x|gap-y)-(0|0\.5|1|1\.5|2|2\.5|3|4|5|6|8|10|12|16|20|24)$/ },
|
||||
{ pattern: /^(w|h)-(0|1|2|3|4|5|6|8|10|12|16|20|24|32|40|48|56|64|72|80|96|auto|full|screen|px|1\/2|1\/3|2\/3|1\/4|3\/4)$/ },
|
||||
{ pattern: /^min-(w|h)-(0|full|screen)$/ },
|
||||
{ pattern: /^max-w-(xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|full|screen-sm|screen-md|screen-lg|none)$/ },
|
||||
{ pattern: /^max-h-(32|48|64|96|full|screen)$/ },
|
||||
|
||||
// Layout
|
||||
{ pattern: /^(block|inline-block|inline|flex|inline-flex|grid|inline-grid|table|table-row|table-cell|hidden|contents)$/, variants: ['sm', 'md', 'lg'] },
|
||||
{ pattern: /^flex-(1|auto|initial|none|row|row-reverse|col|col-reverse|wrap|nowrap|grow|shrink|grow-0|shrink-0)$/, variants: ['sm', 'md', 'lg'] },
|
||||
{ pattern: /^(grow|shrink|grow-0|shrink-0)$/ },
|
||||
{ pattern: /^(items|content)-(start|end|center|baseline|stretch|between)$/ },
|
||||
{ pattern: /^(justify|self)-(start|end|center|between|around|evenly|auto|stretch)$/ },
|
||||
{ pattern: /^grid-cols-(1|2|3|4|5|6|12)$/, variants: ['sm', 'md', 'lg'] },
|
||||
{ pattern: /^col-span-(1|2|3|4|5|6|12|full)$/, variants: ['sm', 'md', 'lg'] },
|
||||
{ pattern: /^(static|relative|absolute|fixed|sticky)$/ },
|
||||
{ pattern: /^(inset|top|right|bottom|left)-(0|1|2|3|4|auto|full)$/ },
|
||||
{ pattern: /^inset-(x|y)-0$/ },
|
||||
{ pattern: /^z-(0|10|20|30|40|50|auto)$/ },
|
||||
{ pattern: /^(overflow|overflow-x|overflow-y)-(auto|hidden|visible|scroll)$/ },
|
||||
{ pattern: /^(float-left|float-right|clear-both|mx-auto|container|sr-only|not-sr-only)$/ },
|
||||
{ pattern: /^object-(contain|cover|center)$/ },
|
||||
|
||||
// Typography
|
||||
{ pattern: /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl)$/, variants: ['sm', 'md'] },
|
||||
{ pattern: /^text-(left|center|right|justify)$/ },
|
||||
{ pattern: /^font-(sans|serif|mono|light|normal|medium|semibold|bold|extrabold)$/ },
|
||||
{ pattern: /^(uppercase|lowercase|capitalize|normal-case|italic|not-italic|underline|line-through|no-underline|truncate|break-words|break-all)$/, variants: ['hover'] },
|
||||
{ pattern: /^whitespace-(normal|nowrap|pre|pre-line|pre-wrap)$/ },
|
||||
{ pattern: /^leading-(none|tight|snug|normal|relaxed|loose|4|5|6|7|8)$/ },
|
||||
{ pattern: /^tracking-(tighter|tight|normal|wide|wider|widest)$/ },
|
||||
{ pattern: /^(list-none|list-disc|list-decimal|list-inside|list-outside)$/ },
|
||||
{ pattern: /^align-(top|middle|bottom|baseline)$/ },
|
||||
|
||||
// Borders, effects, interaction
|
||||
{ pattern: /^border(-0|-2|-4|-t|-b|-l|-r|-t-0|-b-0|-t-2|-b-2|-l-4)?$/ },
|
||||
{ pattern: /^border-(solid|dashed|dotted|none)$/ },
|
||||
{ pattern: /^divide-(x|y)$/ },
|
||||
{ pattern: /^divide-(gray|red|yellow|green|blue|indigo|purple|pink)-(100|200|300)$/ },
|
||||
{ pattern: /^rounded(-none|-sm|-md|-lg|-xl|-2xl|-full)?$/ },
|
||||
{ pattern: /^rounded-(t|b|l|r)(-md|-lg)?$/ },
|
||||
{ pattern: /^shadow(-sm|-md|-lg|-xl|-2xl|-inner|-none)?$/, variants: ['hover'] },
|
||||
{ pattern: /^(ring|ring-0|ring-1|ring-2|ring-4|ring-offset-2|outline-none)$/, variants: ['focus'] },
|
||||
{ pattern: /^(transition|transition-colors|transition-opacity|transition-all|transform)$/ },
|
||||
{ pattern: /^(duration|ease)-(150|200|300|in|out|in-out)$/ },
|
||||
{ pattern: /^(animate-spin|animate-pulse)$/ },
|
||||
{ pattern: /^cursor-(pointer|default|not-allowed|move|wait)$/ },
|
||||
{ pattern: /^(select-none|select-all|pointer-events-none|pointer-events-auto|resize|resize-none|resize-y|appearance-none)$/ },
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
// Tailwind v2's default palette, which fragments were written against:
|
||||
// its green, yellow and purple were v3's emerald, amber and violet.
|
||||
colors: {
|
||||
green: colors.emerald,
|
||||
yellow: colors.amber,
|
||||
purple: colors.violet,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Input for static/v3/plugin-frame.css, the stylesheet for plugin web_ui/
|
||||
* fragments shown in an iframe. Don't edit the output; edit this file or
|
||||
* plugin-frame.config.js and run `python3 scripts/build_css.py`.
|
||||
*
|
||||
* Includes Tailwind's preflight, as the v2 CDN build the fragments were
|
||||
* written against did.
|
||||
*/
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Tailwind config for the web UI (static/v3/tailwind.css).
|
||||
*
|
||||
* Built by scripts/build_css.py with the pinned standalone CLI; the output is
|
||||
* committed and CI fails if it is stale. After changing a template, a static
|
||||
* JS file or this config, run `python3 scripts/build_css.py` and commit
|
||||
* static/v3/tailwind.css with your change.
|
||||
*
|
||||
* tailwind.css holds the utilities; app.css (loaded after it) holds the
|
||||
* theme tokens, base element styles, components and dark-theme overrides.
|
||||
* The theme below keeps the values app.css used to hand-write, so moving to
|
||||
* a real build restyled nothing that already worked.
|
||||
*/
|
||||
const colors = require('tailwindcss/colors');
|
||||
|
||||
module.exports = {
|
||||
content: {
|
||||
relative: true,
|
||||
files: [
|
||||
'../templates/v3/**/*.html',
|
||||
'../static/v3/*.js',
|
||||
'../static/v3/js/**/*.js',
|
||||
'!../static/v3/js/*.min.js',
|
||||
'../blueprints/**/*.py',
|
||||
],
|
||||
},
|
||||
|
||||
// Class names assembled at runtime, which the scanner can't see.
|
||||
safelist: [
|
||||
// tools.html showResult() and the power-status badge build
|
||||
// `bg-${color}-50`, `text-${color}-800`, ... from green/red/yellow.
|
||||
{ pattern: /^bg-(green|red|yellow)-(50|100)$/ },
|
||||
{ pattern: /^border-(green|red|yellow)-200$/ },
|
||||
{ pattern: /^text-(green|red|yellow)-(600|700|800)$/ },
|
||||
// Hand-written in app.css before this build and unused by core since.
|
||||
// Third-party plugin widgets render into the page and may use them, so
|
||||
// the generated set stays a superset of what used to work.
|
||||
'grid-cols-3', 'grid-cols-4', 'gap-x-3', 'gap-x-4', 'hover:border-gray-300',
|
||||
'sm:hidden', 'sm:inline', 'sm:max-w-4xl', 'md:flex',
|
||||
'lg:grid-cols-5', 'lg:grid-cols-6', 'lg:gap-x-6',
|
||||
'xl:grid-cols-5', 'xl:grid-cols-6', 'xl:grid-cols-7', 'xl:grid-cols-8', 'xl:gap-x-8',
|
||||
],
|
||||
|
||||
// Rules app.css defines on purpose and Tailwind must not override. Tailwind
|
||||
// would show these on every focus; app.css shows them for keyboard focus
|
||||
// only (:focus-visible), so a mouse click doesn't flash a ring.
|
||||
blocklist: [
|
||||
'focus:outline-none',
|
||||
'peer-focus:outline-none',
|
||||
'peer-focus:ring-4',
|
||||
'peer-focus:ring-blue-300',
|
||||
],
|
||||
|
||||
// The app switches themes with <html data-theme="dark">, not a class.
|
||||
darkMode: ['selector', '[data-theme="dark"]'],
|
||||
|
||||
// app.css owns the base styles; preflight would reset headings, lists and
|
||||
// form controls the UI already styles. The border reset utilities rely on
|
||||
// is in app.input.css.
|
||||
corePlugins: { preflight: false },
|
||||
|
||||
theme: {
|
||||
extend: {
|
||||
// Light-mode gray text is one step darker than stock Tailwind for
|
||||
// contrast (text-gray-400 reads as gray-500, and so on).
|
||||
textColor: {
|
||||
gray: {
|
||||
...colors.gray,
|
||||
400: colors.gray[500],
|
||||
500: colors.gray[600],
|
||||
600: colors.gray[700],
|
||||
},
|
||||
green: { ...colors.green, 600: colors.emerald[600] },
|
||||
yellow: { ...colors.yellow, 300: colors.amber[300] },
|
||||
},
|
||||
// Solid green/yellow buttons and dots use the deeper emerald/amber
|
||||
// shades, which keep white text readable.
|
||||
backgroundColor: {
|
||||
green: {
|
||||
...colors.green,
|
||||
500: colors.emerald[500],
|
||||
600: colors.emerald[600],
|
||||
700: colors.emerald[700],
|
||||
},
|
||||
yellow: {
|
||||
...colors.yellow,
|
||||
500: colors.amber[500],
|
||||
600: colors.amber[600],
|
||||
700: colors.amber[700],
|
||||
},
|
||||
},
|
||||
fontSize: {
|
||||
xs: ['0.75rem', '1.4'],
|
||||
sm: ['0.875rem', '1.5'],
|
||||
base: ['1rem', '1.5'],
|
||||
md: ['1rem', '1.5rem'],
|
||||
lg: ['1.125rem', '1.75'],
|
||||
xl: ['1.25rem', '1.75'],
|
||||
'2xl': ['1.5rem', '2'],
|
||||
'4xl': ['2.25rem', '2.5'],
|
||||
},
|
||||
// Shadows follow the theme tokens in app.css (:root / [data-theme]).
|
||||
boxShadow: {
|
||||
sm: 'var(--shadow-sm)',
|
||||
DEFAULT: 'var(--shadow)',
|
||||
md: 'var(--shadow-md)',
|
||||
lg: 'var(--shadow-lg)',
|
||||
},
|
||||
// `transition` animates only compositor-friendly properties.
|
||||
transitionProperty: {
|
||||
DEFAULT: 'transform, opacity, color, border-color',
|
||||
},
|
||||
// The gap a focus ring leaves matches the surface, in both themes.
|
||||
ringOffsetColor: {
|
||||
DEFAULT: 'var(--color-surface, #fff)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user