import plugin from "tailwindcss/plugin"; import type { BareValueResolver, NamedUtilityValue as BareValue, MatchOptions, PluginAPI, MatchFn, } from "tailwindcss/plugin"; import flattenColorPalette from "tailwindcss/lib/util/flattenColorPalette"; type FlatThemeLeaf = Record; const POSITIVE_NUMBER_PATTERN = /^(?:\d+|\d*\.\d+)$/; //accept numbers like "12", ".5", "12.5" function omitDefault(values: FlatThemeLeaf): FlatThemeLeaf { const { DEFAULT: _default, ...rest } = values; return rest; } function spacingBareValueResolver(candidate: BareValue): string | undefined { if ( candidate.fraction !== null || !POSITIVE_NUMBER_PATTERN.test(candidate.value) ) { return undefined; } return `calc(var(--spacing) * ${candidate.value})`; } function borderWidthBareValueResolver( candidate: BareValue, ): string | undefined { if ( candidate.fraction !== null || !POSITIVE_NUMBER_PATTERN.test(candidate.value) ) { return undefined; } return `${candidate.value}px`; } // Please not use T as an object; that not how things used to be in tw anyway, just don't. type ConfigValuesTree = { [key: string]: ConfigValuesTree | T; }; function isConfigValuesTree( x: ConfigValuesTree | T, ): x is ConfigValuesTree { const isObject = typeof x === "object" && x !== null && !Array.isArray(x); return isObject; } type BetterMatchUtility = { utilityName: string; arbitraryValueResolver: MatchFn; bareValueResolver?: BareValueResolver; type?: MatchOptions["type"]; defaultValues?: ConfigValuesTree | undefined; modifiers?: MatchOptions["modifiers"]; supportsNegativeValues?: MatchOptions["supportsNegativeValues"]; }; function betterMatchUtility( matchUtilities: PluginAPI["matchUtilities"], ): (utility: BetterMatchUtility | BetterMatchUtility[]) => void { function handleUtility(utility: BetterMatchUtility): void { // define utility base const arbitraryValueResolver: Record = { [utility.utilityName]: utility.arbitraryValueResolver, }; // define options const defaultValues = utility.defaultValues ? { ...utility.defaultValues } : {}; defaultValues["__CSS_VALUES__"] = undefined; const flatDefaultValues = flat(defaultValues); const stringValues = toStringOrSkip(flatDefaultValues); const values = utility.bareValueResolver ? withBareValueResolver(stringValues, utility.bareValueResolver) : stringValues; const options: MatchOptions = { type: utility.type, supportsNegativeValues: utility.supportsNegativeValues, values, modifiers: utility.modifiers, }; // Register this utility matchUtilities(arbitraryValueResolver, options); } return function (utilities) { if (Array.isArray(utilities)) { utilities.forEach(handleUtility); } else { handleUtility(utilities); } }; } type MatchValues = Required["values"]; function withBareValueResolver( values: FlatThemeLeaf, resolver: BareValueResolver, ): MatchValues { const valuesWithResolver: MatchValues = {}; Object.assign(valuesWithResolver, values); valuesWithResolver.__BARE_VALUE__ = resolver; return valuesWithResolver; } function flat( configPart: ConfigValuesTree, path: string = "", ): Record { function addKeyToPath(path: string, key: string): string { if (key === "DEFAULT") return path; if (path === "") return key; return `${path}-${key}`; } const flatConf: Record = {}; for (const [key, value] of Object.entries(configPart)) { const currentPath = addKeyToPath(path, key); if (isConfigValuesTree(value)) { Object.assign(flatConf, flat(value, currentPath)); } else { flatConf[currentPath] = value; } } return flatConf; } const _typeOf = typeof "any"; type BaseTypes = typeof _typeOf; function toStringOrSkip( flatConfigPart: Record, ): Record { const allowedTypes: BaseTypes[] = ["bigint", "boolean", "number", "string"]; const entries = Object.entries(flatConfigPart); const stringifiedEntries: [string, string][] = []; entries.forEach(([key, value]) => { const type = typeof value; if (allowedTypes.includes(type)) stringifiedEntries.push([key, String(value)]); }); const result = Object.fromEntries(stringifiedEntries); return result; } function toCssVariable( cssVariable: string, ): (value: string) => ReturnType { return (value) => { const cssInJs = { [cssVariable]: value, }; return cssInJs; }; } const dashedBorderPlugin: ReturnType = plugin( ({ addBase, addUtilities, theme, ...pluginApi }) => { // const matchUtilities = pluginApi.matchUtilities; const matchUtilities = betterMatchUtility(pluginApi.matchUtilities); const DEFAULT_GAP = "0.5rem"; const DEFAULT_LENGTH = "0.75rem"; const DEFAULT_OFFSET = "0px"; const DEFAULT_WIDTH = "1px"; const DEFAULT_COLOR = "currentColor"; const DEFAULT_RADIUS = "0px"; addBase({ "@property --tw-dashed-border-gap": { syntax: '""', inherits: "true", "initial-value": "8px", }, "@property --tw-dashed-border-length": { syntax: '""', inherits: "true", "initial-value": "10px", }, "@property --tw-dashed-border-offset": { syntax: '""', inherits: "true", "initial-value": "0px", }, "@property --tw-dashed-border-width": { syntax: '""', inherits: "true", "initial-value": "1px", }, "@property --tw-dashed-border-radius": { syntax: '""', inherits: "true", "initial-value": "currentColor", }, "@property --tw-dashed-border-alignment": { syntax: '""', inherits: "true", "initial-value": "0", }, "@property --tw-dashed-border-duration": { syntax: '"