From c64db01bb44ddc52b17c297a104ca7243bb709f7 Mon Sep 17 00:00:00 2001 From: Mira Date: Wed, 15 Apr 2026 14:16:05 +0300 Subject: [PATCH] Everything broken... --- TECHNICAL_DESCRIPTION.md | 306 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 75 ++++++---- 2 files changed, 352 insertions(+), 29 deletions(-) create mode 100644 TECHNICAL_DESCRIPTION.md diff --git a/TECHNICAL_DESCRIPTION.md b/TECHNICAL_DESCRIPTION.md new file mode 100644 index 0000000..ea8a723 --- /dev/null +++ b/TECHNICAL_DESCRIPTION.md @@ -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 `` 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 +
+ + + +
+``` + +## 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 `` 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: ; +``` + +## 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 +
+``` + +### 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 +
+``` + +### 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 +
+``` + +### 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 +
+``` + +### 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 +
+``` + +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 +
+``` + +This means the expected way to apply radius is: + +```html +
+``` + +not: + +```html +
+``` + +## 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 +
+``` + +## 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 `` 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`. diff --git a/src/index.ts b/src/index.ts index 1c8b9e2..9ec0314 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,11 +11,14 @@ 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 svgRadius = + "max(0px, calc(var(--tw-dashed-border-radius) - (var(--tw-dashed-border-width) / 2)))"; const dashedBorderTransitionProperties = - "stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx, x, y, width, height"; + "stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx"; +const strokeDasharrayValue = ( + length: string, + gap: string, +): string => `${length} max(${gap}, 0.001px)`; const flattenThemeMap = ( input: ThemeValueMap, @@ -40,11 +43,6 @@ const flattenThemeMap = ( 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 = plugin( ({ addBase, addUtilities, matchUtilities, theme }) => { addBase({ @@ -68,11 +66,6 @@ const dashedBorderPlugin: ReturnType = plugin( inherits: "true", "initial-value": DEFAULT_BORDER_WIDTH, }, - "@property --tw-dashed-border-align": { - syntax: "", - inherits: "true", - "initial-value": DEFAULT_ALIGN, - }, "@property --tw-dashed-border-radius": { syntax: "", inherits: "true", @@ -87,7 +80,6 @@ const dashedBorderPlugin: ReturnType = plugin( "--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", }, @@ -104,15 +96,17 @@ const dashedBorderPlugin: ReturnType = plugin( 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)", + strokeDasharray: strokeDasharrayValue( + "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))`, + x: "calc(var(--tw-dashed-border-width) / 2)", + y: "calc(var(--tw-dashed-border-width) / 2)", + width: "calc(100% - var(--tw-dashed-border-width))", + height: "calc(100% - var(--tw-dashed-border-width))", rx: svgRadius, }, ".dashed-border-transition, .dashed-border.transition, .dashed-border.transition-all": @@ -139,6 +133,7 @@ const dashedBorderPlugin: ReturnType = plugin( }, }); + const spacing = theme("spacing", {}) as Record; const borderWidths = theme("borderWidth", { DEFAULT: DEFAULT_BORDER_WIDTH, }) as Record; @@ -151,26 +146,37 @@ const dashedBorderPlugin: ReturnType = plugin( { "dashed-border-gap": (value: string) => ({ "--tw-dashed-border-gap": value, + "& .dashed-border-rect": { + strokeDasharray: strokeDasharrayValue( + "var(--tw-dashed-border-length)", + value, + ), + }, }), "dashed-border-length": (value: string) => ({ "--tw-dashed-border-length": value, + "& .dashed-border-rect": { + strokeDasharray: strokeDasharrayValue( + value, + "var(--tw-dashed-border-gap)", + ), + }, }), "dashed-border-offset": (value: string) => ({ "--tw-dashed-border-offset": value, - }), - "dashed-border-align": (value: string) => ({ - "--tw-dashed-border-align": resolveAlignValue(value), + "& .dashed-border-rect": { + strokeDashoffset: value, + }, }), "dashed-border-radius": (value: string) => ({ "--tw-dashed-border-radius": value, + "& .dashed-border-rect": { + rx: `max(0px, calc(${value} - (var(--tw-dashed-border-width) / 2)))`, + }, }), }, { - values: { - inside: DEFAULT_ALIGN, - edge: "0px", - outside: "calc(var(--tw-dashed-border-width) / -2)", - }, + values: spacing, }, ); @@ -178,6 +184,14 @@ const dashedBorderPlugin: ReturnType = plugin( { "dashed-border-width": (value: string) => ({ "--tw-dashed-border-width": value, + "& .dashed-border-rect": { + strokeWidth: value, + x: `calc(${value} / 2)`, + y: `calc(${value} / 2)`, + width: `calc(100% - ${value})`, + height: `calc(100% - ${value})`, + rx: `max(0px, calc(var(--tw-dashed-border-radius) - (${value} / 2)))`, + }, }), }, { @@ -189,6 +203,9 @@ const dashedBorderPlugin: ReturnType = plugin( { "dashed-border-color": (value: string) => ({ "--tw-dashed-border-color": value, + "& .dashed-border-rect": { + stroke: value, + }, }), }, {