WIP: Get control back from AI

Steps:
- Write a README to understand what do I really want to have.
- Go line by line and get learn how it work.
- Rework a lot of shit into sustainable form.
- Understanding how tw work make me mad, so BetterMatchUtility was created - it separated default values (such as xl or 3 which will be shown as a tip), bare values (19 - as default but out of range), and arbitrary (like *-[5vw]).

P.S. AI is so bad in 2026, if it have choose between two evil it for sure take both, and that not good thing. And it's struggle to make any good design. And did not able see what it do clearly. But so great for exploration thought.
This commit is contained in:
Mira
2026-04-28 03:23:32 +03:00
parent 4c36674c60
commit 0a25911ba9
2 changed files with 278 additions and 113 deletions
+52
View File
@@ -0,0 +1,52 @@
# Tailwind Dashed Border SVG
A plugin for adressing issue with custamable and animatable dashed border via tailwind interface (mostly).
It uses real svg under the hood what provide the maximum flexibility. Non animated border coming soon.
# Setup
```
pnpm i tailwind-dashed-border
```
prepare a component for svg
```tsx
export function BorderSVG() {
return (
<svg className="dashed-border-svg" aria-hidden="true">
<rect className="dashed-border-rect" />
</svg>
);
}
```
and now you are ready to make your borders
```tsx
<div className="dashed-border">
<BorderSVG/>
</div>
```
# Properties
## Setup
- dashed-border
- dashed-border-svg
- dashed-border-rect
## Configuration
- dashed-border-color-[color]
- dashed-border-width-[spacing]
- dashed-border-length-[spacing]
- dashed-border-gap-[spacing]
- dashed-border-offset-[spacing]
- dashed-border-align-["inside"/"edge"/"outside"]
- dashed-border-align-[spacing] (0 - inside, toward the outside)
## Vanilla extended
- duration-[time]
- [timing function] (easing-linear, ease-in-out, etc.)
- transition-all
+226 -113
View File
@@ -1,13 +1,13 @@
import plugin from "tailwindcss/plugin";
import type {
BareValueResolver,
NamedUtilityValue as BareValue,
MatchOptions,
PluginAPI,
MatchFn,
} from "tailwindcss/plugin";
import flattenColorPalette from "tailwindcss/lib/util/flattenColorPalette";
const DEFAULT_GAP = "calc(var(--spacing, 0.25rem) * 2)";
const DEFAULT_LENGTH = "calc(var(--spacing, 0.25rem) * 3)";
const DEFAULT_OFFSET = "0px";
const DEFAULT_WIDTH = "1px";
const DEFAULT_COLOR = "currentColor";
const DEFAULT_RADIUS = "0px";
const DASH_ARRAY = "var(--tw-dashed-border-length) var(--tw-dashed-border-gap)";
const INSET = "calc(var(--tw-dashed-border-width) / 2)";
const INNER_SIZE = "calc(100% - var(--tw-dashed-border-width))";
@@ -22,48 +22,148 @@ const TRANSITION_DURATION =
const TRANSITION_TIMING_FUNCTION =
"var(--tw-ease, var(--default-transition-timing-function))";
type ThemeLeafMap = Record<string, string>;
type FlatThemeLeaf = Record<string, string>;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
const POSITIVE_NUMBER_PATTERN = /^(?:\d+|\d*\.\d+)$/; //accept numbers like "12", ".5", "12.5"
function flattenThemeLeaves(
value: unknown,
path: string[] = [],
): ThemeLeafMap {
if (!isRecord(value)) {
return {};
}
const flattened: ThemeLeafMap = {};
for (const [key, entry] of Object.entries(value)) {
if (key === "__CSS_VALUES__") {
continue;
}
const nextPath = key === "DEFAULT" ? path : [...path, key];
if (isRecord(entry)) {
Object.assign(flattened, flattenThemeLeaves(entry, nextPath));
continue;
}
if (typeof entry === "string" || typeof entry === "number") {
const flattenedKey = nextPath.join("-");
flattened[flattenedKey === "" ? "DEFAULT" : flattenedKey] = String(entry);
}
}
return flattened;
}
function omitDefaultKey(values: ThemeLeafMap): ThemeLeafMap {
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 createTransitionStyles(properties: string) {
return {
"transition-property": properties,
@@ -72,19 +172,28 @@ function createTransitionStyles(properties: string) {
};
}
const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
({ addBase, addUtilities, matchUtilities, theme }) => {
const spacingValues = flattenThemeLeaves(theme("spacing"));
const borderWidthValues = omitDefaultKey(
flattenThemeLeaves(theme("borderWidth")),
);
const borderRadiusValues = flattenThemeLeaves(theme("borderRadius"));
const colorValues = {
...flattenColorPalette(theme("colors") ?? {}),
current: "currentColor",
inherit: "inherit",
transparent: "transparent",
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": {
@@ -105,7 +214,7 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
"@property --tw-dashed-border-width": {
syntax: "<length>",
inherits: "true",
"initial-value": DEFAULT_WIDTH,
"initial-value": "1px",
},
"@property --tw-dashed-border-radius": {
syntax: "<length>",
@@ -114,19 +223,18 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
},
".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-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-radius": DEFAULT_RADIUS,
// "--tw-dashed-border-radius": DEFAULT_RADIUS,
},
".dashed-border-svg": {
position: "absolute",
inset: "0",
width: "100%",
height: "100%",
overflow: "hidden",
"pointer-events": "none",
},
".dashed-border-rect": {
@@ -144,62 +252,67 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
},
});
matchUtilities(
{
"dashed-border-gap": (value) => ({
"--tw-dashed-border-gap": value,
}),
"dashed-border-length": (value) => ({
"--tw-dashed-border-length": value,
}),
"dashed-border-offset": (value) => ({
"--tw-dashed-border-offset": value,
}),
},
{
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",
values: spacingValues,
},
);
};
};
matchUtilities(
{
"dashed-border-width": (value) => ({
"--tw-dashed-border-width": value,
}),
},
{
type: "length",
values: borderWidthValues,
},
);
matchUtilities(
{
"dashed-border-color": (value) => ({
"--tw-dashed-border-color": value,
}),
},
{
type: "color",
values: colorValues,
},
);
const radiusUtilities = Object.fromEntries(
Object.entries(borderRadiusValues).map(([key, value]) => {
const roundedClass = key === "DEFAULT" ? "rounded" : `rounded-${key}`;
return [
`.dashed-border.${roundedClass}`,
{
"--tw-dashed-border-radius": value,
},
];
matchUtilities([
spacingUtil({ utilityName: "dashed-border-gap" }),
spacingUtil({ utilityName: "dashed-border-length" }),
spacingUtil({
utilityName: "dashed-border-offset",
supportsNegativeValues: true,
}),
);
{
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"),
},
});
addUtilities({
...radiusUtilities,
".dashed-border-transition .dashed-border-rect": createTransitionStyles(
ALL_TRANSITION_PROPERTIES,
),