Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e524c5046a | |||
| 0b807644b1 | |||
| 2ce7a39c1a | |||
| 0a25911ba9 | |||
| 4c36674c60 | |||
| 1acf0ce9cd | |||
| fe93824063 | |||
| c64db01bb4 | |||
| e4c91c4a93 | |||
| dd4ab29df2 |
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
.direnv
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
patchedDependencies:
|
||||||
|
tailwindcss: patches/tailwindcss.patch
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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') }
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user