diff --git a/packages/tailwindcss/reference.css b/packages/tailwindcss/reference.css new file mode 100644 index 000000000000..eb6736a35856 --- /dev/null +++ b/packages/tailwindcss/reference.css @@ -0,0 +1,256 @@ +/* Tailwind CSS syntax reference: demonstrates all supported directives and functions */ + +@config "./reference/reference.config.js"; + +@plugin "./reference/reference.plugin.js" { + enable-grid: true; + card-color: 'white'; + accent-scale: 'sky', 'amber'; + shadow-depth: 24; +} + +@import 'tailwindcss' prefix(tw); +@import 'tailwindcss/utilities' important; +@import 'tailwindcss/theme' theme(static); +@import './reference/reference-import.css' layer(utilities) supports(display: grid); + +@reference 'tailwindcss/theme'; +@reference './reference/reference-variant.css'; + +@source "./app/**/*.{js,ts,jsx,tsx}"; +@source not "./app/**/__tests__/*"; +@source inline("tw:prose tw:underline"); +@source not inline("tw:legacy"); + +@theme reference prefix(tw); + +@theme default { + --color-brand: #1d4ed8; + --font-sans: 'Inter', system-ui, sans-serif; + --spacing: 0.25rem; + + @keyframes reference-pulse { + from { + opacity: 1; + } + + to { + opacity: 0.35; + } + } +} + +@theme { + --color-accent: oklch(70% 0.12 220); + --radius-card: 1rem; + --value-sm: 14px; + --modifier-7: 28px; +} + +@theme inline { + --shadow-card: 0 25px 50px -12px color-mix(in oklab, var(--color-brand) 25%, transparent); +} + +@theme inline prefix(tw) { + --size-icon: 1.25rem; +} + +@theme reference inline { + --card-outline: 1px solid color-mix(in oklab, var(--color-brand) 60%, transparent); +} + +@theme static { + --ring-brand: var(--color-brand); + + @keyframes reference-spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } + } +} + +@media theme(reference) prefix(tw) important { + @theme { + --color-card-foreground: var(--color-accent); + } +} + +@media source("./components/**/*.{tsx,jsx}") theme(inline) { + @theme { + --spacing-component: calc(var(--spacing) * 6); + } +} + +@tailwind base; +@tailwind components; +@tailwind utilities source("./app/**/*.{html,js,ts,jsx,tsx}"); + +@layer base { + *, *::before, *::after { + box-sizing: border-box; + } + + html { + font-family: theme('fontFamily.sans', var(--font-sans)); + } + + body { + background-color: --alpha(var(--color-brand) / 7.5%); + color: var(--color-card-foreground, var(--color-accent)); + } +} + +@layer theme { + @theme { + --layer-spacing-8: calc(var(--spacing) * 8); + } +} + +@layer components { + .card { + @apply tw:bg-white tw:text-slate-900 tw:shadow-lg tw:rounded; + padding: --spacing(6); + border-radius: --theme(--radius-card, 0.75rem); + outline: --theme(--card-outline inline, 0 solid transparent); + box-shadow: var(--shadow-card, --theme(--shadow-card, 0 10px 30px rgba(15, 23, 42, 0.15))); + } + + .card { + @variant reference-dark { + outline-color: --theme(--ring-brand inline, var(--color-brand)); + } + + @variant motion-safe { + transition: transform 200ms ease; + } + + @variant hover { + transform: translateY(-2px); + } + } + + .chip { + display: inline-flex; + align-items: center; + gap: --spacing(2); + font-size: theme('fontSize.sm', 0.875rem); + } + + .chip { + @variant hocus { + @apply tw:ring-2 tw:ring-offset-2; + } + } +} + +@layer utilities { + @variant hover { + .hover\\:reference-underline { + text-decoration-line: underline; + } + } + + .content-auto { + content-visibility: auto; + } +} + +@utility card { + --tw-card-shadow: var(--shadow-card); + box-shadow: var(--tw-card-shadow); +} + +@utility stack-* { + --stack-gap: calc(var(--spacing) * --value(number)); + margin-top: var(--stack-gap); + margin-bottom: var(--stack-gap); +} + +@utility aspect-* { + aspect-ratio: --value(ratio); +} + +@utility example-* { + --value: --value(--value, [length]); + --modifier: --modifier(--modifier, [length]); + --modifier-with-calc: calc(--modifier(--modifier, [length]) * 2); + --modifier-literals: --modifier('literal', 'literal-2'); +} + +@utility mask-r-* { + --mask-right: linear-gradient( + to left, + transparent calc(var(--spacing) * --modifier(integer)), + black calc(var(--spacing) * --value(integer)) + ); + mask-image: var(--mask-right); +} + +@custom-variant hocus (&:hover, &:focus); + +@custom-variant theme-wrap { + @variant reference-dark { + @slot; + } +} + +@variant reference-dark { + &:where([data-theme='reference-dark'] *) { + @slot; + } +} + +@variant motion-safe { + @media (prefers-reduced-motion: no-preference) { + @slot; + } +} + +@variant hover { + &:hover { + @slot; + } +} + +@variant hover { + @variant reference-dark { + .reference-hover-dark { + color: var(--color-brand); + } + } +} + +@media (prefers-color-scheme: dark) { + @variant reference-dark { + @page { + margin: 1in; + } + } +} + +.badge { + border-radius: var(--radius-card); + padding-inline: --spacing(3); + padding-block: --spacing(1.5); +} + +.badge { + @variant reference-dark { + border-color: var(--color-brand); + } +} + +.icon { + width: var(--size-icon); + height: var(--size-icon); +} + +.icon { + @variant theme-wrap { + color: var(--color-card-foreground, var(--color-brand)); + } +} diff --git a/packages/tailwindcss/reference/reference-import.css b/packages/tailwindcss/reference/reference-import.css new file mode 100644 index 000000000000..889f163b9896 --- /dev/null +++ b/packages/tailwindcss/reference/reference-import.css @@ -0,0 +1,12 @@ +/* Imported stylesheet used by reference.css via @import */ + +@theme inline { + --imported-gap: 1.5rem; +} + +@layer utilities { + .from-import { + display: grid; + gap: var(--imported-gap, 1rem); + } +} diff --git a/packages/tailwindcss/reference/reference-variant.css b/packages/tailwindcss/reference/reference-variant.css new file mode 100644 index 000000000000..23bdb0eaa4db --- /dev/null +++ b/packages/tailwindcss/reference/reference-variant.css @@ -0,0 +1,19 @@ +/* Referenced stylesheet providing additional variants and theme tokens */ + +@theme reference { + --reference-accent: #f97316; +} + +@variant data-open { + &[data-open='true'] { + @slot; + } +} + +@variant data-open { + @variant hover { + .data-open-hover { + color: var(--reference-accent, currentColor); + } + } +} diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index f1dad6ccda4e..3a78f58912f5 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -217,6 +217,160 @@ describe('compiling CSS', () => { `) }) + test('reference.css compiles end-to-end', async () => { + let referencePath = path.resolve(__dirname, '..', 'reference.css') + let referenceCss = fs.readFileSync(referencePath, 'utf-8') + let referenceDir = path.dirname(referencePath) + + let builtinStyles = new Map([ + ['tailwindcss', path.resolve(referenceDir, 'index.css')], + ['tailwindcss/index.css', path.resolve(referenceDir, 'index.css')], + ['tailwindcss/preflight', path.resolve(referenceDir, 'preflight.css')], + ['tailwindcss/preflight.css', path.resolve(referenceDir, 'preflight.css')], + ['tailwindcss/theme', path.resolve(referenceDir, 'theme.css')], + ['tailwindcss/theme.css', path.resolve(referenceDir, 'theme.css')], + ['tailwindcss/utilities', path.resolve(referenceDir, 'utilities.css')], + ['tailwindcss/utilities.css', path.resolve(referenceDir, 'utilities.css')], + ]) + + let loadStylesheet = async (id: string, base: string) => { + let normalizedBase = base === '' ? referenceDir : base + let normalizedId = id.replace(/\\/g, '/') + if (builtinStyles.has(normalizedId)) { + let filePath = builtinStyles.get(normalizedId)! + return { + path: filePath, + base: path.dirname(filePath), + content: fs.readFileSync(filePath, 'utf-8'), + } + } + + let resolved = path.resolve(normalizedBase, id) + return { + path: resolved, + base: path.dirname(resolved), + content: fs.readFileSync(resolved, 'utf-8'), + } + } + + let referencePlugin = plugin.withOptions( + (options: Record = {}) => { + return ({ addUtilities, addVariant, theme }: PluginAPI) => { + let enableGrid = + typeof options['enable-grid'] === 'boolean' ? options['enable-grid'] : true + let cardColor = + typeof options['card-color'] === 'string' ? options['card-color'] : 'white' + let shadowDepthValue = options['shadow-depth'] + let shadowDepth = + typeof shadowDepthValue === 'number' + ? shadowDepthValue + : Number(shadowDepthValue) || 16 + + let accentScale = Array.isArray(options['accent-scale']) ? options['accent-scale'] : [] + let accentUtilities: Record> = {} + for (let accent of accentScale) { + if (typeof accent === 'string') { + accentUtilities[`.plugin-accent-${accent}`] = { + color: `var(--color-${accent}, currentColor)`, + } + } + } + + addUtilities({ + '.plugin-card': { + display: enableGrid ? 'grid' : 'block', + gap: theme('spacing.6', '1.5rem'), + 'background-color': cardColor, + 'box-shadow': `0 ${shadowDepth}px 32px -12px rgba(15, 23, 42, 0.18)`, + }, + ...accentUtilities, + }) + + addVariant('supports-grid', { + '@supports (display: grid)': '@slot', + }) + } + }, + () => ({ + theme: { + extend: { + colors: { + brand: '#2563eb', + 'card-foreground': '#1f2937', + }, + }, + }, + }), + ) + + let referenceConfig = { + darkMode: ['class', '[data-theme="reference-dark"]'], + safelist: ['tw:underline', 'plugin-card'], + theme: { + extend: { + colors: { + brand: '#1d4ed8', + 'card-foreground': '#111827', + }, + spacing: { + 6: '1.5rem', + }, + }, + }, + } + + let moduleRegistry = new Map([ + [path.resolve(referenceDir, 'reference/reference.plugin.js'), referencePlugin], + [path.resolve(referenceDir, 'reference/reference.config.js'), referenceConfig], + ]) + + let loadModule = async (id: string, base: string) => { + let normalizedBase = base === '' ? referenceDir : base + let resolved = path.resolve(normalizedBase, id) + if (!moduleRegistry.has(resolved)) { + throw new Error(`Unknown module requested by reference.css test: ${id}`) + } + + return { + path: resolved, + base: path.dirname(resolved), + module: moduleRegistry.get(resolved), + } + } + + let candidates = [ + 'tw:underline', + 'stack-4', + 'aspect-16/9', + 'example-sm/7', + 'example-[12px]/[16px]', + 'mask-r-4/6', + 'theme-wrap:tw:bg-brand', + 'supports-grid:tw:flex', + 'motion-safe:tw:animate-pulse', + 'reference-dark:tw:text-brand', + 'data-open:tw:px-4', + 'hocus:tw:ring-2', + 'plugin-card', + ] + + let output = await compileCss(referenceCss, candidates, { + from: referencePath, + loadStylesheet, + loadModule, + }) + + expect(output).toContain('.stack-4') + expect(output).toContain('.example-sm\\/7') + expect(output).toContain('.mask-r-4\\/6') + expect(output).toContain('.supports-grid\\:tw\\:flex') + expect(output).toContain('.theme-wrap\\:tw\\:bg-brand') + expect(output).toContain('.plugin-card') + expect(output).toContain('.data-open\\:tw\\:px-4') + expect(output).toContain('.from-import') + expect(output).toMatch(/@media \\(prefers-reduced-motion: no-preference\\)/) + }) + test('adds vendor prefixes', async () => { expect( await compileCss(