mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
854973a9f1
* fix(tailwind): make tailwind.config.js loadable by a plain CommonJS require
`tailwind.config.js` is a CommonJS module, but it `require()`d two TypeScript
sources:
require('./src/utils/tailwind') -> src/utils/tailwind.ts
require('./src/tailwind/container-queries') -> src/tailwind/container-queries.ts
Node's CJS resolver does not resolve `.ts`, so a plain `require()` of the config
threw `Cannot find module './src/utils/tailwind'`.
This was latent rather than user-visible: tailwind v3's `loadConfig()` routes the
config through its bundled jiti, and jiti *does* resolve `.ts`, so Next and the
PostCSS pipeline kept loading the real theme. (Verified: building CSS through the
repo's own postcss.config.js resolves `theme('colors.blue.8')` and
`theme('screens.xs')` correctly both before and after this change.)
The problem is that the project theme silently depended on being loaded by a
TypeScript-aware loader. Any consumer that plain-`require()`s the config — a
script, a lint rule, a tool that inlines it — and any tailwind version that stops
routing through jiti gets tailwind's *stock* theme instead of this project's.
Stock tailwind has no `xs` screen and no numeric colour scale, while ~80
stylesheets use `theme('screens.…')` / `theme('colors.<name>.<digit>')`.
Fix, keeping `breakpoints` single-sourced:
- Move the breakpoints literal to `src/utils/breakpoints.json`. JSON is the one
format Node's CJS resolver, jiti, webpack and Vite all load natively, so both
module systems can read the same file.
- `src/utils/tailwind.ts` re-exports it, so the two importers
(`ContainerGrid.tsx`, `ImageCarousel.tsx`) keep their import path unchanged.
`resolveJsonModule` keeps the inferred type byte-identical to the old inline
literal — verified with a type-equality probe that passes against both the old
and the new module, and fails when the expected type is perturbed.
- Convert `src/tailwind/container-queries.ts` to CommonJS `.js` (types preserved
as JSDoc). It is loaded only by the tailwind config, so it has no ESM consumers.
Adds `src/utils/tailwind.test.ts`, which loads the config through `createRequire`
— a genuine Node CJS require, no jiti, no TS loader — and asserts the project
screens resolve. Red before this change with the original
`Cannot find module './src/utils/tailwind'`; green after.
* fix(tailwind): keep container-queries.js inside tsc, and tighten the plugin assertion
Both findings from the adversarial audit of this PR.
1. 🔴 The .ts -> .js conversion silently dropped this file out of the type system.
tsconfig.json:10 sets `allowJs: true` but leaves `checkJs` UNSET, so a .js file
under src/ is compiled by tsc and type-checked by nothing — the JSDoc @typedef
/@param annotations added in the previous commit were pure decoration. Adding
`// @ts-check` restores checking for this one file without touching the global
config.
Verified by mutation in BOTH directions, not asserted:
- with `// @ts-check`, no mutation ....... 13 errors (baseline, file clean)
- with `// @ts-check`, `(42).toUpperCase()` injected
....... 14 errors, and the new one is
src/tailwind/container-queries.js(20,31)
TS2339 — this file, this reason
- WITHOUT `// @ts-check`, same mutation ... 13 errors, ZERO in this file
(i.e. genuinely invisible)
Every run carried a negative control: the known pre-existing
packages/civitai-db-schema/.../updated-at-tables.ts(7,38) TS2769 was present in
all three, proving tsc actually ran. That control matters here — tsc OOM-crashes
on this repo at the default heap and then reports zero `error TS` lines, which is
indistinguishable from clean. All runs used --max_old_space_size=8192.
2. The `eslint-disable-next-line @typescript-eslint/no-var-requires` was INERT:
.eslintignore line 1 is `*.js`, so this file is not linted at all and the
suppression documented a rule that never runs. Replaced with a note recording
that fact and what to do if `*.js` is ever removed from .eslintignore.
3. `expect(config.plugins.length).toBeGreaterThanOrEqual(2)` -> `toBe(2)`. The
loose bound still passed if a plugin were dropped, which is exactly the
regression the test exists to catch. plugins.length is exactly 2.
src/utils/tailwind.test.ts: 4/4 still pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
285 lines
7.8 KiB
JavaScript
285 lines
7.8 KiB
JavaScript
const plugin = require('tailwindcss/plugin');
|
|
const colors = require('tailwindcss/colors');
|
|
// Shared with app code via `src/utils/tailwind.ts`, which re-exports this same JSON.
|
|
// It must stay a format Node's CommonJS resolver can load on its own: this file is a
|
|
// CJS module, and a plain `require()` of it (a script, a lint rule, a bundler that
|
|
// inlines the config) gets Node's resolver, which cannot resolve `.ts`.
|
|
const breakpoints = require('./src/utils/breakpoints.json');
|
|
|
|
const fontFamilies = [
|
|
'"-apple-system"',
|
|
'"BlinkMacSystemFont"',
|
|
'"Segoe UI"',
|
|
'"Roboto"',
|
|
'"Helvetica"',
|
|
'"Arial"',
|
|
'"sans-serif"',
|
|
'"Apple Color Emoji"',
|
|
'"Segoe UI Emoji"',
|
|
]
|
|
|
|
/** @type {import('tailwindcss').Config} */
|
|
module.exports = {
|
|
content: ['./src/**/*.{ts,tsx}'],
|
|
darkMode: ['selector', '[data-mantine-color-scheme="dark"]'],
|
|
experimental: {
|
|
optimizeUniversalDefaults: true,
|
|
},
|
|
theme: {
|
|
fontFamily: {
|
|
body: fontFamilies
|
|
},
|
|
screens: breakpoints,
|
|
extend: {
|
|
textShadow: {
|
|
sm: '0 1px 2px var(--tw-shadow-color)',
|
|
default: '0 2px 4px var(--tw-shadow-color)',
|
|
},
|
|
// for container queries
|
|
containers: breakpoints,
|
|
width: breakpoints,
|
|
maxWidth: breakpoints,
|
|
minWidth: breakpoints,
|
|
container: {
|
|
padding: '1rem',
|
|
center: true,
|
|
},
|
|
colors: {
|
|
buzz: 'rgb(var(--buzz-color))',
|
|
white: '#fefefe',
|
|
black: '#222222',
|
|
dark: {
|
|
0: '#C1C2C5',
|
|
1: '#A6A7AB',
|
|
2: '#8c8fa3',
|
|
3: '#5C5F66',
|
|
4: '#373A40',
|
|
5: '#2C2E33',
|
|
6: '#25262B',
|
|
7: '#1A1B1E',
|
|
8: '#141517',
|
|
9: '#101113',
|
|
},
|
|
gray: {
|
|
0: '#f8f9fa',
|
|
1: '#f1f3f5',
|
|
2: '#e9ecef',
|
|
3: '#dee2e6',
|
|
4: '#ced4da',
|
|
5: '#adb5bd',
|
|
6: '#868e96',
|
|
7: '#495057',
|
|
8: '#343a40',
|
|
9: '#212529',
|
|
},
|
|
yellow: {
|
|
0: '#FFF9DB',
|
|
1: '#FFF3BF',
|
|
2: '#FFEC99',
|
|
3: '#FFE066',
|
|
4: '#FFD43B',
|
|
5: '#FCC419',
|
|
6: '#FAB005',
|
|
7: '#F59F00',
|
|
8: '#F08C00',
|
|
9: '#E67700',
|
|
},
|
|
green: {
|
|
0: '#EBFBEE',
|
|
1: '#D3F9D8',
|
|
2: '#B2F2BB',
|
|
3: '#8CE99A',
|
|
4: '#69DB7C',
|
|
5: '#51CF66',
|
|
6: '#40C057',
|
|
7: '#37B24D',
|
|
8: '#2F9E44',
|
|
9: '#2B8A3E',
|
|
},
|
|
blue: {
|
|
0: '#E7F5FF',
|
|
1: '#D0EBFF',
|
|
2: '#A5D8FF',
|
|
3: '#74C0FC',
|
|
4: '#4DABF7',
|
|
5: '#339AF0',
|
|
6: '#228BE6',
|
|
7: '#1C7ED6',
|
|
8: '#1971C2',
|
|
9: '#1864AB',
|
|
},
|
|
red: {
|
|
0: '#fff5f5',
|
|
1: '#ffe3e3',
|
|
2: '#ffc9c9',
|
|
3: '#ffa8a8',
|
|
4: '#ff8787',
|
|
5: '#ff6b6b',
|
|
6: '#fa5252',
|
|
7: '#f03e3e',
|
|
8: '#e03131',
|
|
9: '#c92a2a',
|
|
},
|
|
orange: {
|
|
0: '#fff4e6',
|
|
1: '#ffe8cc',
|
|
2: '#ffd8a8',
|
|
3: '#ffc078',
|
|
4: '#ffa94d',
|
|
5: '#ff922b',
|
|
6: '#fd7e14',
|
|
7: '#f76707',
|
|
8: '#e8590c',
|
|
9: '#d9480f',
|
|
},
|
|
lime: {
|
|
0: '#f4fce3',
|
|
1: '#e9fac8',
|
|
2: '#d8f5a2',
|
|
3: '#c0eb75',
|
|
4: '#a9e34b',
|
|
5: '#94d82d',
|
|
6: '#82c91e',
|
|
7: '#74b816',
|
|
8: '#66a80f',
|
|
9: '#5c940d',
|
|
},
|
|
gold: {
|
|
0: '#F6EDDF',
|
|
1: '#F2E4CF',
|
|
2: '#EDDBBF',
|
|
3: '#E9D2AF',
|
|
4: '#E5C99F',
|
|
5: '#E0C08F',
|
|
6: '#DCB77F',
|
|
7: '#D8AE6F',
|
|
8: '#D3A55F',
|
|
9: '#CD9848',
|
|
},
|
|
},
|
|
keyframes: {
|
|
'border-chase': {
|
|
'0%': { transform: 'rotate(0deg)' },
|
|
'100%': { transform: 'rotate(360deg)' },
|
|
},
|
|
wiggle: {
|
|
'0%, 100%': { transform: 'rotate(-3deg)' },
|
|
'50%': { transform: 'rotate(3deg)' },
|
|
},
|
|
jello: {
|
|
'0%': {
|
|
transform: 'scale3d(1, 1, 1)',
|
|
},
|
|
'30%': {
|
|
transform: 'scale3d(0.75, 1.25, 1)',
|
|
},
|
|
'40%': {
|
|
transform: 'scale3d(1.25, 0.75, 1)',
|
|
},
|
|
'50%': {
|
|
transform: 'scale3d(0.85, 1.15, 1)',
|
|
},
|
|
'65%': {
|
|
transform: 'scale3d(1.05, 0.95, 1)',
|
|
},
|
|
'75%': {
|
|
transform: 'scale3d(0.95, 1.05, 1)',
|
|
},
|
|
'100%': {
|
|
transform: 'scale3d(1, 1, 1)',
|
|
},
|
|
},
|
|
glowPulse: {
|
|
'0%': {
|
|
boxShadow: '0 0 0px rgba(0,255,0,0)',
|
|
},
|
|
'50%': {
|
|
boxShadow: '0 0 10px rgba(0,255,0,.8)',
|
|
},
|
|
'100%': {
|
|
boxShadow: '0 0 0px rgba(0,255,0,0)',
|
|
},
|
|
},
|
|
'gradient-shift': {
|
|
'0%': { backgroundPosition: '0% 50%' },
|
|
'50%': { backgroundPosition: '100% 50%' },
|
|
'100%': { backgroundPosition: '0% 50%' },
|
|
},
|
|
shimmer: {
|
|
'0%': { backgroundPosition: '-200% 0' },
|
|
'100%': { backgroundPosition: '200% 0' },
|
|
},
|
|
'buzz-glow': {
|
|
'0%, 100%': { boxShadow: '0 0 4px 1px rgba(var(--buzz-color), 0.3)' },
|
|
'50%': { boxShadow: '0 0 12px 4px rgba(var(--buzz-color), 0.7)' },
|
|
},
|
|
'icon-glow': {
|
|
// Two stacked drop-shadows hug the stroke tightly so the lines
|
|
// themselves read as "lit" rather than the bounding box casting a halo.
|
|
'0%, 100%': {
|
|
filter:
|
|
'drop-shadow(0 0 0.5px rgba(var(--icon-glow-color), 0.9)) drop-shadow(0 0 2px rgba(var(--icon-glow-color), 0.4))',
|
|
},
|
|
'50%': {
|
|
filter:
|
|
'drop-shadow(0 0 1px rgba(var(--icon-glow-color), 1)) drop-shadow(0 0 4px rgba(var(--icon-glow-color), 0.7))',
|
|
},
|
|
},
|
|
},
|
|
animation: {
|
|
wiggle: 'wiggle 1s ease-in-out infinite',
|
|
jello: 'jello 2s 1s ease-in-out',
|
|
glowPulse: 'glowPulse 1.2s ease-in-out 3',
|
|
'border-chase': 'border-chase 3s linear infinite',
|
|
'gradient-shift': 'gradient-shift 8s ease infinite',
|
|
shimmer: 'shimmer 6s ease-in-out infinite',
|
|
'buzz-glow': 'buzz-glow 2s ease-in-out infinite',
|
|
'icon-glow': 'icon-glow 2s ease-in-out infinite',
|
|
},
|
|
},
|
|
},
|
|
plugins: [
|
|
require('./src/tailwind/container-queries'),
|
|
plugin(function ({ matchUtilities, theme, addUtilities, addVariant, e }) {
|
|
matchUtilities(
|
|
{
|
|
'text-shadow': (value) => ({
|
|
textShadow: value,
|
|
}),
|
|
},
|
|
{ values: theme('textShadow') }
|
|
),
|
|
addUtilities({
|
|
'.aspect-portrait': {
|
|
aspectRatio: '7 / 9',
|
|
},
|
|
'.card': {},
|
|
'.absolute-center': {},
|
|
'.scrollbar-none': {
|
|
scrollbarWidth: 'none',
|
|
'::-webkit-scrollbar': {
|
|
display: 'none',
|
|
},
|
|
},
|
|
'.scrollbar-thin': {
|
|
scrollbarWidth: 'thin',
|
|
},
|
|
'.transform-3d': {
|
|
transform: 'translate3d(0, 0, 0)'
|
|
},
|
|
}),
|
|
addVariant('not-first', ({ modifySelectors, separator }) => {
|
|
modifySelectors(({ className }) => {
|
|
return `.${e(`not-first${separator}${className}`)}:not(:first-child)`;
|
|
});
|
|
}),
|
|
addVariant('not-last', ({ modifySelectors, separator }) => {
|
|
modifySelectors(({ className }) => {
|
|
return `.${e(`not-last${separator}${className}`)}:not(:last-child)`;
|
|
});
|
|
});
|
|
}),
|
|
// ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
|
|
],
|
|
};
|