Jonathan Reinink dad9ac6209
Revert new default form styles (#15100)
Closes #15071

This PR reverts the changes in #15036 which add consistent base styles
for buttons and form controls to Preflight.

While this felt like a good idea (for the reasons explained in that PR),
practically this is just too disruptive of a change for people upgrading
from v3 to v4.

While updating some of our projects to v4 we found ourselves adding
classes to undo styles more often than we expected, and it also felt
inconsistent to have to use a different set of classes to style a link
or a button when we wanted them to look the same.

We also decided it feels a little strange that you could change the
border color of an element without ever specifying that it should have a
border, for example this just feels a little wrong:

```html
<button class="border-blue-500">
```

We also needed to set a default `color-scheme` value for any of this
stuff to work which breaks the ability to use the `color-scheme` meta
tag.

Since this change was a fairly major breaking change and we aren't
feeling much benefit from it, it doesn't feel worth making this change
for v4.

---------

Co-authored-by: Philipp Spiess <hello@philippspiess.com>
2024-11-22 08:55:05 -05:00

178 lines
5.2 KiB
TypeScript

import dedent from 'dedent'
import postcss, { type Plugin, type Root } from 'postcss'
import { keyPathToCssProperty } from '../../../tailwindcss/src/compat/apply-config-to-theme'
import type { Config } from '../../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
import { toKeyPath } from '../../../tailwindcss/src/utils/to-key-path'
import * as ValueParser from '../../../tailwindcss/src/value-parser'
// Defaults in v4
const DEFAULT_BORDER_COLOR = 'currentColor'
const css = dedent
const BORDER_COLOR_COMPATIBILITY_CSS = css`
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: theme(borderColor.DEFAULT);
}
}
`
export function migratePreflight({
designSystem,
userConfig,
}: {
designSystem: DesignSystem
userConfig?: Config
}): Plugin {
// @ts-expect-error
let defaultBorderColor = userConfig?.theme?.borderColor?.DEFAULT
function canResolveThemeValue(path: string) {
let variable = `--${keyPathToCssProperty(toKeyPath(path))}` as const
return Boolean(designSystem.theme.get([variable]))
}
function migrate(root: Root) {
let isTailwindRoot = false
root.walkAtRules('import', (node) => {
if (
/['"]tailwindcss['"]/.test(node.params) ||
/['"]tailwindcss\/preflight['"]/.test(node.params)
) {
isTailwindRoot = true
return false
}
})
if (!isTailwindRoot) return
// Figure out the compatibility CSS to inject
let compatibilityCssString = ''
if (defaultBorderColor !== DEFAULT_BORDER_COLOR) {
compatibilityCssString += BORDER_COLOR_COMPATIBILITY_CSS
compatibilityCssString += '\n\n'
}
compatibilityCssString = `\n@tw-bucket compatibility {\n${compatibilityCssString}\n}\n`
let compatibilityCss = postcss.parse(compatibilityCssString)
// Replace the `theme(…)` with v3 values if we can't resolve the theme
// value.
compatibilityCss.walkDecls((decl) => {
if (decl.value.includes('theme(')) {
decl.value = substituteFunctionsInValue(ValueParser.parse(decl.value), (path) => {
if (canResolveThemeValue(path)) {
return defaultBorderColor
} else {
if (path === 'borderColor.DEFAULT') {
return 'var(--color-gray-200, currentColor)'
}
}
return null
})
}
})
// Cleanup `--border-color` definition in `theme(…)`
root.walkAtRules('theme', (node) => {
node.walkDecls('--border-color', (decl) => {
decl.remove()
})
if (node.nodes?.length === 0) {
node.remove()
}
})
// Inject the compatibility CSS
root.append(compatibilityCss)
}
return {
postcssPlugin: '@tailwindcss/upgrade/migrate-preflight',
OnceExit: migrate,
}
}
function substituteFunctionsInValue(
ast: ValueParser.ValueAstNode[],
handle: (value: string, fallback?: string) => string | null,
) {
ValueParser.walk(ast, (node, { replaceWith }) => {
if (node.kind === 'function' && node.value === 'theme') {
if (node.nodes.length < 1) return
// Ignore whitespace before the first argument
if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') {
node.nodes.shift()
}
let pathNode = node.nodes[0]
if (pathNode.kind !== 'word') return
let path = pathNode.value
// For the theme function arguments, we require all separators to contain
// comma (`,`), spaces alone should be merged into the previous word to
// avoid splitting in this case:
//
// theme(--color-red-500 / 75%) theme(--color-red-500 / 75%, foo, bar)
//
// We only need to do this for the first node, as the fallback values are
// passed through as-is.
let skipUntilIndex = 1
for (let i = skipUntilIndex; i < node.nodes.length; i++) {
if (node.nodes[i].value.includes(',')) {
break
}
path += ValueParser.toCss([node.nodes[i]])
skipUntilIndex = i + 1
}
path = eventuallyUnquote(path)
let fallbackValues = node.nodes.slice(skipUntilIndex + 1)
let replacement =
fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path)
if (replacement === null) return
replaceWith(ValueParser.parse(replacement))
}
})
return ValueParser.toCss(ast)
}
function eventuallyUnquote(value: string) {
if (value[0] !== "'" && value[0] !== '"') return value
let unquoted = ''
let quoteChar = value[0]
for (let i = 1; i < value.length - 1; i++) {
let currentChar = value[i]
let nextChar = value[i + 1]
if (currentChar === '\\' && (nextChar === quoteChar || nextChar === '\\')) {
unquoted += nextChar
i++
} else {
unquoted += currentChar
}
}
return unquoted
}