From 1acf0ce9cdc8cd39d15af3652f3addb1d896c6df Mon Sep 17 00:00:00 2001 From: Mira Date: Wed, 15 Apr 2026 14:27:39 +0300 Subject: [PATCH] attempt from scratch --- src/index.ts | 217 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/index.ts diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7c213a2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,217 @@ +import plugin from "tailwindcss/plugin"; +import flattenColorPalette from "tailwindcss/lib/util/flattenColorPalette"; + +const DEFAULT_GAP = "calc(var(--spacing, 0.25rem) * 2)"; +const DEFAULT_LENGTH = "calc(var(--spacing, 0.25rem) * 3)"; +const DEFAULT_OFFSET = "0px"; +const DEFAULT_WIDTH = "1px"; +const DEFAULT_COLOR = "currentColor"; +const DEFAULT_RADIUS = "0px"; + +const DASH_ARRAY = "var(--tw-dashed-border-length) var(--tw-dashed-border-gap)"; +const INSET = "calc(var(--tw-dashed-border-width) / 2)"; +const INNER_SIZE = "calc(100% - var(--tw-dashed-border-width))"; +const ADJUSTED_RADIUS = + "max(0px, calc(var(--tw-dashed-border-radius) - (var(--tw-dashed-border-width) / 2)))"; + +const ALL_TRANSITION_PROPERTIES = + "stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx"; +const COLOR_TRANSITION_PROPERTIES = "stroke"; +const TRANSITION_DURATION = + "var(--tw-duration, var(--default-transition-duration))"; +const TRANSITION_TIMING_FUNCTION = + "var(--tw-ease, var(--default-transition-timing-function))"; + +type ThemeLeafMap = Record; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function flattenThemeLeaves( + value: unknown, + path: string[] = [], +): ThemeLeafMap { + if (!isRecord(value)) { + return {}; + } + + const flattened: ThemeLeafMap = {}; + + for (const [key, entry] of Object.entries(value)) { + if (key === "__CSS_VALUES__") { + continue; + } + + const nextPath = key === "DEFAULT" ? path : [...path, key]; + + if (isRecord(entry)) { + Object.assign(flattened, flattenThemeLeaves(entry, nextPath)); + continue; + } + + if (typeof entry === "string" || typeof entry === "number") { + const flattenedKey = nextPath.join("-"); + flattened[flattenedKey === "" ? "DEFAULT" : flattenedKey] = String(entry); + } + } + + return flattened; +} + +function omitDefaultKey(values: ThemeLeafMap): ThemeLeafMap { + const { DEFAULT: _default, ...rest } = values; + return rest; +} + +function createTransitionStyles(properties: string) { + return { + "transition-property": properties, + "transition-duration": TRANSITION_DURATION, + "transition-timing-function": TRANSITION_TIMING_FUNCTION, + }; +} + +const dashedBorderPlugin: ReturnType = plugin( + ({ addBase, addUtilities, matchUtilities, theme }) => { + const spacingValues = flattenThemeLeaves(theme("spacing")); + const borderWidthValues = omitDefaultKey( + flattenThemeLeaves(theme("borderWidth")), + ); + const borderRadiusValues = flattenThemeLeaves(theme("borderRadius")); + const colorValues = { + ...flattenColorPalette(theme("colors") ?? {}), + current: "currentColor", + inherit: "inherit", + transparent: "transparent", + }; + + addBase({ + "@property --tw-dashed-border-gap": { + syntax: "", + inherits: "true", + "initial-value": DEFAULT_GAP, + }, + "@property --tw-dashed-border-length": { + syntax: "", + inherits: "true", + "initial-value": DEFAULT_LENGTH, + }, + "@property --tw-dashed-border-offset": { + syntax: "", + inherits: "true", + "initial-value": DEFAULT_OFFSET, + }, + "@property --tw-dashed-border-width": { + syntax: "", + inherits: "true", + "initial-value": DEFAULT_WIDTH, + }, + "@property --tw-dashed-border-radius": { + syntax: "", + inherits: "true", + "initial-value": DEFAULT_RADIUS, + }, + ".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-radius": DEFAULT_RADIUS, + }, + ".dashed-border-svg": { + position: "absolute", + inset: "0", + width: "100%", + height: "100%", + overflow: "hidden", + "pointer-events": "none", + }, + ".dashed-border-rect": { + fill: "none", + stroke: "var(--tw-dashed-border-color)", + "stroke-width": "var(--tw-dashed-border-width)", + "stroke-dasharray": DASH_ARRAY, + "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, + }, + }); + + matchUtilities( + { + "dashed-border-gap": (value) => ({ + "--tw-dashed-border-gap": value, + }), + "dashed-border-length": (value) => ({ + "--tw-dashed-border-length": value, + }), + "dashed-border-offset": (value) => ({ + "--tw-dashed-border-offset": value, + }), + }, + { + type: "length", + values: spacingValues, + }, + ); + + matchUtilities( + { + "dashed-border-width": (value) => ({ + "--tw-dashed-border-width": value, + }), + }, + { + type: "length", + values: borderWidthValues, + }, + ); + + matchUtilities( + { + "dashed-border-color": (value) => ({ + "--tw-dashed-border-color": value, + }), + }, + { + type: "color", + values: colorValues, + }, + ); + + const radiusUtilities = Object.fromEntries( + Object.entries(borderRadiusValues).map(([key, value]) => { + const roundedClass = key === "DEFAULT" ? "rounded" : `rounded-${key}`; + return [ + `.dashed-border.${roundedClass}`, + { + "--tw-dashed-border-radius": value, + }, + ]; + }), + ); + + addUtilities({ + ...radiusUtilities, + ".dashed-border-transition .dashed-border-rect": createTransitionStyles( + ALL_TRANSITION_PROPERTIES, + ), + ".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(COLOR_TRANSITION_PROPERTIES), + }); + }, +); + +export default dashedBorderPlugin;