358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
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<string, string>;
|
|
|
|
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<T = string> = {
|
|
[key: string]: ConfigValuesTree<T> | T;
|
|
};
|
|
|
|
function isConfigValuesTree<T>(
|
|
x: ConfigValuesTree<T> | T,
|
|
): x is ConfigValuesTree<T> {
|
|
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<unknown> | 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<string, MatchFn> = {
|
|
[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<MatchOptions>["values"];
|
|
function withBareValueResolver(
|
|
values: FlatThemeLeaf,
|
|
resolver: BareValueResolver,
|
|
): MatchValues {
|
|
const valuesWithResolver: MatchValues = {};
|
|
Object.assign(valuesWithResolver, values);
|
|
valuesWithResolver.__BARE_VALUE__ = resolver;
|
|
return valuesWithResolver;
|
|
}
|
|
|
|
function flat<T>(
|
|
configPart: ConfigValuesTree<T>,
|
|
path: string = "",
|
|
): Record<string, T> {
|
|
function addKeyToPath(path: string, key: string): string {
|
|
if (key === "DEFAULT") return path;
|
|
if (path === "") return key;
|
|
return `${path}-${key}`;
|
|
}
|
|
|
|
const flatConf: Record<string, T> = {};
|
|
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<string, unknown>,
|
|
): Record<string, string> {
|
|
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<MatchFn> {
|
|
return (value) => {
|
|
const cssInJs = {
|
|
[cssVariable]: value,
|
|
};
|
|
return cssInJs;
|
|
};
|
|
}
|
|
|
|
const dashedBorderPlugin: ReturnType<typeof plugin> = 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: '"<length>"',
|
|
inherits: "true",
|
|
"initial-value": "8px",
|
|
},
|
|
"@property --tw-dashed-border-length": {
|
|
syntax: '"<length>"',
|
|
inherits: "true",
|
|
"initial-value": "10px",
|
|
},
|
|
"@property --tw-dashed-border-offset": {
|
|
syntax: '"<length>"',
|
|
inherits: "true",
|
|
"initial-value": "0px",
|
|
},
|
|
"@property --tw-dashed-border-width": {
|
|
syntax: '"<length>"',
|
|
inherits: "true",
|
|
"initial-value": "1px",
|
|
},
|
|
"@property --tw-dashed-border-radius": {
|
|
syntax: '"<length>"',
|
|
inherits: "true",
|
|
"initial-value": "currentColor",
|
|
},
|
|
"@property --tw-dashed-border-alignment": {
|
|
syntax: '"<number>"',
|
|
inherits: "true",
|
|
"initial-value": "0",
|
|
},
|
|
"@property --tw-dashed-border-duration": {
|
|
syntax: '"<time>"',
|
|
inherits: "true",
|
|
"initial-value": "150ms",
|
|
},
|
|
"@property --tw-dashed-border-ease": {
|
|
syntax: '"*"',
|
|
inherits: "true",
|
|
"initial-value": "initial",
|
|
},
|
|
".dashed-border": {
|
|
position: "relative",
|
|
// "--tw-dashed-border-gap": DEFAULT_GAP,
|
|
// "--tw-dashed-border-length": DEFAULT_LENGTH,
|
|
// "--tw-dashed-border-offset": DEFAULT_OFFSET,
|
|
// "--tw-dashed-border-width": DEFAULT_WIDTH,
|
|
"--tw-dashed-border-color": DEFAULT_COLOR,
|
|
"--tw-dashed-border-ease": "var(--tw-ease)",
|
|
"--tw-dashed-border-duration": "var(--tw-duration)",
|
|
// "--tw-dashed-border-radius": DEFAULT_RADIUS,
|
|
},
|
|
".dashed-border-svg": {
|
|
position: "absolute",
|
|
inset: "0",
|
|
width: "100%",
|
|
height: "100%",
|
|
"pointer-events": "none",
|
|
},
|
|
});
|
|
|
|
const INSET =
|
|
"calc((0.5 - var(--tw-dashed-border-alignment)) * var(--tw-dashed-border-width))";
|
|
const INNER_SIZE =
|
|
"calc(100% + var(--tw-dashed-border-width) * (2 * var(--tw-dashed-border-alignment) - 1))";
|
|
const ADJUSTED_RADIUS =
|
|
"max(0px, calc(var(--tw-dashed-border-radius) - (0.5 - var(--tw-dashed-border-alignment)) * var(--tw-dashed-border-width)))";
|
|
|
|
addBase({
|
|
".dashed-border-rect": {
|
|
fill: "none",
|
|
stroke: "var(--tw-dashed-border-color)",
|
|
"stroke-width": "var(--tw-dashed-border-width)",
|
|
"stroke-dasharray":
|
|
"var(--tw-dashed-border-length) var(--tw-dashed-border-gap)",
|
|
"stroke-dashoffset": "var(--tw-dashed-border-offset)",
|
|
"vector-effect": "non-scaling-stroke",
|
|
x: INSET,
|
|
y: INSET,
|
|
width: INNER_SIZE,
|
|
height: INNER_SIZE,
|
|
rx: ADJUSTED_RADIUS,
|
|
},
|
|
});
|
|
|
|
const spacingRawValues = theme("spacing") as
|
|
| ConfigValuesTree<string>
|
|
| undefined;
|
|
const spacingValues = flat(spacingRawValues ?? {});
|
|
|
|
const spacingUtil = (opts: {
|
|
utilityName: string;
|
|
supportsNegativeValues?: boolean;
|
|
}): BetterMatchUtility => {
|
|
return {
|
|
utilityName: opts.utilityName,
|
|
arbitraryValueResolver: toCssVariable(`--tw-${opts.utilityName}`),
|
|
bareValueResolver: spacingBareValueResolver,
|
|
defaultValues: spacingValues,
|
|
supportsNegativeValues: opts.supportsNegativeValues,
|
|
type: "length",
|
|
};
|
|
};
|
|
|
|
matchUtilities([
|
|
spacingUtil({ utilityName: "dashed-border-gap" }),
|
|
spacingUtil({ utilityName: "dashed-border-length" }),
|
|
spacingUtil({
|
|
utilityName: "dashed-border-offset",
|
|
supportsNegativeValues: true,
|
|
}),
|
|
{
|
|
utilityName: "dashed-border-align",
|
|
arbitraryValueResolver: toCssVariable("--tw-dashed-border-alignment"),
|
|
defaultValues: {
|
|
inside: "0",
|
|
edge: "0.5",
|
|
outside: "1",
|
|
},
|
|
type: "number",
|
|
},
|
|
{
|
|
utilityName: "dashed-border-width",
|
|
arbitraryValueResolver: toCssVariable("--tw-dashed-border-width"),
|
|
bareValueResolver: borderWidthBareValueResolver,
|
|
defaultValues: omitDefault(theme("borderWidth")),
|
|
type: "length",
|
|
},
|
|
]);
|
|
|
|
const colorValues = {
|
|
DEFAULT: flattenColorPalette(theme("colors")),
|
|
current: "currentColor",
|
|
inherit: "inherit",
|
|
transparent: "transparent",
|
|
};
|
|
|
|
matchUtilities({
|
|
utilityName: "dashed-border-color",
|
|
arbitraryValueResolver: toCssVariable("--tw-dashed-border-color"),
|
|
defaultValues: colorValues,
|
|
type: "color",
|
|
});
|
|
|
|
matchUtilities({
|
|
utilityName: "rounded",
|
|
arbitraryValueResolver: toCssVariable("--tw-dashed-border-radius"),
|
|
defaultValues: omitDefault(theme("borderRadius")),
|
|
});
|
|
addUtilities({
|
|
".rounded": {
|
|
"--tw-dashed-border-radius": theme("borderRadius.DEFAULT"),
|
|
},
|
|
});
|
|
|
|
function createTransitionStyles(properties: string) {
|
|
return {
|
|
"transition-property": properties,
|
|
"transition-duration":
|
|
"var(--tw-dashed-border-duration, var(--default-transition-duration))",
|
|
"transition-timing-function":
|
|
"var(--tw-dashed-border-ease, var(--default-transition-timing-function))",
|
|
};
|
|
}
|
|
const ALL_TRANSITION_PROPERTIES =
|
|
"stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx, x, y, width, height";
|
|
addUtilities({
|
|
".dashed-border.transition .dashed-border-rect": createTransitionStyles(
|
|
ALL_TRANSITION_PROPERTIES,
|
|
),
|
|
".dashed-border.transition-all .dashed-border-rect":
|
|
createTransitionStyles(ALL_TRANSITION_PROPERTIES),
|
|
".dashed-border.transition-colors .dashed-border-rect":
|
|
createTransitionStyles("stroke"),
|
|
|
|
".dashed-border.transition-dashed-border .dashed-border-rect":
|
|
createTransitionStyles(ALL_TRANSITION_PROPERTIES),
|
|
});
|
|
},
|
|
);
|
|
|
|
export default dashedBorderPlugin;
|