Compare commits

...

10 Commits

Author SHA1 Message Date
Mira e524c5046a back-up & some testing results 2026-05-07 17:44:09 +03:00
Mira 0b807644b1 Solve the duration/ease function problem 2026-05-07 17:32:30 +03:00
Mira 2ce7a39c1a Return align function! 2026-04-29 02:05:45 +03:00
Mira 0a25911ba9 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.
2026-04-28 03:23:32 +03:00
Mira 4c36674c60 Problem: Tailwind share too little about their interfaces.
Solution:
- Instead of type digging there is a little patch pnpm automatically will apply.
2026-04-28 03:08:05 +03:00
Mira 1acf0ce9cd attempt from scratch 2026-04-15 14:27:39 +03:00
Mira fe93824063 delete everythihg for fresh start 2026-04-15 14:18:46 +03:00
Mira c64db01bb4 Everything broken... 2026-04-15 14:16:05 +03:00
Mira e4c91c4a93 align 2026-04-15 02:46:44 +03:00
Mira dd4ab29df2 proto-1 2026-04-15 02:00:24 +03:00
14 changed files with 7882 additions and 0 deletions
View File
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
pnpm-lock.yaml
.direnv
+306
View File
@@ -0,0 +1,306 @@
# Tailwind Dashed Border Plugin: Technical Description
This plugin provides a small set of Tailwind CSS utilities for drawing configurable dashed borders with an SVG `<rect>` overlay instead of using the native CSS `border-style: dashed`. The SVG approach makes dash length, gap, offset, stroke width, color, and rounded corners independently controllable.
## What the plugin does
The plugin defines:
- Base CSS custom properties for dashed-border configuration.
- Structural utilities for the container, SVG overlay, and SVG rectangle.
- Generated utility families for gap, dash length, offset, stroke width, stroke color, and border radius.
- Transition helpers so dashed-border visuals can animate consistently with Tailwind transition utilities.
The plugin expects the dashed border to be rendered by an element structure like this:
```html
<div class="dashed-border dashed-border-gap-2 dashed-border-length-4 dashed-border-width-2">
<svg class="dashed-border-svg" aria-hidden="true">
<rect class="dashed-border-rect" />
</svg>
<!-- content -->
</div>
```
## Core structural utilities
### `.dashed-border`
This is the root utility for any element using the plugin. It:
- Sets default CSS variables.
- Applies `position: relative` so the SVG overlay can be absolutely positioned.
Default values:
- `--tw-dashed-border-gap`: `calc(var(--spacing, 0.25rem) * 2)`
- `--tw-dashed-border-length`: `calc(var(--spacing, 0.25rem) * 3)`
- `--tw-dashed-border-offset`: `0px`
- `--tw-dashed-border-width`: `1px`
- `--tw-dashed-border-color`: `currentColor`
- `--tw-dashed-border-radius`: `0px`
### `.dashed-border-svg`
This utility is intended for the SVG overlay element. It:
- Makes the SVG fill the container with `position: absolute` and `inset: 0`.
- Sets `width: 100%` and `height: 100%`.
- Uses `overflow: hidden`.
- Uses `pointer-events: none` so the overlay does not block interaction.
### `.dashed-border-rect`
This utility is intended for the SVG `<rect>` that draws the actual border. It:
- Removes fill with `fill: none`.
- Uses `stroke` for the border color.
- Uses `stroke-dasharray` for dash length and gap.
- Uses `stroke-dashoffset` for dash phase shifting.
- Uses `vector-effect: non-scaling-stroke` to keep stroke width stable.
- Positions the rectangle inset by half the stroke width, so the stroke remains visually inside the element bounds.
- Uses `rx` to simulate rounded corners.
Computed shape rules:
- `x` / `y`: `calc(var(--tw-dashed-border-width) / 2)`
- `width` / `height`: `calc(100% - var(--tw-dashed-border-width))`
- `rx`: `max(0px, calc(var(--tw-dashed-border-radius) - (var(--tw-dashed-border-width) / 2)))`
Dash pattern:
```css
stroke-dasharray: <length> <gap>;
```
## Registered custom properties
The plugin registers these `@property` definitions:
- `--tw-dashed-border-gap`
- `--tw-dashed-border-length`
- `--tw-dashed-border-offset`
- `--tw-dashed-border-width`
- `--tw-dashed-border-radius`
These registrations provide typed CSS custom properties so transitions and interpolation behave more predictably in supporting browsers.
## Generated utility families
### Gap utilities
Class shape:
```text
dashed-border-gap-*
```
Source values:
- Tailwind `theme.spacing`
Effect:
- Sets `--tw-dashed-border-gap`
- Recomputes `stroke-dasharray`
Example:
```html
<div class="dashed-border dashed-border-gap-4">
```
### Dash length utilities
Class shape:
```text
dashed-border-length-*
```
Source values:
- Tailwind `theme.spacing`
Effect:
- Sets `--tw-dashed-border-length`
- Recomputes `stroke-dasharray`
Example:
```html
<div class="dashed-border dashed-border-length-6">
```
### Dash offset utilities
Class shape:
```text
dashed-border-offset-*
```
Source values:
- Tailwind `theme.spacing`
Effect:
- Sets `--tw-dashed-border-offset`
- Updates `stroke-dashoffset`
Example:
```html
<div class="dashed-border dashed-border-offset-2">
```
### Stroke width utilities
Class shape:
```text
dashed-border-width-*
```
Source values:
- Tailwind `theme.borderWidth`
Effect:
- Sets `--tw-dashed-border-width`
- Updates `stroke-width`
- Recomputes `x`, `y`, `width`, `height`, and `rx`
Example:
```html
<div class="dashed-border dashed-border-width-2">
```
### Stroke color utilities
Class shape:
```text
dashed-border-color-*
```
Source values:
- Flattened Tailwind `theme.colors`
- Plus explicit aliases:
- `dashed-border-color-current`
- `dashed-border-color-inherit`
- `dashed-border-color-transparent`
Effect:
- Sets `--tw-dashed-border-color`
- Updates SVG `stroke`
Example:
```html
<div class="dashed-border dashed-border-color-slate-500">
```
Nested Tailwind colors are flattened with dash-separated keys, so a theme color like `slate.500` becomes:
```text
dashed-border-color-slate-500
```
### Radius utilities
Class shape:
```text
dashed-border.rounded-*
```
These are not generated as `dashed-border-radius-*`. Instead, the plugin creates compound selectors that combine `.dashed-border` with Tailwind rounded classes.
Examples of generated selectors:
- `.dashed-border.rounded`
- `.dashed-border.rounded-sm`
- `.dashed-border.rounded-lg`
- `.dashed-border.rounded-full`
Source values:
- Tailwind `theme.borderRadius`
Effect:
- Sets `--tw-dashed-border-radius`
- Lets the SVG rectangle inherit matching rounded-corner behavior through its computed `rx`
Example:
```html
<div class="dashed-border rounded-lg">
```
This means the expected way to apply radius is:
```html
<div class="dashed-border rounded-xl">
```
not:
```html
<div class="dashed-border-radius-xl">
```
## Transition behavior
The plugin adds transition helpers for dashed-border visuals.
Supported patterns:
- `.dashed-border-transition`
- `.dashed-border.transition`
- `.dashed-border.transition-all`
- `.dashed-border.transition-colors`
Behavior:
- Inherits Tailwind-style transition duration and timing defaults.
- Applies transitions to the inner `.dashed-border-rect`.
- `transition` / `transition-all` animate:
- `stroke-dasharray`
- `stroke-dashoffset`
- `stroke-width`
- `stroke`
- `rx`
- `transition-colors` animates only `stroke`
Example:
```html
<div class="dashed-border rounded-lg transition-all dashed-border-color-slate-400 hover:dashed-border-color-slate-900">
```
## Expected visual model
When used correctly, the utilities should produce:
- A container element that establishes the positioning context.
- A full-size SVG overlay sitting on top of the container.
- A `<rect>` inset by half the stroke width so the line is not clipped.
- A dashed stroke whose dash length and gap are independently tunable.
- Rounded corners that visually track Tailwind `rounded-*` utilities.
- Hover, or others transition triggers should be embedded from main tailwind flow. Use their duration and animation function.
## Implementation notes
- The plugin flattens nested color objects from `theme.colors` into dash-separated utility suffixes.
- `DEFAULT` keys are handled specially so default theme values map cleanly to `rounded` and other default-style selectors.
- Radius is adjusted by subtracting half the stroke width, which keeps the visible curve aligned with the outer element radius.
- The plugin is built around Tailwind CSS v4 plugin APIs: `addBase`, `addUtilities`, and `matchUtilities`.
Generated
+60
View File
@@ -0,0 +1,60 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1776105745,
"narHash": "sha256-3TdZByz6NafPdfpFS+pevgO75kSS9qFOR2pI9cZp7XE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d17aeaf88dea0fe37c73d33629d71cb51deafdb0",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+6509
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
{
"name": "tailwind-dashed-border",
"version": "0.1.0",
"description": "Configurable dashed border utility for Tailwind CSS v4",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"check": "tsc -p tsconfig.json --noEmit"
},
"keywords": [
"tailwindcss",
"tailwind",
"plugin",
"dashed-border",
"border"
],
"peerDependencies": {
"tailwindcss": "^4.0.0"
},
"devDependencies": {
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3"
},
"publishConfig": {
"access": "public"
},
"license": "MIT"
}
+76
View File
@@ -0,0 +1,76 @@
diff --git a/dist/plugin.d.mts b/dist/plugin.d.mts
index 5c32fa73db5d471048e566b1c676a4225805d39a..77b8cbfe7d79bce396eb77537baa657be31af32d 100644
--- a/dist/plugin.d.mts
+++ b/dist/plugin.d.mts
@@ -1,6 +1,6 @@
-export { P as PluginUtils } from './resolve-config-QUZ9b-Gn.mjs';
+export type { P as PluginUtils, N as NamedUtilityValue } from './resolve-config-QUZ9b-Gn.mjs';
import { a as PluginFn, C as Config, b as PluginWithConfig, c as PluginWithOptions } from './types-CJYAW1ql.mjs';
-export { d as PluginAPI, P as PluginsConfig, T as ThemeConfig } from './types-CJYAW1ql.mjs';
+export type { d as PluginAPI, P as PluginsConfig, T as ThemeConfig, MatchOptions, BareValueResolver, MatchFn } from './types-CJYAW1ql.mjs';
import './colors.mjs';
declare function createPlugin(handler: PluginFn, config?: Partial<Config>): PluginWithConfig;
@@ -8,4 +8,4 @@ declare namespace createPlugin {
var withOptions: <T>(pluginFunction: (options?: T) => PluginFn, configFunction?: (options?: T) => Partial<Config>) => PluginWithOptions<T>;
}
-export { Config, PluginFn as PluginCreator, createPlugin as default };
+export { type Config, type PluginFn, createPlugin as default };
diff --git a/dist/types-CJYAW1ql.d.mts b/dist/types-CJYAW1ql.d.mts
index 6d5ca4a855759cbd60007c69d072bdd7b8710e77..2505e2accb9e9b01de53c9dbb0b27c14abb252e1 100644
--- a/dist/types-CJYAW1ql.d.mts
+++ b/dist/types-CJYAW1ql.d.mts
@@ -56,31 +56,26 @@ type PluginAPI = {
}): number;
}): void;
addUtilities(utilities: Record<string, CssInJs | CssInJs[]> | Record<string, CssInJs | CssInJs[]>[], options?: {}): void;
- matchUtilities(utilities: Record<string, (value: string, extra: {
- modifier: string | null;
- }) => CssInJs | CssInJs[]>, options?: Partial<{
- type: string | string[];
- supportsNegativeValues: boolean;
- values: Record<string, string> & {
- __BARE_VALUE__?: (value: NamedUtilityValue) => string | undefined;
- };
- modifiers: 'any' | Record<string, string>;
- }>): void;
+ matchUtilities(utilities: Record<string, MatchFn>, options?: MatchOptions): void;
addComponents(utilities: Record<string, CssInJs> | Record<string, CssInJs>[], options?: {}): void;
- matchComponents(utilities: Record<string, (value: string, extra: {
- modifier: string | null;
- }) => CssInJs>, options?: Partial<{
- type: string | string[];
- supportsNegativeValues: boolean;
- values: Record<string, string> & {
- __BARE_VALUE__?: (value: NamedUtilityValue) => string | undefined;
- };
- modifiers: 'any' | Record<string, string>;
- }>): void;
+ matchComponents(utilities: Record<string, MatchFn>, options?: MatchOptions): void;
theme(path: string, defaultValue?: any): any;
config(path?: string, defaultValue?: any): any;
prefix(className: string): string;
};
+
+type MatchFn = (value: string, extra: { modifier: string | null; }) => CssInJs | CssInJs[]
+
+type MatchOptions = Partial<{
+ type: string | string[];
+ supportsNegativeValues: boolean;
+ values: Record<string, string> & {
+ __BARE_VALUE__?: BareValueResolver;
+ };
+ modifiers: 'any' | Record<string, string>;
+}>
+type BareValueResolver = (value: NamedUtilityValue) => string | undefined
+
type CssInJs = {
[key: string]: string | string[] | CssInJs | CssInJs[];
};
@@ -125,4 +120,4 @@ interface UserConfig {
experimental?: 'all' | Record<string, boolean>;
}
-export type { Config as C, Plugin as P, SourceLocation as S, ThemeConfig as T, UserConfig as U, PluginFn as a, PluginWithConfig as b, PluginWithOptions as c, PluginAPI as d };
+export type { Config as C, Plugin as P, SourceLocation as S, ThemeConfig as T, UserConfig as U, PluginFn as a, PluginWithConfig as b, PluginWithOptions as c, PluginAPI as d, MatchOptions, BareValueResolver, MatchFn };
+2
View File
@@ -0,0 +1,2 @@
patchedDependencies:
tailwindcss: patches/tailwindcss.patch
+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
+357
View File
@@ -0,0 +1,357 @@
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;
+217
View File
@@ -0,0 +1,217 @@
import plugin from "tailwindcss/plugin";
interface ThemeValueMap {
[key: string]: string | ThemeValueMap;
}
const DEFAULT_SPACING_VALUE = "0.25rem";
const DEFAULT_GAP = `calc(var(--spacing, ${DEFAULT_SPACING_VALUE}) * 2)`;
const DEFAULT_LENGTH = `calc(var(--spacing, ${DEFAULT_SPACING_VALUE}) * 3)`;
const DEFAULT_BORDER_WIDTH = "1px";
const DEFAULT_OFFSET = "0px";
const DEFAULT_COLOR = "currentColor";
const DEFAULT_RADIUS = "0px";
const DEFAULT_ALIGN = "calc(var(--tw-dashed-border-width) / 2)";
const dashedBorderAlign = "var(--tw-dashed-border-align)";
const svgRadius = `max(0px, calc(var(--tw-dashed-border-radius) - (${dashedBorderAlign})))`;
const dashedBorderTransitionProperties =
"stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx, x, y, width, height";
const flattenThemeMap = (
input: ThemeValueMap,
path: string[] = [],
): Record<string, string> => {
const output: Record<string, string> = {};
for (const [key, value] of Object.entries(input)) {
const nextPath = key === "DEFAULT" ? path : [...path, key];
if (typeof value === "string") {
output[nextPath.join("-")] = value;
continue;
}
Object.assign(output, flattenThemeMap(value, nextPath));
}
return output;
};
const roundedSelector = (name: string): string =>
name === "DEFAULT" ? ".rounded" : `.rounded-${name}`;
const resolveAlignValue = (value: string): string =>
/^-?(?:\d+|\d*\.\d+)$/.test(value)
? `calc((0.5 - ${value}) * var(--tw-dashed-border-width))`
: `calc((var(--tw-dashed-border-width) / 2) - (${value}))`;
const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
({ addBase, addUtilities, matchUtilities, theme }) => {
addBase({
"@property --tw-dashed-border-gap": {
syntax: "<length-percentage>",
inherits: "true",
"initial-value": "0.5rem",
},
"@property --tw-dashed-border-length": {
syntax: "<length-percentage>",
inherits: "true",
"initial-value": "0.75rem",
},
"@property --tw-dashed-border-offset": {
syntax: "<length-percentage>",
inherits: "true",
"initial-value": DEFAULT_OFFSET,
},
"@property --tw-dashed-border-width": {
syntax: "<length>",
inherits: "true",
"initial-value": DEFAULT_BORDER_WIDTH,
},
"@property --tw-dashed-border-align": {
syntax: "<length-percentage>",
inherits: "true",
"initial-value": DEFAULT_ALIGN,
},
"@property --tw-dashed-border-radius": {
syntax: "<length-percentage>",
inherits: "true",
"initial-value": DEFAULT_RADIUS,
},
});
addUtilities({
".dashed-border": {
"--tw-dashed-border-gap": DEFAULT_GAP,
"--tw-dashed-border-length": DEFAULT_LENGTH,
"--tw-dashed-border-offset": DEFAULT_OFFSET,
"--tw-dashed-border-width": DEFAULT_BORDER_WIDTH,
"--tw-dashed-border-color": DEFAULT_COLOR,
"--tw-dashed-border-align": DEFAULT_ALIGN,
"--tw-dashed-border-radius": DEFAULT_RADIUS,
position: "relative",
},
".dashed-border-svg": {
display: "block",
position: "absolute",
inset: "0",
width: "100%",
height: "100%",
overflow: "hidden",
pointerEvents: "none",
},
".dashed-border-rect": {
fill: "none",
stroke: "var(--tw-dashed-border-color)",
strokeWidth: "var(--tw-dashed-border-width)",
strokeDasharray:
"var(--tw-dashed-border-length) var(--tw-dashed-border-gap)",
strokeDashoffset: "var(--tw-dashed-border-offset)",
shapeRendering: "geometricPrecision",
vectorEffect: "non-scaling-stroke",
x: dashedBorderAlign,
y: dashedBorderAlign,
width: `calc(100% - (${dashedBorderAlign} * 2))`,
height: `calc(100% - (${dashedBorderAlign} * 2))`,
rx: svgRadius,
},
".dashed-border-transition, .dashed-border.transition, .dashed-border.transition-all":
{
transitionTimingFunction:
"var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1))",
transitionDuration: "var(--default-transition-duration, 150ms)",
},
".dashed-border.transition-colors": {
transitionTimingFunction:
"var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1))",
transitionDuration: "var(--default-transition-duration, 150ms)",
},
".dashed-border-transition .dashed-border-rect, .dashed-border.transition .dashed-border-rect, .dashed-border.transition-all .dashed-border-rect":
{
transitionProperty: dashedBorderTransitionProperties,
transitionTimingFunction: "inherit",
transitionDuration: "inherit",
},
".dashed-border.transition-colors .dashed-border-rect": {
transitionProperty: "stroke",
transitionTimingFunction: "inherit",
transitionDuration: "inherit",
},
});
const borderWidths = theme("borderWidth", {
DEFAULT: DEFAULT_BORDER_WIDTH,
}) as Record<string, string>;
const colors = flattenThemeMap(theme("colors", {}) as ThemeValueMap);
const radii = theme("borderRadius", {
DEFAULT: DEFAULT_RADIUS,
}) as Record<string, string>;
matchUtilities(
{
"dashed-border-gap": (value: string) => ({
"--tw-dashed-border-gap": value,
}),
"dashed-border-length": (value: string) => ({
"--tw-dashed-border-length": value,
}),
"dashed-border-offset": (value: string) => ({
"--tw-dashed-border-offset": value,
}),
"dashed-border-align": (value: string) => ({
"--tw-dashed-border-align": resolveAlignValue(value),
}),
"dashed-border-radius": (value: string) => ({
"--tw-dashed-border-radius": value,
}),
},
{
values: {
inside: DEFAULT_ALIGN,
edge: "0px",
outside: "calc(var(--tw-dashed-border-width) / -2)",
},
},
);
matchUtilities(
{
"dashed-border-width": (value: string) => ({
"--tw-dashed-border-width": value,
}),
},
{
values: borderWidths,
},
);
matchUtilities(
{
"dashed-border-color": (value: string) => ({
"--tw-dashed-border-color": value,
}),
},
{
values: {
...colors,
current: "currentColor",
inherit: "inherit",
transparent: "transparent",
},
},
);
addUtilities(
Object.fromEntries(
Object.entries(radii).map(([name, value]) => [
`.dashed-border${roundedSelector(name)}`,
{
"--tw-dashed-border-radius": value,
},
]),
),
);
},
);
export default dashedBorderPlugin;
+210
View File
@@ -0,0 +1,210 @@
import plugin from "tailwindcss/plugin";
interface ThemeValueMap {
[key: string]: string | ThemeValueMap;
}
const DEFAULT_SPACING_VALUE = "0.25rem";
const DEFAULT_GAP = `calc(var(--spacing, ${DEFAULT_SPACING_VALUE}) * 2)`;
const DEFAULT_LENGTH = `calc(var(--spacing, ${DEFAULT_SPACING_VALUE}) * 3)`;
const DEFAULT_BORDER_WIDTH = "1px";
const DEFAULT_OFFSET = "0px";
const DEFAULT_COLOR = "currentColor";
const DEFAULT_RADIUS = "0px";
const DEFAULT_ALIGNMENT = "0%";
const dashedBorderInset =
"calc((0.5 - (var(--tw-dashed-border-alignment) / 100%)) * var(--tw-dashed-border-width))";
const svgRadius = `max(0px, calc(var(--tw-dashed-border-radius) - (${dashedBorderInset})))`;
const dashedBorderTransitionProperties =
"stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx, x, y, width, height";
const flattenThemeMap = (
input: ThemeValueMap,
path: string[] = [],
): Record<string, string> => {
const output: Record<string, string> = {};
for (const [key, value] of Object.entries(input)) {
const nextPath = key === "DEFAULT" ? path : [...path, key];
if (typeof value === "string") {
output[nextPath.join("-")] = value;
continue;
}
Object.assign(output, flattenThemeMap(value, nextPath));
}
return output;
};
const roundedSelector = (name: string): string =>
name === "DEFAULT" ? ".rounded" : `.rounded-${name}`;
const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
({ addBase, addUtilities, matchUtilities, theme }) => {
addBase({
"@property --tw-dashed-border-gap": {
syntax: "<length-percentage>",
inherits: "true",
"initial-value": "0.5rem",
},
"@property --tw-dashed-border-length": {
syntax: "<length-percentage>",
inherits: "true",
"initial-value": "0.75rem",
},
"@property --tw-dashed-border-offset": {
syntax: "<length-percentage>",
inherits: "true",
"initial-value": DEFAULT_OFFSET,
},
"@property --tw-dashed-border-width": {
syntax: "<length>",
inherits: "true",
"initial-value": DEFAULT_BORDER_WIDTH,
},
"@property --tw-dashed-border-alignment": {
syntax: "<percentage>",
inherits: "true",
"initial-value": DEFAULT_ALIGNMENT,
},
"@property --tw-dashed-border-radius": {
syntax: "<length-percentage>",
inherits: "true",
"initial-value": DEFAULT_RADIUS,
},
});
addUtilities({
".dashed-border": {
"--tw-dashed-border-gap": DEFAULT_GAP,
"--tw-dashed-border-length": DEFAULT_LENGTH,
"--tw-dashed-border-offset": DEFAULT_OFFSET,
"--tw-dashed-border-width": DEFAULT_BORDER_WIDTH,
"--tw-dashed-border-color": DEFAULT_COLOR,
"--tw-dashed-border-alignment": DEFAULT_ALIGNMENT,
"--tw-dashed-border-radius": DEFAULT_RADIUS,
position: "relative",
},
".dashed-border-svg": {
display: "block",
position: "absolute",
inset: "0",
width: "100%",
height: "100%",
overflow: "hidden",
pointerEvents: "none",
},
".dashed-border-rect": {
fill: "none",
stroke: "var(--tw-dashed-border-color)",
strokeWidth: "var(--tw-dashed-border-width)",
strokeDasharray:
"var(--tw-dashed-border-length) var(--tw-dashed-border-gap)",
strokeDashoffset: "var(--tw-dashed-border-offset)",
shapeRendering: "geometricPrecision",
vectorEffect: "non-scaling-stroke",
x: dashedBorderInset,
y: dashedBorderInset,
width: `calc(100% - (${dashedBorderInset} * 2))`,
height: `calc(100% - (${dashedBorderInset} * 2))`,
rx: svgRadius,
},
".dashed-border-transition, .dashed-border.transition, .dashed-border.transition-all":
{
transitionTimingFunction:
"var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1))",
transitionDuration: "var(--default-transition-duration, 150ms)",
},
".dashed-border.transition-colors": {
transitionTimingFunction:
"var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1))",
transitionDuration: "var(--default-transition-duration, 150ms)",
},
".dashed-border-transition .dashed-border-rect, .dashed-border.transition .dashed-border-rect, .dashed-border.transition-all .dashed-border-rect":
{
transitionProperty: dashedBorderTransitionProperties,
transitionTimingFunction: "inherit",
transitionDuration: "inherit",
},
".dashed-border.transition-colors .dashed-border-rect": {
transitionProperty: "stroke",
transitionTimingFunction: "inherit",
transitionDuration: "inherit",
},
});
const spacing = theme("spacing", {}) as Record<string, string>;
const borderWidths = theme("borderWidth", {
DEFAULT: DEFAULT_BORDER_WIDTH,
}) as Record<string, string>;
const colors = flattenThemeMap(theme("colors", {}) as ThemeValueMap);
const radii = theme("borderRadius", {
DEFAULT: DEFAULT_RADIUS,
}) as Record<string, string>;
matchUtilities(
{
"dashed-border-gap": (value: string) => ({
"--tw-dashed-border-gap": value,
}),
"dashed-border-length": (value: string) => ({
"--tw-dashed-border-length": value,
}),
"dashed-border-offset": (value: string) => ({
"--tw-dashed-border-offset": value,
}),
"dashed-border-align": (value: string) => ({
"--tw-dashed-border-alignment": value,
}),
"dashed-border-radius": (value: string) => ({
"--tw-dashed-border-radius": value,
}),
},
{
values: spacing,
},
);
matchUtilities(
{
"dashed-border-width": (value: string) => ({
"--tw-dashed-border-width": value,
}),
},
{
values: borderWidths,
},
);
matchUtilities(
{
"dashed-border-color": (value: string) => ({
"--tw-dashed-border-color": value,
}),
},
{
values: {
...colors,
current: "currentColor",
inherit: "inherit",
transparent: "transparent",
},
},
);
addUtilities(
Object.fromEntries(
Object.entries(radii).map(([name, value]) => [
`.dashed-border${roundedSelector(name)}`,
{
"--tw-dashed-border-radius": value,
},
]),
),
);
},
);
export default dashedBorderPlugin;
+33
View File
@@ -0,0 +1,33 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { createRequire } from 'node:module'
import plugin from 'tailwindcss/plugin'
import { compile } from 'tailwindcss'
import flattenColorPalette from 'tailwindcss/lib/util/flattenColorPalette'
const require = createRequire(import.meta.url)
const pluginTest = plugin(({ theme, config }) => {
const raw = theme('colors')
const flat = flattenColorPalette(raw ?? {})
const random_fn = theme("backgroundOpacity.__BARE_VALUE__")
const random_fn = theme("backgroundOpacity.__BARE_VALUE__")
console.log('default bare function at 80', random_fn, random_fn({value: 80, fraction: null}));
})
await compile('@import "tailwindcss"; @plugin "inspect-plugin";', {
base: process.cwd(),
async loadModule(id, base) {
if (id === 'inspect-plugin') {
return { path: 'inspect-plugin', base, module: pluginTest }
}
},
async loadStylesheet(id, base) {
if (id === 'tailwindcss') {
const pkg = path.dirname(require.resolve('tailwindcss/package.json'))
const file = path.join(pkg, 'index.css')
return { path: file, base: path.dirname(file), content: await fs.readFile(file, 'utf8') }
}
const resolved = path.resolve(base, id)
return { path: resolved, base: path.dirname(resolved), content: await fs.readFile(resolved, 'utf8') }
},
})
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": [
"src"
]
}