From 2158f2c01c6192273ed784eca7a5437439c40fbf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 05:43:15 +0000 Subject: [PATCH 01/10] feat: add squircle-none and squircle-full static utilities Defined the same way Tailwind defines rounded-none and rounded-full: static utilities with 0 and calc(infinity * 1px) registered alongside each variant root, for both the generated CSS and the JS plugin. --- README.md | 211 ++++++++++++++++++++++- package/scripts/generate-squircle-css.ts | 27 ++- package/src/squircle-css.test.ts | 28 +++ package/src/tailwind.test.ts | 40 +++++ package/src/tailwind.ts | 19 +- package/src/variants.ts | 4 + 6 files changed, 319 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index db74c31..8cab00a 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,8 @@ const twMerge = extendTailwindMerge(squircleMergeConfig, { Values are validated strictly so typos fail at build time instead of producing invalid CSS: -- **`squircle-*` and its variants** accept the same theme values as `rounded-*` (`sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, `full`, plus anything you add to `@theme`) and arbitrary lengths like `squircle-[16px]`. Non-length arbitraries (`[50%]`, `[foo]`) and paren refs (`squircle-(--my-radius)`) are rejected — use a theme key instead. +- **`squircle-*` and its variants** accept the same theme values as `rounded-*` (`sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, plus anything you add to `@theme`) and arbitrary lengths like `squircle-[16px]`. Non-length arbitraries (`[50%]`, `[foo]`) and paren refs (`squircle-(--my-radius)`) are rejected — use a theme key instead. +- **`squircle-none` and `squircle-full`** (and their side/corner variants) are static utilities defined the same way as `rounded-none` and `rounded-full`: `0` and `calc(infinity * 1px)`. `squircle-full` gets the superellipse treatment; `squircle-none` is a plain zero radius, since there's no corner left to shape. - **`squircle-amt-*`** accepts bare numbers (`squircle-amt-2`), arbitrary numbers (`squircle-amt-[3.5]`), and theme values. Unit-bearing arbitraries (`[1em]`) and paren refs (`(--my-amt)`) are rejected. ### What does `squircle-amt-*` control? @@ -596,7 +597,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand /* ── Squircle utilities ─────────────────────────────────────── */ /* squircle-amt-[n] sets the superellipse amount (default 2) */ -/* squircle-* mirrors rounded-* variants: all, t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */ +/* squircle-* mirrors rounded-* variants: t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */ +/* squircle-*-none and squircle-*-full are static, matching rounded-none and rounded-full */ @utility squircle-amt-* { --squircle-amt: --value(--squircle-amt-*, number, [number]); @@ -605,6 +607,19 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-none { + border-radius: 0; +} + +@utility squircle-full { + border-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-radius: var(--squircle-r); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-* { border-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { @@ -616,6 +631,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand /* --- Per-side physical variants --- */ +@utility squircle-t-none { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +@utility squircle-t-full { + border-top-left-radius: calc(infinity * 1px); + border-top-right-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-top-left-radius: var(--squircle-r); + border-top-right-radius: var(--squircle-r); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-t-* { border-top-left-radius: --value(--radius-*, [length]); border-top-right-radius: --value(--radius-*, [length]); @@ -627,6 +658,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-r-none { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +@utility squircle-r-full { + border-top-right-radius: calc(infinity * 1px); + border-bottom-right-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-top-right-radius: var(--squircle-r); + border-bottom-right-radius: var(--squircle-r); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-r-* { border-top-right-radius: --value(--radius-*, [length]); border-bottom-right-radius: --value(--radius-*, [length]); @@ -638,6 +685,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-b-none { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +@utility squircle-b-full { + border-bottom-left-radius: calc(infinity * 1px); + border-bottom-right-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-bottom-left-radius: var(--squircle-r); + border-bottom-right-radius: var(--squircle-r); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-b-* { border-bottom-left-radius: --value(--radius-*, [length]); border-bottom-right-radius: --value(--radius-*, [length]); @@ -649,6 +712,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-l-none { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +@utility squircle-l-full { + border-top-left-radius: calc(infinity * 1px); + border-bottom-left-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-top-left-radius: var(--squircle-r); + border-bottom-left-radius: var(--squircle-r); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-l-* { border-top-left-radius: --value(--radius-*, [length]); border-bottom-left-radius: --value(--radius-*, [length]); @@ -662,6 +741,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand /* --- Per-side logical variants --- */ +@utility squircle-s-none { + border-start-start-radius: 0; + border-end-start-radius: 0; +} + +@utility squircle-s-full { + border-start-start-radius: calc(infinity * 1px); + border-end-start-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-start-start-radius: var(--squircle-r); + border-end-start-radius: var(--squircle-r); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-s-* { border-start-start-radius: --value(--radius-*, [length]); border-end-start-radius: --value(--radius-*, [length]); @@ -673,6 +768,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-e-none { + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +@utility squircle-e-full { + border-start-end-radius: calc(infinity * 1px); + border-end-end-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-start-end-radius: var(--squircle-r); + border-end-end-radius: var(--squircle-r); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-e-* { border-start-end-radius: --value(--radius-*, [length]); border-end-end-radius: --value(--radius-*, [length]); @@ -686,6 +797,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand /* --- Per-corner physical variants --- */ +@utility squircle-tl-none { + border-top-left-radius: 0; +} + +@utility squircle-tl-full { + border-top-left-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + border-top-left-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-tl-* { border-top-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { @@ -694,6 +817,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-tr-none { + border-top-right-radius: 0; +} + +@utility squircle-tr-full { + border-top-right-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + border-top-right-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-tr-* { border-top-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { @@ -702,6 +837,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-br-none { + border-bottom-right-radius: 0; +} + +@utility squircle-br-full { + border-bottom-right-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + border-bottom-right-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-br-* { border-bottom-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { @@ -710,6 +857,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-bl-none { + border-bottom-left-radius: 0; +} + +@utility squircle-bl-full { + border-bottom-left-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + border-bottom-left-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-bl-* { border-bottom-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { @@ -720,6 +879,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand /* --- Per-corner logical variants --- */ +@utility squircle-ss-none { + border-start-start-radius: 0; +} + +@utility squircle-ss-full { + border-start-start-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + border-start-start-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-ss-* { border-start-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { @@ -728,6 +899,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-se-none { + border-start-end-radius: 0; +} + +@utility squircle-se-full { + border-start-end-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + border-start-end-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-se-* { border-start-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { @@ -736,6 +919,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-es-none { + border-end-start-radius: 0; +} + +@utility squircle-es-full { + border-end-start-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + border-end-start-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-es-* { border-end-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { @@ -744,6 +939,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } } +@utility squircle-ee-none { + border-end-end-radius: 0; +} + +@utility squircle-ee-full { + border-end-end-radius: calc(infinity * 1px); + @supports (corner-shape: superellipse(2)) { + border-end-end-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + corner-shape: superellipse(var(--squircle-amt, 2)); + } +} + @utility squircle-ee-* { border-end-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { diff --git a/package/scripts/generate-squircle-css.ts b/package/scripts/generate-squircle-css.ts index 996e05a..9f30afb 100644 --- a/package/scripts/generate-squircle-css.ts +++ b/package/scripts/generate-squircle-css.ts @@ -1,7 +1,16 @@ import { copyFileSync, mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { DEFAULT_AMT, SUPPORTS_RULE, VARIANTS, isComment, squircleCssObj } from "../src/variants"; +import { + DEFAULT_AMT, + FULL_RADIUS, + NONE_RADIUS, + SUPPORTS_RULE, + VARIANTS, + isComment, + squircleCssObj, + type CssLikeObject, +} from "../src/variants"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -9,8 +18,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // https://tailwindcss.com/docs/adding-custom-styles#functional-utilities const value = "--value(--radius-*, [length])"; -function renderUtility(name: string, props: string[]): string { - const obj = squircleCssObj(props, value); +function renderUtility(name: string, obj: CssLikeObject): string { const lines: string[] = [`@utility ${name} {`]; for (const [key, val] of Object.entries(obj)) { @@ -40,7 +48,8 @@ function generateCss(): string { blocks.push(`\ /* ── Squircle utilities ─────────────────────────────────────── */ /* squircle-amt-[n] sets the superellipse amount (default ${DEFAULT_AMT}) */ -/* squircle-* mirrors rounded-* variants: all, t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */ +/* squircle-* mirrors rounded-* variants: t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */ +/* squircle-*-none and squircle-*-full are static, matching rounded-none and rounded-full */ @utility squircle-amt-* { --squircle-amt: --value(--squircle-amt-*, number, [number]); @@ -55,8 +64,14 @@ function generateCss(): string { continue; } - const name = suffix ? `squircle-${suffix}-*` : "squircle-*"; - blocks.push(renderUtility(name, entry)); + const root = suffix ? `squircle-${suffix}` : "squircle"; + // Static -none/-full first, then the functional utility — the same shape + // Tailwind uses for rounded-none/rounded-full. -none needs no correction. + blocks.push( + renderUtility(`${root}-none`, Object.fromEntries(entry.map((p) => [p, NONE_RADIUS]))), + ); + blocks.push(renderUtility(`${root}-full`, squircleCssObj(entry, FULL_RADIUS))); + blocks.push(renderUtility(`${root}-*`, squircleCssObj(entry, value))); } return blocks.join("\n\n") + "\n"; diff --git a/package/src/squircle-css.test.ts b/package/src/squircle-css.test.ts index e165a51..7e856a0 100644 --- a/package/src/squircle-css.test.ts +++ b/package/src/squircle-css.test.ts @@ -50,6 +50,34 @@ describe("squircle.css utilities", () => { }); } + describe("static none/full utilities (matching rounded-none/rounded-full)", () => { + it("squircle-full uses calc(infinity * 1px) with correction", async () => { + const css = await compileCss(["squircle-full"]); + expect(css).toContain("border-radius: calc(infinity * 1px)"); + expect(css).toContain("calc(calc(infinity * 1px) *"); + expect(css).toContain("corner-shape: superellipse(var(--squircle-amt, 2))"); + }); + + it("squircle-t-full applies the full radius to side variants", async () => { + const css = await compileCss(["squircle-t-full"]); + expect(css).toContain("border-top-left-radius: calc(infinity * 1px)"); + expect(css).toContain("border-top-right-radius: calc(infinity * 1px)"); + }); + + it("squircle-none is plain zero radius with no correction", async () => { + const css = await compileCss(["squircle-none"]); + expect(css).toContain("border-radius: 0"); + expect(css).not.toContain("@supports"); + expect(css).not.toContain("corner-shape"); + }); + + it("squircle-tl-none zeroes a single corner", async () => { + const css = await compileCss(["squircle-tl-none"]); + expect(css).toContain("border-top-left-radius: 0"); + expect(css).not.toContain("@supports"); + }); + }); + describe("arbitrary values", () => { it("squircle-[1rem] emits literal length in fallback and calc", async () => { const css = await compileCss(["squircle-[1rem]"]); diff --git a/package/src/tailwind.test.ts b/package/src/tailwind.test.ts index fa7e8b2..6a4dfeb 100644 --- a/package/src/tailwind.test.ts +++ b/package/src/tailwind.test.ts @@ -48,6 +48,46 @@ describe("plugin.ts utilities", () => { }); } + describe("static none/full utilities (matching rounded-none/rounded-full)", () => { + it("squircle-full uses calc(infinity * 1px) with correction", async () => { + const css = await compilePlugin(["squircle-full"]); + expect(css).toContain("border-radius: calc(infinity * 1px)"); + expect(css).toContain("calc(calc(infinity * 1px) *"); + }); + + it("squircle-full is emitted exactly once (no v3-compat theme duplicate)", async () => { + const css = await compilePlugin(["squircle-full"]); + expect(css.match(/\.squircle-full \{/g)).toHaveLength(1); + expect(css).not.toContain("9999px"); + }); + + it("squircle-tl-full applies the full radius to corner variants", async () => { + const css = await compilePlugin(["squircle-tl-full"]); + expect(css).toContain("border-top-left-radius: calc(infinity * 1px)"); + }); + + it("squircle-none is plain zero radius with no correction", async () => { + const css = await compilePlugin(["squircle-none"]); + expect(css).toContain("border-radius: 0"); + expect(css).not.toContain("@supports"); + expect(css).not.toContain("corner-shape"); + expect(css.match(/\.squircle-none \{/g)).toHaveLength(1); + }); + + it("squircle-t-none zeroes side variants", async () => { + const css = await compilePlugin(["squircle-t-none"]); + expect(css).toContain("border-top-left-radius: 0"); + expect(css).toContain("border-top-right-radius: 0"); + }); + + it("custom prefix applies to static none/full utilities", async () => { + const css = await compilePlugin(["se-none", "se-full"], "prefix: se;"); + expect(css).toContain(".se-none"); + expect(css).toContain(".se-full"); + expect(css).toContain("border-radius: calc(infinity * 1px)"); + }); + }); + describe("arbitrary and invalid values", () => { it("squircle-[1rem] emits literal length", async () => { const css = await compilePlugin(["squircle-[1rem]"]); diff --git a/package/src/tailwind.ts b/package/src/tailwind.ts index cd430d7..3227735 100644 --- a/package/src/tailwind.ts +++ b/package/src/tailwind.ts @@ -7,6 +7,8 @@ import plugin from "tailwindcss/plugin"; import { DEFAULT_AMOUNT_VAR_NAME, DEFAULT_R_VAR_NAME, + FULL_RADIUS, + NONE_RADIUS, SUPPORTS_RULE, squircleCssObj, variantEntries, @@ -30,11 +32,14 @@ export interface SquirclePluginOptions { const squircle: ReturnType> = plugin.withOptions((options = {}) => // eslint-disable-next-line @typescript-eslint/unbound-method - ({ matchUtilities, theme }) => { + ({ addUtilities, matchUtilities, theme }) => { const amtVar = options.amtVar ?? options["amt-var"] ?? DEFAULT_AMOUNT_VAR_NAME; const rVar = options.rVar ?? options["r-var"] ?? DEFAULT_R_VAR_NAME; const prefix = options.prefix ?? "squircle"; - const radiusValues = theme("borderRadius"); + // Drop none/full from the functional values (the v3-compat theme still + // carries them) — they're registered as static utilities below instead, + // with the same values Tailwind uses for rounded-none/rounded-full. + const { none: _none, full: _full, ...radiusValues } = theme("borderRadius") ?? {}; matchUtilities( { @@ -50,6 +55,16 @@ const squircle: ReturnType> = for (const [suffix, props] of variantEntries()) { const name = suffix ? `${prefix}-${suffix}` : prefix; + // Static -none/-full utilities, registered the same way Tailwind + // defines rounded-none and rounded-full (0 and calc(infinity * 1px) + // rather than theme values). -none needs no superellipse correction. + addUtilities({ + [`.${name}-none`]: Object.fromEntries(props.map((p) => [p, NONE_RADIUS])), + [`.${name}-full`]: squircleCssObj(props, FULL_RADIUS, { amtVar, rVar }) as Record< + string, + string | Record + >, + }); matchUtilities( { [name]: (value: string) => diff --git a/package/src/variants.ts b/package/src/variants.ts index ad05bb2..39512d0 100644 --- a/package/src/variants.ts +++ b/package/src/variants.ts @@ -6,6 +6,10 @@ export const DEFAULT_AMT = 2 as const; export const DEFAULT_AMOUNT_VAR_NAME = "--squircle-amt" as const; export const DEFAULT_R_VAR_NAME = "--squircle-r" as const; +/** Static value for `squircle-full`; matches Tailwind's `rounded-full`. */ +export const FULL_RADIUS = "calc(infinity * 1px)" as const; +/** Static value for `squircle-none`; matches Tailwind's `rounded-none`. */ +export const NONE_RADIUS = "0" as const; export const DEFAULT_AMT_CSS = `var(${DEFAULT_AMOUNT_VAR_NAME}, ${DEFAULT_AMT})` as const; export const getCornerShape = (varName: string = DEFAULT_AMOUNT_VAR_NAME) => From d59b57d721a501ca672ed37a5f294cca77cc3950 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 05:44:51 +0000 Subject: [PATCH 02/10] refactor: emit squircle-full without the radius correction Correcting zero or an infinite radius is a no-op (the factor is finite and positive, and the browser clamps either way), so the static -none/-full utilities keep the exact rounded-none/rounded-full values in both branches. squircle-full only adds corner-shape under @supports; squircle-none is identical to rounded-none. --- README.md | 147 +++++++++++++---------- package/scripts/generate-squircle-css.ts | 4 +- package/src/squircle-css.test.ts | 6 +- package/src/tailwind.test.ts | 7 +- package/src/tailwind.ts | 4 +- package/src/variants.ts | 18 +++ 6 files changed, 113 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 8cab00a..e3b5f47 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ const twMerge = extendTailwindMerge(squircleMergeConfig, { Values are validated strictly so typos fail at build time instead of producing invalid CSS: - **`squircle-*` and its variants** accept the same theme values as `rounded-*` (`sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, plus anything you add to `@theme`) and arbitrary lengths like `squircle-[16px]`. Non-length arbitraries (`[50%]`, `[foo]`) and paren refs (`squircle-(--my-radius)`) are rejected — use a theme key instead. -- **`squircle-none` and `squircle-full`** (and their side/corner variants) are static utilities defined the same way as `rounded-none` and `rounded-full`: `0` and `calc(infinity * 1px)`. `squircle-full` gets the superellipse treatment; `squircle-none` is a plain zero radius, since there's no corner left to shape. +- **`squircle-none` and `squircle-full`** (and their side/corner variants) are static utilities defined the same way as `rounded-none` and `rounded-full`: `0` and `calc(infinity * 1px)`. Neither needs the visual radius correction — correcting zero or infinity is a no-op — so `squircle-full` is just `rounded-full` plus the superellipse corner shape, and `squircle-none` is identical to `rounded-none`. - **`squircle-amt-*`** accepts bare numbers (`squircle-amt-2`), arbitrary numbers (`squircle-amt-[3.5]`), and theme values. Unit-bearing arbitraries (`[1em]`) and paren refs (`(--my-amt)`) are rejected. ### What does `squircle-amt-*` control? @@ -612,10 +612,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-full { - border-radius: calc(infinity * 1px); + border-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - border-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -623,7 +621,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-* { border-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + --squircle-r: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); border-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -637,12 +638,9 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-t-full { - border-top-left-radius: calc(infinity * 1px); - border-top-right-radius: calc(infinity * 1px); + border-top-left-radius: calc(infinity* 1px); + border-top-right-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - border-top-left-radius: var(--squircle-r); - border-top-right-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -651,7 +649,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-left-radius: --value(--radius-*, [length]); border-top-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + --squircle-r: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); border-top-left-radius: var(--squircle-r); border-top-right-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -664,12 +665,9 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-r-full { - border-top-right-radius: calc(infinity * 1px); - border-bottom-right-radius: calc(infinity * 1px); + border-top-right-radius: calc(infinity* 1px); + border-bottom-right-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - border-top-right-radius: var(--squircle-r); - border-bottom-right-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -678,7 +676,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-right-radius: --value(--radius-*, [length]); border-bottom-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + --squircle-r: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); border-top-right-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -691,12 +692,9 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-b-full { - border-bottom-left-radius: calc(infinity * 1px); - border-bottom-right-radius: calc(infinity * 1px); + border-bottom-left-radius: calc(infinity* 1px); + border-bottom-right-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - border-bottom-left-radius: var(--squircle-r); - border-bottom-right-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -705,7 +703,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-bottom-left-radius: --value(--radius-*, [length]); border-bottom-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + --squircle-r: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); border-bottom-left-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -718,12 +719,9 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-l-full { - border-top-left-radius: calc(infinity * 1px); - border-bottom-left-radius: calc(infinity * 1px); + border-top-left-radius: calc(infinity* 1px); + border-bottom-left-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - border-top-left-radius: var(--squircle-r); - border-bottom-left-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -732,7 +730,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-left-radius: --value(--radius-*, [length]); border-bottom-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + --squircle-r: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); border-top-left-radius: var(--squircle-r); border-bottom-left-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -747,12 +748,9 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-s-full { - border-start-start-radius: calc(infinity * 1px); - border-end-start-radius: calc(infinity * 1px); + border-start-start-radius: calc(infinity* 1px); + border-end-start-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - border-start-start-radius: var(--squircle-r); - border-end-start-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -761,7 +759,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-start-start-radius: --value(--radius-*, [length]); border-end-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + --squircle-r: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); border-start-start-radius: var(--squircle-r); border-end-start-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -774,12 +775,9 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-e-full { - border-start-end-radius: calc(infinity * 1px); - border-end-end-radius: calc(infinity * 1px); + border-start-end-radius: calc(infinity* 1px); + border-end-end-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - border-start-end-radius: var(--squircle-r); - border-end-end-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -788,7 +786,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-start-end-radius: --value(--radius-*, [length]); border-end-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + --squircle-r: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); border-start-end-radius: var(--squircle-r); border-end-end-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -802,9 +803,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-tl-full { - border-top-left-radius: calc(infinity * 1px); + border-top-left-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - border-top-left-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -812,7 +812,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-tl-* { border-top-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-top-left-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-top-left-radius: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -822,9 +825,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-tr-full { - border-top-right-radius: calc(infinity * 1px); + border-top-right-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - border-top-right-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -832,7 +834,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-tr-* { border-top-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-top-right-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-top-right-radius: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -842,9 +847,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-br-full { - border-bottom-right-radius: calc(infinity * 1px); + border-bottom-right-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - border-bottom-right-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -852,7 +856,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-br-* { border-bottom-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-bottom-right-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-bottom-right-radius: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -862,9 +869,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-bl-full { - border-bottom-left-radius: calc(infinity * 1px); + border-bottom-left-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - border-bottom-left-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -872,7 +878,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-bl-* { border-bottom-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-bottom-left-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-bottom-left-radius: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -884,9 +893,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-ss-full { - border-start-start-radius: calc(infinity * 1px); + border-start-start-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - border-start-start-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -894,7 +902,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-ss-* { border-start-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-start-start-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-start-start-radius: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -904,9 +915,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-se-full { - border-start-end-radius: calc(infinity * 1px); + border-start-end-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - border-start-end-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -914,7 +924,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-se-* { border-start-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-start-end-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-start-end-radius: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -924,9 +937,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-es-full { - border-end-start-radius: calc(infinity * 1px); + border-end-start-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - border-end-start-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -934,7 +946,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-es-* { border-end-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-end-start-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-end-start-radius: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -944,9 +959,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-ee-full { - border-end-end-radius: calc(infinity * 1px); + border-end-end-radius: calc(infinity* 1px); @supports (corner-shape: superellipse(2)) { - border-end-end-radius: calc(calc(infinity * 1px) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -954,7 +968,10 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-ee-* { border-end-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-end-end-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); + border-end-end-radius: calc( + --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / + (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) + ); corner-shape: superellipse(var(--squircle-amt, 2)); } } diff --git a/package/scripts/generate-squircle-css.ts b/package/scripts/generate-squircle-css.ts index 9f30afb..e18af96 100644 --- a/package/scripts/generate-squircle-css.ts +++ b/package/scripts/generate-squircle-css.ts @@ -3,8 +3,8 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { DEFAULT_AMT, - FULL_RADIUS, NONE_RADIUS, + squircleFullCssObj, SUPPORTS_RULE, VARIANTS, isComment, @@ -70,7 +70,7 @@ function generateCss(): string { blocks.push( renderUtility(`${root}-none`, Object.fromEntries(entry.map((p) => [p, NONE_RADIUS]))), ); - blocks.push(renderUtility(`${root}-full`, squircleCssObj(entry, FULL_RADIUS))); + blocks.push(renderUtility(`${root}-full`, squircleFullCssObj(entry))); blocks.push(renderUtility(`${root}-*`, squircleCssObj(entry, value))); } diff --git a/package/src/squircle-css.test.ts b/package/src/squircle-css.test.ts index 7e856a0..9895433 100644 --- a/package/src/squircle-css.test.ts +++ b/package/src/squircle-css.test.ts @@ -51,11 +51,13 @@ describe("squircle.css utilities", () => { } describe("static none/full utilities (matching rounded-none/rounded-full)", () => { - it("squircle-full uses calc(infinity * 1px) with correction", async () => { + it("squircle-full uses calc(infinity * 1px) uncorrected, with corner-shape", async () => { const css = await compileCss(["squircle-full"]); expect(css).toContain("border-radius: calc(infinity * 1px)"); - expect(css).toContain("calc(calc(infinity * 1px) *"); expect(css).toContain("corner-shape: superellipse(var(--squircle-amt, 2))"); + // Correcting an infinite radius is a no-op, so no correction is emitted. + expect(css).not.toContain("pow("); + expect(css).not.toContain("--squircle-r"); }); it("squircle-t-full applies the full radius to side variants", async () => { diff --git a/package/src/tailwind.test.ts b/package/src/tailwind.test.ts index 6a4dfeb..107c1ff 100644 --- a/package/src/tailwind.test.ts +++ b/package/src/tailwind.test.ts @@ -49,10 +49,13 @@ describe("plugin.ts utilities", () => { } describe("static none/full utilities (matching rounded-none/rounded-full)", () => { - it("squircle-full uses calc(infinity * 1px) with correction", async () => { + it("squircle-full uses calc(infinity * 1px) uncorrected, with corner-shape", async () => { const css = await compilePlugin(["squircle-full"]); expect(css).toContain("border-radius: calc(infinity * 1px)"); - expect(css).toContain("calc(calc(infinity * 1px) *"); + expect(css).toContain("corner-shape: superellipse(var(--squircle-amt, 2))"); + // Correcting an infinite radius is a no-op, so no correction is emitted. + expect(css).not.toContain("pow("); + expect(css).not.toContain("--squircle-r"); }); it("squircle-full is emitted exactly once (no v3-compat theme duplicate)", async () => { diff --git a/package/src/tailwind.ts b/package/src/tailwind.ts index 3227735..aacf908 100644 --- a/package/src/tailwind.ts +++ b/package/src/tailwind.ts @@ -7,8 +7,8 @@ import plugin from "tailwindcss/plugin"; import { DEFAULT_AMOUNT_VAR_NAME, DEFAULT_R_VAR_NAME, - FULL_RADIUS, NONE_RADIUS, + squircleFullCssObj, SUPPORTS_RULE, squircleCssObj, variantEntries, @@ -60,7 +60,7 @@ const squircle: ReturnType> = // rather than theme values). -none needs no superellipse correction. addUtilities({ [`.${name}-none`]: Object.fromEntries(props.map((p) => [p, NONE_RADIUS])), - [`.${name}-full`]: squircleCssObj(props, FULL_RADIUS, { amtVar, rVar }) as Record< + [`.${name}-full`]: squircleFullCssObj(props, { amtVar }) as Record< string, string | Record >, diff --git a/package/src/variants.ts b/package/src/variants.ts index 39512d0..554fff7 100644 --- a/package/src/variants.ts +++ b/package/src/variants.ts @@ -19,6 +19,24 @@ export function correctedRadius(radius: string, amt: string = DEFAULT_AMT_CSS): return `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt}))))` as const; } +/** + * CSS object for the static `-full` utilities. The radius stays uncorrected — + * an infinite radius times any positive factor is still infinite, and the + * browser clamps it either way — so both branches share the same value, like + * `rounded-full`; the `@supports` block only adds the superellipse shape. + */ +export function squircleFullCssObj( + props: string[], + options: Pick = {}, +): CssLikeObject { + const obj: CssLikeObject = {}; + for (const p of props) obj[p] = FULL_RADIUS; + obj[SUPPORTS_RULE] = { + "corner-shape": getCornerShape(options.amtVar ?? DEFAULT_AMOUNT_VAR_NAME), + }; + return obj; +} + type SectionComment = { comment: string }; type VariantEntry = string[] | SectionComment; From 5b569d244fdf2a38409cdde775ebedc1b228a9fa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 05:47:37 +0000 Subject: [PATCH 03/10] fix: restore raw generated CSS in README copy-paste block A vp check --fix run from the package directory had reformatted the generated dist/tailwind/utils.css (breaking calc(infinity * 1px) and --value(--radius-*) tokens), and the README sync copied the mangled output. Regenerated the CSS and re-synced. --- README.md | 117 +++++++++++++++++------------------------------------- 1 file changed, 36 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index e3b5f47..e49a9e6 100644 --- a/README.md +++ b/README.md @@ -612,7 +612,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-full { - border-radius: calc(infinity* 1px); + border-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -621,10 +621,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-* { border-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -638,8 +635,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-t-full { - border-top-left-radius: calc(infinity* 1px); - border-top-right-radius: calc(infinity* 1px); + border-top-left-radius: calc(infinity * 1px); + border-top-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -649,10 +646,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-left-radius: --value(--radius-*, [length]); border-top-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-left-radius: var(--squircle-r); border-top-right-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -665,8 +659,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-r-full { - border-top-right-radius: calc(infinity* 1px); - border-bottom-right-radius: calc(infinity* 1px); + border-top-right-radius: calc(infinity * 1px); + border-bottom-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -676,10 +670,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-right-radius: --value(--radius-*, [length]); border-bottom-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-right-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -692,8 +683,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-b-full { - border-bottom-left-radius: calc(infinity* 1px); - border-bottom-right-radius: calc(infinity* 1px); + border-bottom-left-radius: calc(infinity * 1px); + border-bottom-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -703,10 +694,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-bottom-left-radius: --value(--radius-*, [length]); border-bottom-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-bottom-left-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -719,8 +707,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-l-full { - border-top-left-radius: calc(infinity* 1px); - border-bottom-left-radius: calc(infinity* 1px); + border-top-left-radius: calc(infinity * 1px); + border-bottom-left-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -730,10 +718,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-left-radius: --value(--radius-*, [length]); border-bottom-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-left-radius: var(--squircle-r); border-bottom-left-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -748,8 +733,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-s-full { - border-start-start-radius: calc(infinity* 1px); - border-end-start-radius: calc(infinity* 1px); + border-start-start-radius: calc(infinity * 1px); + border-end-start-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -759,10 +744,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-start-start-radius: --value(--radius-*, [length]); border-end-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-start-start-radius: var(--squircle-r); border-end-start-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -775,8 +757,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-e-full { - border-start-end-radius: calc(infinity* 1px); - border-end-end-radius: calc(infinity* 1px); + border-start-end-radius: calc(infinity * 1px); + border-end-end-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -786,10 +768,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-start-end-radius: --value(--radius-*, [length]); border-end-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - --squircle-r: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-start-end-radius: var(--squircle-r); border-end-end-radius: var(--squircle-r); corner-shape: superellipse(var(--squircle-amt, 2)); @@ -803,7 +782,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-tl-full { - border-top-left-radius: calc(infinity* 1px); + border-top-left-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -812,10 +791,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-tl-* { border-top-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-top-left-radius: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + border-top-left-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -825,7 +801,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-tr-full { - border-top-right-radius: calc(infinity* 1px); + border-top-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -834,10 +810,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-tr-* { border-top-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-top-right-radius: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + border-top-right-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -847,7 +820,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-br-full { - border-bottom-right-radius: calc(infinity* 1px); + border-bottom-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -856,10 +829,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-br-* { border-bottom-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-bottom-right-radius: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + border-bottom-right-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -869,7 +839,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-bl-full { - border-bottom-left-radius: calc(infinity* 1px); + border-bottom-left-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -878,10 +848,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-bl-* { border-bottom-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-bottom-left-radius: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + border-bottom-left-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -893,7 +860,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-ss-full { - border-start-start-radius: calc(infinity* 1px); + border-start-start-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -902,10 +869,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-ss-* { border-start-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-start-start-radius: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + border-start-start-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -915,7 +879,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-se-full { - border-start-end-radius: calc(infinity* 1px); + border-start-end-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -924,10 +888,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-se-* { border-start-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-start-end-radius: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + border-start-end-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -937,7 +898,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-es-full { - border-end-start-radius: calc(infinity* 1px); + border-end-start-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -946,10 +907,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-es-* { border-end-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-end-start-radius: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + border-end-start-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } @@ -959,7 +917,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility squircle-ee-full { - border-end-end-radius: calc(infinity* 1px); + border-end-end-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { corner-shape: superellipse(var(--squircle-amt, 2)); } @@ -968,10 +926,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-ee-* { border-end-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { - border-end-end-radius: calc( - --value(--radius- *, [length]) * (1 - pow(2, -0.5)) / - (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))) - ); + border-end-end-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); corner-shape: superellipse(var(--squircle-amt, 2)); } } From 5a2f00ecd97d8c52def3c5ab423cf96887671914 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 05:55:23 +0000 Subject: [PATCH 04/10] feat: mirror Tailwind's rounded hierarchy in the tailwind-merge config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the single catch-all squircle class group into per-variant groups with hierarchical conflicts matching tailwind-merge's own rounded config: a later all-corners utility cancels earlier side/corner utilities (in either family), a side cancels its two corners, and a narrower utility refines a broader one instead of canceling it. Also fixes squircle-amt being canceled by radius classes — it controls corner shape, not radius, so it is now orthogonal. Adds a test suite covering the merge semantics. --- README.md | 73 +++++++++++++++------------ package/src/tailwind-merge.test.ts | 80 ++++++++++++++++++++++++++++++ package/src/tailwind.ts | 79 ++++++++++++++--------------- 3 files changed, 162 insertions(+), 70 deletions(-) create mode 100644 package/src/tailwind-merge.test.ts diff --git a/README.md b/README.md index e49a9e6..1e0e0d3 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ const twMerge = extendTailwindMerge(squircleMergeConfig, { }); ``` +The conflicts mirror Tailwind's own `rounded` hierarchy: a later all-corners utility cancels earlier side/corner utilities (from either family), a side cancels its two corners, and a narrower utility refines a broader one instead of canceling it — `squircle-md squircle-tl-sm` keeps both. `squircle-amt-*` is independent of radius classes. + ### Utilities | Utility | Equivalent | Description | @@ -986,38 +988,47 @@ If you'd rather not add a dependency, copy the source directly. Click to expand import { extendTailwindMerge } from "tailwind-merge"; -const allRoundedGroups = [ - "rounded", "rounded-s", "rounded-e", "rounded-t", "rounded-r", - "rounded-b", "rounded-l", "rounded-ss", "rounded-se", "rounded-es", - "rounded-ee", "rounded-tl", "rounded-tr", "rounded-br", "rounded-bl", -]; - -export const squircleMergeConfig = { extend: { - classGroups: { - squircle: [ - { squircle: [() => true] }, - { "squircle-t": [() => true] }, - { "squircle-r": [() => true] }, - { "squircle-b": [() => true] }, - { "squircle-l": [() => true] }, - { "squircle-s": [() => true] }, - { "squircle-e": [() => true] }, - { "squircle-tl": [() => true] }, - { "squircle-tr": [() => true] }, - { "squircle-br": [() => true] }, - { "squircle-bl": [() => true] }, - { "squircle-ss": [() => true] }, - { "squircle-se": [() => true] }, - { "squircle-es": [() => true] }, - { "squircle-ee": [() => true] }, - ], - "squircle-amt": [{ "squircle-amt": [() => true] }], - }, - conflictingClassGroups: { - squircle: [...allRoundedGroups, "squircle-amt"], - ...Object.fromEntries(allRoundedGroups.map((g) => [g, ["squircle", "squircle-amt"]])), +// Mirrors tailwind-merge's own `rounded` hierarchy: a later all-corners +// utility cancels earlier side/corner utilities, a side cancels its two +// corners, and a narrower utility never cancels a broader one. Each squircle +// group also conflicts with its `rounded` counterpart (and vice versa). +// `squircle-amt-*` is orthogonal: radius classes never cancel it. +const SIDE_CORNERS = { + t: ["tl", "tr"], r: ["tr", "br"], b: ["br", "bl"], + l: ["tl", "bl"], s: ["ss", "es"], e: ["se", "ee"], +}; +const CORNERS = ["tl", "tr", "br", "bl", "ss", "se", "es", "ee"]; +const SIDES = Object.keys(SIDE_CORNERS); +const ALL_SUFFIXES = ["", ...SIDES, ...CORNERS]; + +const sq = (suffix) => (suffix ? `squircle-${suffix}` : "squircle"); +const rd = (suffix) => (suffix ? `rounded-${suffix}` : "rounded"); + +const conflictingClassGroups = { + squircle: [...ALL_SUFFIXES.slice(1).map(sq), ...ALL_SUFFIXES.map(rd)], + rounded: ALL_SUFFIXES.map(sq), +}; +for (const side of SIDES) { + const corners = SIDE_CORNERS[side]; + conflictingClassGroups[sq(side)] = [...corners.map(sq), rd(side), ...corners.map(rd)]; + conflictingClassGroups[rd(side)] = [sq(side), ...corners.map(sq)]; +} +for (const corner of CORNERS) { + conflictingClassGroups[sq(corner)] = [rd(corner)]; + conflictingClassGroups[rd(corner)] = [sq(corner)]; +} + +export const squircleMergeConfig = { + extend: { + classGroups: { + ...Object.fromEntries( + ALL_SUFFIXES.map((suffix) => [sq(suffix), [{ [sq(suffix)]: [() => true] }]]), + ), + "squircle-amt": [{ "squircle-amt": [() => true] }], + }, + conflictingClassGroups, }, -} }; +}; export const twMerge = extendTailwindMerge(squircleMergeConfig); ``` diff --git a/package/src/tailwind-merge.test.ts b/package/src/tailwind-merge.test.ts new file mode 100644 index 0000000..4f65390 --- /dev/null +++ b/package/src/tailwind-merge.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import { extendTailwindMerge } from "tailwind-merge"; +import { squircleMergeConfig } from "./tailwind"; + +const twMerge = extendTailwindMerge(squircleMergeConfig); + +describe("squircleMergeConfig", () => { + describe("squircle vs squircle", () => { + it("later value wins within the same group", () => { + expect(twMerge("squircle-md squircle-lg")).toBe("squircle-lg"); + expect(twMerge("squircle-tl-sm squircle-tl-lg")).toBe("squircle-tl-lg"); + }); + + it("all-corners cancels earlier side and corner utilities", () => { + expect(twMerge("squircle-tl-sm squircle-md")).toBe("squircle-md"); + expect(twMerge("squircle-t-lg squircle-md")).toBe("squircle-md"); + expect(twMerge("squircle-tl-full squircle-none")).toBe("squircle-none"); + }); + + it("a single corner does not cancel an earlier all-corners utility", () => { + expect(twMerge("squircle-md squircle-tl-sm")).toBe("squircle-md squircle-tl-sm"); + expect(twMerge("squircle-md squircle-tl-full")).toBe("squircle-md squircle-tl-full"); + }); + + it("a side cancels its own two corners", () => { + expect(twMerge("squircle-tl-sm squircle-t-lg")).toBe("squircle-t-lg"); + expect(twMerge("squircle-tr-sm squircle-t-lg")).toBe("squircle-t-lg"); + expect(twMerge("squircle-ss-sm squircle-s-lg")).toBe("squircle-s-lg"); + }); + + it("a side does not cancel an unrelated corner or an all-corners utility", () => { + expect(twMerge("squircle-bl-sm squircle-t-lg")).toBe("squircle-bl-sm squircle-t-lg"); + expect(twMerge("squircle-md squircle-t-lg")).toBe("squircle-md squircle-t-lg"); + }); + + it("a corner does not cancel a side", () => { + expect(twMerge("squircle-t-lg squircle-tl-sm")).toBe("squircle-t-lg squircle-tl-sm"); + }); + }); + + describe("squircle vs rounded", () => { + it("all-corners utilities cancel each other across families", () => { + expect(twMerge("rounded-lg squircle-md")).toBe("squircle-md"); + expect(twMerge("squircle-md rounded-lg")).toBe("rounded-lg"); + expect(twMerge("rounded-full squircle-full")).toBe("squircle-full"); + }); + + it("all-corners cancels the other family's sides and corners", () => { + expect(twMerge("rounded-tl-sm squircle-md")).toBe("squircle-md"); + expect(twMerge("rounded-t-lg squircle-md")).toBe("squircle-md"); + expect(twMerge("squircle-tl-sm rounded-lg")).toBe("rounded-lg"); + }); + + it("a narrower utility refines the other family's broader one", () => { + expect(twMerge("squircle-md rounded-tl-sm")).toBe("squircle-md rounded-tl-sm"); + expect(twMerge("rounded-lg squircle-tl-full")).toBe("rounded-lg squircle-tl-full"); + }); + + it("a side cancels the other family's matching side and corners", () => { + expect(twMerge("squircle-tl-sm rounded-t-lg")).toBe("rounded-t-lg"); + expect(twMerge("rounded-tr-sm squircle-t-none")).toBe("squircle-t-none"); + }); + + it("corners cancel only the matching corner across families", () => { + expect(twMerge("rounded-tl-sm squircle-tl-lg")).toBe("squircle-tl-lg"); + expect(twMerge("rounded-t-lg squircle-tl-sm")).toBe("rounded-t-lg squircle-tl-sm"); + }); + }); + + describe("squircle-amt is orthogonal to radius classes", () => { + it("radius utilities never cancel squircle-amt", () => { + expect(twMerge("squircle-amt-3 squircle-md")).toBe("squircle-amt-3 squircle-md"); + expect(twMerge("squircle-amt-3 rounded-lg")).toBe("squircle-amt-3 rounded-lg"); + }); + + it("later squircle-amt wins over earlier squircle-amt", () => { + expect(twMerge("squircle-amt-2 squircle-amt-3")).toBe("squircle-amt-3"); + }); + }); +}); diff --git a/package/src/tailwind.ts b/package/src/tailwind.ts index aacf908..32d83b7 100644 --- a/package/src/tailwind.ts +++ b/package/src/tailwind.ts @@ -82,49 +82,50 @@ export default squircle; // --- tailwind-merge config --------------------------------------------------- -const allRoundedGroups: string[] = [ - "rounded", - "rounded-s", - "rounded-e", - "rounded-t", - "rounded-r", - "rounded-b", - "rounded-l", - "rounded-ss", - "rounded-se", - "rounded-es", - "rounded-ee", - "rounded-tl", - "rounded-tr", - "rounded-br", - "rounded-bl", -]; +// Mirrors tailwind-merge's own `rounded` hierarchy: a later all-corners +// utility cancels earlier side/corner utilities, a side cancels its two +// corners, and a narrower utility never cancels a broader one — so +// `squircle-md squircle-tl-sm` keeps both, refining one corner. Each squircle +// group also conflicts with its `rounded` counterpart (and vice versa), since +// both set the same border-radius properties. `squircle-amt-*` is orthogonal: +// it controls corner shape, not radius, so radius classes never cancel it. +const SIDE_CORNERS = { + t: ["tl", "tr"], + r: ["tr", "br"], + b: ["br", "bl"], + l: ["tl", "bl"], + s: ["ss", "es"], + e: ["se", "ee"], +} as const; +const CORNERS = ["tl", "tr", "br", "bl", "ss", "se", "es", "ee"] as const; +const SIDES = Object.keys(SIDE_CORNERS) as (keyof typeof SIDE_CORNERS)[]; +const ALL_SUFFIXES: readonly string[] = ["", ...SIDES, ...CORNERS]; + +const sq = (suffix: string) => (suffix ? `squircle-${suffix}` : "squircle"); +const rd = (suffix: string) => (suffix ? `rounded-${suffix}` : "rounded"); + +const conflictingClassGroups: Record = { + squircle: [...ALL_SUFFIXES.slice(1).map(sq), ...ALL_SUFFIXES.map(rd)], + rounded: ALL_SUFFIXES.map(sq), +}; +for (const side of SIDES) { + const corners: readonly string[] = SIDE_CORNERS[side]; + conflictingClassGroups[sq(side)] = [...corners.map(sq), rd(side), ...corners.map(rd)]; + conflictingClassGroups[rd(side)] = [sq(side), ...corners.map(sq)]; +} +for (const corner of CORNERS) { + conflictingClassGroups[sq(corner)] = [rd(corner)]; + conflictingClassGroups[rd(corner)] = [sq(corner)]; +} export const squircleMergeConfig = { extend: { classGroups: { - squircle: [ - { squircle: [() => true] }, - { "squircle-t": [() => true] }, - { "squircle-r": [() => true] }, - { "squircle-b": [() => true] }, - { "squircle-l": [() => true] }, - { "squircle-s": [() => true] }, - { "squircle-e": [() => true] }, - { "squircle-tl": [() => true] }, - { "squircle-tr": [() => true] }, - { "squircle-br": [() => true] }, - { "squircle-bl": [() => true] }, - { "squircle-ss": [() => true] }, - { "squircle-se": [() => true] }, - { "squircle-es": [() => true] }, - { "squircle-ee": [() => true] }, - ], + ...Object.fromEntries( + ALL_SUFFIXES.map((suffix) => [sq(suffix), [{ [sq(suffix)]: [() => true] }]]), + ), "squircle-amt": [{ "squircle-amt": [() => true] }], }, - conflictingClassGroups: { - squircle: [...allRoundedGroups, "squircle-amt"], - ...Object.fromEntries(allRoundedGroups.map((g) => [g, ["squircle", "squircle-amt"]])), - }, + conflictingClassGroups, }, -} as const; +}; From 1839e6c2c5c7dfabd4d9a8b08cb28915f0a1d1c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 06:01:26 +0000 Subject: [PATCH 05/10] test: verify cascade order agrees with tailwind-merge semantics Keeping both classes only refines a corner if the narrower utility also wins the cascade, which comes down to emission order at equal specificity. Pins that down for every non-conflicting pair, on both the CSS-import and plugin paths, so a change in utility registration order fails loudly. --- package/src/tailwind-merge.test.ts | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/package/src/tailwind-merge.test.ts b/package/src/tailwind-merge.test.ts index 4f65390..672e068 100644 --- a/package/src/tailwind-merge.test.ts +++ b/package/src/tailwind-merge.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from "vitest"; import { extendTailwindMerge } from "tailwind-merge"; import { squircleMergeConfig } from "./tailwind"; +import { createCompiler } from "./test-utils"; const twMerge = extendTailwindMerge(squircleMergeConfig); +const { compileCss, compilePlugin } = createCompiler(import.meta.dirname); describe("squircleMergeConfig", () => { describe("squircle vs squircle", () => { @@ -78,3 +80,49 @@ describe("squircleMergeConfig", () => { }); }); }); + +/** + * Keeping both classes only refines the corner if the narrower utility also + * wins the cascade — these all have single-class specificity, so it comes down + * to emission order. Tailwind sorts radius utilities by property breadth + * (`border-radius` shorthand, then two-corner sides, then single corners) and + * interleaves `squircle-*` with `rounded-*` in each tier, so the narrower + * utility lands later regardless of family. These cases pin that down. + */ +describe("cascade order agrees with merge semantics", () => { + // [broader utility, narrower utility that must win the overlapping corner] + const pairs: [string, string][] = [ + ["squircle-md", "squircle-t-lg"], + ["squircle-md", "squircle-tl-sm"], + ["squircle-md", "squircle-tl-full"], + ["squircle-md", "squircle-tl-none"], + ["squircle-t-lg", "squircle-tl-sm"], + ["squircle-full", "squircle-tl-sm"], + ["squircle-none", "squircle-tl-full"], + ["squircle-t-full", "squircle-tl-none"], + ["squircle-s-md", "squircle-ss-sm"], + ["squircle-e-full", "squircle-ee-none"], + ["squircle-md", "rounded-tl-sm"], + ["rounded-lg", "squircle-tl-full"], + ["rounded-lg", "squircle-tl-none"], + ["rounded-full", "squircle-tl-sm"], + ["rounded-t-lg", "squircle-tl-sm"], + ]; + + function selectorIndex(css: string, className: string): number { + const index = css.indexOf(`.${className} {`); + expect(index, `${className} was not emitted`).toBeGreaterThan(-1); + return index; + } + + for (const [broad, narrow] of pairs) { + it(`"${broad} ${narrow}" keeps both, and ${narrow} is emitted last`, async () => { + expect(twMerge(`${broad} ${narrow}`)).toBe(`${broad} ${narrow}`); + + for (const compile of [compileCss, compilePlugin]) { + const css = await compile([broad, narrow]); + expect(selectorIndex(css, narrow)).toBeGreaterThan(selectorIndex(css, broad)); + } + }); + } +}); From e025aafcb0bac8f9a27ea191b35ad45adc23baab Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 06:03:26 +0000 Subject: [PATCH 06/10] fix: build before test:tailwind so the merge cascade test finds dist The test:tailwind filter matches tailwind-merge.test.ts as well, and its cascade-order cases compile the generated utils.css, so the task needs the same build dependency test:css and test:radius already declare. --- package/vite.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package/vite.config.ts b/package/vite.config.ts index c7a847f..ec5db17 100644 --- a/package/vite.config.ts +++ b/package/vite.config.ts @@ -18,7 +18,10 @@ export default defineConfig({ run: { tasks: { "test:tailwind": { + // Matches tailwind.test.ts and tailwind-merge.test.ts; the latter + // compiles the generated utils.css, so the build has to run first. command: "vp test run tailwind", + dependsOn: ["build"], }, "test:css": { command: "vp test run squircle-css", From 7e4ca23132aeb6c325c8ba77ccc0db02cb724062 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 06:22:53 +0000 Subject: [PATCH 07/10] fix: shape only the corners each utility owns, and let rounded-* reset them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Side and corner squircle utilities set the corner-shape shorthand, which reshaped all four corners even though they only set a radius on the ones they name — so squircle-t-lg rounded-b-lg rendered the bottom corners as squircles at an uncorrected radius. They now emit the matching per-corner longhands (corner-top-left-shape and friends); the all-corners utility keeps the shorthand, since it does own all four. The converse did not work either: Tailwind's rounded-* utilities only set a radius, so they could never take a corner back from a squircle. This re-declares them to also reset their own corners to round. Tailwind still emits its own rule for the radius, and the reset is the initial value, so it is inert unless a squircle class is on the same element. Applies to the generated CSS, the JS plugin, and the StyleX preset. --- README.md | 268 ++++++++++++++++-- package/scripts/generate-squircle-css.ts | 36 +++ package/scripts/generate-stylex.ts | 29 +- .../__snapshots__/squircle-css.test.ts.snap | 34 ++- .../src/__snapshots__/tailwind.test.ts.snap | 34 ++- package/src/panda.test.ts | 5 +- package/src/squircle-css.test.ts | 7 +- package/src/stylex.ts | 52 +++- package/src/tailwind-corner-shape.test.ts | 103 +++++++ package/src/tailwind.ts | 27 ++ package/src/variants.ts | 21 +- 11 files changed, 532 insertions(+), 84 deletions(-) create mode 100644 package/src/tailwind-corner-shape.test.ts diff --git a/README.md b/README.md index 1e0e0d3..67ffed3 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,24 @@ Values are validated strictly so typos fail at build time instead of producing i - **`squircle-none` and `squircle-full`** (and their side/corner variants) are static utilities defined the same way as `rounded-none` and `rounded-full`: `0` and `calc(infinity * 1px)`. Neither needs the visual radius correction — correcting zero or infinity is a no-op — so `squircle-full` is just `rounded-full` plus the superellipse corner shape, and `squircle-none` is identical to `rounded-none`. - **`squircle-amt-*`** accepts bare numbers (`squircle-amt-2`), arbitrary numbers (`squircle-amt-[3.5]`), and theme values. Unit-bearing arbitraries (`[1em]`) and paren refs (`(--my-amt)`) are rejected. +### Mixing squircled and rounded corners + +Corner shape is tracked per corner, so the two families compose in both directions on the same element: + +```html + +
…
+ + +
…
+``` + +Two things make that work. A `squircle-*` utility only shapes the corners it sets a radius on — the all-corners utility uses the `corner-shape` shorthand, while the side and corner variants use the matching longhands (`corner-top-left-shape` and friends). And because Tailwind's own `rounded-*` utilities only set a radius, they could never take a corner back from a squircle, so this package re-declares them to also reset their corners to `round`. That reset is the initial value, so it changes nothing unless a squircle class is on the same element. + +Note that a corner reclaimed by `rounded-*` uses the plain radius, not the corrected one — the correction exists only to make a squircle look the same size as a rounded corner, so a genuinely round corner doesn't want it. + +One gap: Tailwind's `rounded-*` also accepts paren refs like `rounded-tl-(--my-radius)`. The CSS-import path resets those too; the JS plugin cannot, so on that path such a corner keeps its squircle shape. Use a theme key instead. + ### What does `squircle-amt-*` control? The value is the `K` parameter passed to `superellipse(K)`, which controls how square the corner shape is: @@ -640,7 +658,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-left-radius: calc(infinity * 1px); border-top-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -651,7 +670,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-left-radius: var(--squircle-r); border-top-right-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -664,7 +684,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-right-radius: calc(infinity * 1px); border-bottom-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -675,7 +696,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-right-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -688,7 +710,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-bottom-left-radius: calc(infinity * 1px); border-bottom-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -699,7 +722,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-bottom-left-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -712,7 +736,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-left-radius: calc(infinity * 1px); border-bottom-left-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); } } @@ -723,7 +748,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-left-radius: var(--squircle-r); border-bottom-left-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); } } @@ -738,7 +764,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-start-start-radius: calc(infinity * 1px); border-end-start-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-start-shape: superellipse(var(--squircle-amt, 2)); + corner-end-start-shape: superellipse(var(--squircle-amt, 2)); } } @@ -749,7 +776,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-start-start-radius: var(--squircle-r); border-end-start-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-start-shape: superellipse(var(--squircle-amt, 2)); + corner-end-start-shape: superellipse(var(--squircle-amt, 2)); } } @@ -762,7 +790,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-start-end-radius: calc(infinity * 1px); border-end-end-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-end-shape: superellipse(var(--squircle-amt, 2)); + corner-end-end-shape: superellipse(var(--squircle-amt, 2)); } } @@ -773,7 +802,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand --squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-start-end-radius: var(--squircle-r); border-end-end-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-end-shape: superellipse(var(--squircle-amt, 2)); + corner-end-end-shape: superellipse(var(--squircle-amt, 2)); } } @@ -786,7 +816,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-tl-full { border-top-left-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); } } @@ -794,7 +824,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { border-top-left-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); } } @@ -805,7 +835,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-tr-full { border-top-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -813,7 +843,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-top-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { border-top-right-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -824,7 +854,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-br-full { border-bottom-right-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -832,7 +862,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-bottom-right-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { border-bottom-right-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } } @@ -843,7 +873,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-bl-full { border-bottom-left-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); } } @@ -851,7 +881,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-bottom-left-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { border-bottom-left-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); } } @@ -864,7 +894,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-ss-full { border-start-start-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-start-shape: superellipse(var(--squircle-amt, 2)); } } @@ -872,7 +902,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-start-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { border-start-start-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-start-shape: superellipse(var(--squircle-amt, 2)); } } @@ -883,7 +913,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-se-full { border-start-end-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-end-shape: superellipse(var(--squircle-amt, 2)); } } @@ -891,7 +921,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-start-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { border-start-end-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-end-shape: superellipse(var(--squircle-amt, 2)); } } @@ -902,7 +932,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-es-full { border-end-start-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-end-start-shape: superellipse(var(--squircle-amt, 2)); } } @@ -910,7 +940,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-end-start-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { border-end-start-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-end-start-shape: superellipse(var(--squircle-amt, 2)); } } @@ -921,7 +951,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand @utility squircle-ee-full { border-end-end-radius: calc(infinity * 1px); @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-end-end-shape: superellipse(var(--squircle-amt, 2)); } } @@ -929,9 +959,191 @@ If you'd rather not add a dependency, copy the source directly. Click to expand border-end-end-radius: --value(--radius-*, [length]); @supports (corner-shape: superellipse(2)) { border-end-end-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-end-end-shape: superellipse(var(--squircle-amt, 2)); } } + +/* ── rounded-* corner-shape resets ──────────────────────────── */ +/* Tailwind's rounded-* utilities only set a radius, so on their own they can't + take a corner back from a squircle. These re-declarations add the matching + corner-shape reset; Tailwind still emits its own rule for the radius. The + reset is the initial value, so it does nothing unless a squircle class set a + shape on the same element. rounded-*-none needs none: a zero radius has no + visible corner to shape. */ + +@utility rounded-full { + border-radius: calc(infinity * 1px); + corner-shape: round; +} + +@utility rounded-* { + border-radius: --value(--radius-*, [length], [*]); + corner-shape: round; +} + +@utility rounded-t-full { + border-top-left-radius: calc(infinity * 1px); + border-top-right-radius: calc(infinity * 1px); + corner-top-left-shape: round; + corner-top-right-shape: round; +} + +@utility rounded-t-* { + border-top-left-radius: --value(--radius-*, [length], [*]); + border-top-right-radius: --value(--radius-*, [length], [*]); + corner-top-left-shape: round; + corner-top-right-shape: round; +} + +@utility rounded-r-full { + border-top-right-radius: calc(infinity * 1px); + border-bottom-right-radius: calc(infinity * 1px); + corner-top-right-shape: round; + corner-bottom-right-shape: round; +} + +@utility rounded-r-* { + border-top-right-radius: --value(--radius-*, [length], [*]); + border-bottom-right-radius: --value(--radius-*, [length], [*]); + corner-top-right-shape: round; + corner-bottom-right-shape: round; +} + +@utility rounded-b-full { + border-bottom-left-radius: calc(infinity * 1px); + border-bottom-right-radius: calc(infinity * 1px); + corner-bottom-left-shape: round; + corner-bottom-right-shape: round; +} + +@utility rounded-b-* { + border-bottom-left-radius: --value(--radius-*, [length], [*]); + border-bottom-right-radius: --value(--radius-*, [length], [*]); + corner-bottom-left-shape: round; + corner-bottom-right-shape: round; +} + +@utility rounded-l-full { + border-top-left-radius: calc(infinity * 1px); + border-bottom-left-radius: calc(infinity * 1px); + corner-top-left-shape: round; + corner-bottom-left-shape: round; +} + +@utility rounded-l-* { + border-top-left-radius: --value(--radius-*, [length], [*]); + border-bottom-left-radius: --value(--radius-*, [length], [*]); + corner-top-left-shape: round; + corner-bottom-left-shape: round; +} + +@utility rounded-s-full { + border-start-start-radius: calc(infinity * 1px); + border-end-start-radius: calc(infinity * 1px); + corner-start-start-shape: round; + corner-end-start-shape: round; +} + +@utility rounded-s-* { + border-start-start-radius: --value(--radius-*, [length], [*]); + border-end-start-radius: --value(--radius-*, [length], [*]); + corner-start-start-shape: round; + corner-end-start-shape: round; +} + +@utility rounded-e-full { + border-start-end-radius: calc(infinity * 1px); + border-end-end-radius: calc(infinity * 1px); + corner-start-end-shape: round; + corner-end-end-shape: round; +} + +@utility rounded-e-* { + border-start-end-radius: --value(--radius-*, [length], [*]); + border-end-end-radius: --value(--radius-*, [length], [*]); + corner-start-end-shape: round; + corner-end-end-shape: round; +} + +@utility rounded-tl-full { + border-top-left-radius: calc(infinity * 1px); + corner-top-left-shape: round; +} + +@utility rounded-tl-* { + border-top-left-radius: --value(--radius-*, [length], [*]); + corner-top-left-shape: round; +} + +@utility rounded-tr-full { + border-top-right-radius: calc(infinity * 1px); + corner-top-right-shape: round; +} + +@utility rounded-tr-* { + border-top-right-radius: --value(--radius-*, [length], [*]); + corner-top-right-shape: round; +} + +@utility rounded-br-full { + border-bottom-right-radius: calc(infinity * 1px); + corner-bottom-right-shape: round; +} + +@utility rounded-br-* { + border-bottom-right-radius: --value(--radius-*, [length], [*]); + corner-bottom-right-shape: round; +} + +@utility rounded-bl-full { + border-bottom-left-radius: calc(infinity * 1px); + corner-bottom-left-shape: round; +} + +@utility rounded-bl-* { + border-bottom-left-radius: --value(--radius-*, [length], [*]); + corner-bottom-left-shape: round; +} + +@utility rounded-ss-full { + border-start-start-radius: calc(infinity * 1px); + corner-start-start-shape: round; +} + +@utility rounded-ss-* { + border-start-start-radius: --value(--radius-*, [length], [*]); + corner-start-start-shape: round; +} + +@utility rounded-se-full { + border-start-end-radius: calc(infinity * 1px); + corner-start-end-shape: round; +} + +@utility rounded-se-* { + border-start-end-radius: --value(--radius-*, [length], [*]); + corner-start-end-shape: round; +} + +@utility rounded-es-full { + border-end-start-radius: calc(infinity * 1px); + corner-end-start-shape: round; +} + +@utility rounded-es-* { + border-end-start-radius: --value(--radius-*, [length], [*]); + corner-end-start-shape: round; +} + +@utility rounded-ee-full { + border-end-end-radius: calc(infinity * 1px); + corner-end-end-shape: round; +} + +@utility rounded-ee-* { + border-end-end-radius: --value(--radius-*, [length], [*]); + corner-end-end-shape: round; +} ``` diff --git a/package/scripts/generate-squircle-css.ts b/package/scripts/generate-squircle-css.ts index e18af96..96fb08f 100644 --- a/package/scripts/generate-squircle-css.ts +++ b/package/scripts/generate-squircle-css.ts @@ -3,7 +3,9 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { DEFAULT_AMT, + FULL_RADIUS, NONE_RADIUS, + cornerShapeProp, squircleFullCssObj, SUPPORTS_RULE, VARIANTS, @@ -17,6 +19,23 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // Support arbitrary, bare, and theme values in one --value() call. // https://tailwindcss.com/docs/adding-custom-styles#functional-utilities const value = "--value(--radius-*, [length])"; +// The rounded-* overrides additionally accept `(--var)` refs, which core +// supports; without [*] those classes would silently miss the shape reset. +const roundedValue = "--value(--radius-*, [length], [*])"; + +/** + * Re-declare a `rounded-*` utility so it also resets the corners it owns back + * to `round`. Tailwind keeps its own definition too and emits it after this + * one, so the radius comes from core and this only contributes the reset — + * which is the initial value, hence inert unless a squircle class set a shape + * on the same element. + */ +function renderRoundedReset(name: string, props: string[], radius: string): string { + const obj: CssLikeObject = {}; + for (const p of props) obj[p] = radius; + for (const p of props) obj[cornerShapeProp(p)] = "round"; + return renderUtility(name, obj); +} function renderUtility(name: string, obj: CssLikeObject): string { const lines: string[] = [`@utility ${name} {`]; @@ -74,6 +93,23 @@ function generateCss(): string { blocks.push(renderUtility(`${root}-*`, squircleCssObj(entry, value))); } + blocks.push(`\ +/* ── rounded-* corner-shape resets ──────────────────────────── */ +/* Tailwind's rounded-* utilities only set a radius, so on their own they can't + take a corner back from a squircle. These re-declarations add the matching + corner-shape reset; Tailwind still emits its own rule for the radius. The + reset is the initial value, so it does nothing unless a squircle class set a + shape on the same element. rounded-*-none needs none: a zero radius has no + visible corner to shape. */`); + + for (const [suffix, entry] of Object.entries(VARIANTS)) { + if (isComment(entry)) continue; + + const root = suffix ? `rounded-${suffix}` : "rounded"; + blocks.push(renderRoundedReset(`${root}-full`, entry, FULL_RADIUS)); + blocks.push(renderRoundedReset(`${root}-*`, entry, roundedValue)); + } + return blocks.join("\n\n") + "\n"; } diff --git a/package/scripts/generate-stylex.ts b/package/scripts/generate-stylex.ts index fdbca5b..029be2c 100644 --- a/package/scripts/generate-stylex.ts +++ b/package/scripts/generate-stylex.ts @@ -1,7 +1,13 @@ import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { CAMEL_VARIANTS, VARIANTS, isComment, SUPPORTS_RULE } from "../src/variants"; +import { + CAMEL_VARIANTS, + VARIANTS, + cornerShapeProp, + isComment, + SUPPORTS_RULE, +} from "../src/variants"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -30,7 +36,7 @@ function toCamel(kebab: string): string { /** * Render one `key: (radius, amt) => ({ ... })` entry for the * `stylex.create({...})` literal. The body is one CSS-property block per - * radius prop plus a single `cornerShape` block, both gated by `@supports`. + * radius prop plus a matching shape block for each, both gated by `@supports`. * * Tweak the indentation, comment style, or fallback expression here — the * template file just stamps these strings into its create() call. @@ -49,13 +55,24 @@ function renderVariant(side: string, kebabProps: string[]): string { }) .join("\n"); + // One shape block per radius prop, so a variant only reshapes the corners it + // actually sets a radius on. `border-radius` maps to the `cornerShape` + // shorthand, the narrower props to their per-corner longhands. + const shapeBlocks = kebabProps + .map((prop) => + [ + `${indent}${indent}${toCamel(cornerShapeProp(prop))}: {`, + `${indent}${indent}${indent}default: null,`, + `${indent}${indent}${indent}"${SUPPORTS_RULE}": \`superellipse(\${amt ?? 2})\`,`, + `${indent}${indent}},`, + ].join("\n"), + ) + .join("\n"); + return [ `${indent}${side}: (radius: string | number, amt: string | number | undefined) => ({`, radiusBlocks, - `${indent}${indent}cornerShape: {`, - `${indent}${indent}${indent}default: null,`, - `${indent}${indent}${indent}"${SUPPORTS_RULE}": \`superellipse(\${amt ?? 2})\`,`, - `${indent}${indent}},`, + shapeBlocks, `${indent}}),`, ].join("\n"); } diff --git a/package/src/__snapshots__/squircle-css.test.ts.snap b/package/src/__snapshots__/squircle-css.test.ts.snap index 5809a77..2b45d26 100644 --- a/package/src/__snapshots__/squircle-css.test.ts.snap +++ b/package/src/__snapshots__/squircle-css.test.ts.snap @@ -8,7 +8,8 @@ exports[`squircle.css utilities > squircle-b-md snapshot 1`] = ` --squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-bottom-left-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -18,7 +19,7 @@ exports[`squircle.css utilities > squircle-bl-md snapshot 1`] = ` border-bottom-left-radius: var(--radius-md); @supports (corner-shape: superellipse(2)) { border-bottom-left-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -28,7 +29,7 @@ exports[`squircle.css utilities > squircle-br-md snapshot 1`] = ` border-bottom-right-radius: var(--radius-md); @supports (corner-shape: superellipse(2)) { border-bottom-right-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -41,7 +42,8 @@ exports[`squircle.css utilities > squircle-e-md snapshot 1`] = ` --squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-start-end-radius: var(--squircle-r); border-end-end-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-end-shape: superellipse(var(--squircle-amt, 2)); + corner-end-end-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -51,7 +53,7 @@ exports[`squircle.css utilities > squircle-ee-md snapshot 1`] = ` border-end-end-radius: var(--radius-md); @supports (corner-shape: superellipse(2)) { border-end-end-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-end-end-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -61,7 +63,7 @@ exports[`squircle.css utilities > squircle-es-md snapshot 1`] = ` border-end-start-radius: var(--radius-md); @supports (corner-shape: superellipse(2)) { border-end-start-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-end-start-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -74,7 +76,8 @@ exports[`squircle.css utilities > squircle-l-md snapshot 1`] = ` --squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-left-radius: var(--squircle-r); border-bottom-left-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -98,7 +101,8 @@ exports[`squircle.css utilities > squircle-r-md snapshot 1`] = ` --squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-right-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -111,7 +115,8 @@ exports[`squircle.css utilities > squircle-s-md snapshot 1`] = ` --squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-start-start-radius: var(--squircle-r); border-end-start-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-start-shape: superellipse(var(--squircle-amt, 2)); + corner-end-start-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -121,7 +126,7 @@ exports[`squircle.css utilities > squircle-se-md snapshot 1`] = ` border-start-end-radius: var(--radius-md); @supports (corner-shape: superellipse(2)) { border-start-end-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-end-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -131,7 +136,7 @@ exports[`squircle.css utilities > squircle-ss-md snapshot 1`] = ` border-start-start-radius: var(--radius-md); @supports (corner-shape: superellipse(2)) { border-start-start-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-start-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -144,7 +149,8 @@ exports[`squircle.css utilities > squircle-t-md snapshot 1`] = ` --squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-left-radius: var(--squircle-r); border-top-right-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -154,7 +160,7 @@ exports[`squircle.css utilities > squircle-tl-md snapshot 1`] = ` border-top-left-radius: var(--radius-md); @supports (corner-shape: superellipse(2)) { border-top-left-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -164,7 +170,7 @@ exports[`squircle.css utilities > squircle-tr-md snapshot 1`] = ` border-top-right-radius: var(--radius-md); @supports (corner-shape: superellipse(2)) { border-top-right-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; diff --git a/package/src/__snapshots__/tailwind.test.ts.snap b/package/src/__snapshots__/tailwind.test.ts.snap index 9090b84..92994a5 100644 --- a/package/src/__snapshots__/tailwind.test.ts.snap +++ b/package/src/__snapshots__/tailwind.test.ts.snap @@ -8,7 +8,8 @@ exports[`plugin.ts utilities > squircle-b-md snapshot 1`] = ` --squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-bottom-left-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -18,7 +19,7 @@ exports[`plugin.ts utilities > squircle-bl-md snapshot 1`] = ` border-bottom-left-radius: 0.375rem; @supports (corner-shape: superellipse(2)) { border-bottom-left-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -28,7 +29,7 @@ exports[`plugin.ts utilities > squircle-br-md snapshot 1`] = ` border-bottom-right-radius: 0.375rem; @supports (corner-shape: superellipse(2)) { border-bottom-right-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -41,7 +42,8 @@ exports[`plugin.ts utilities > squircle-e-md snapshot 1`] = ` --squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-start-end-radius: var(--squircle-r); border-end-end-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-end-shape: superellipse(var(--squircle-amt, 2)); + corner-end-end-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -51,7 +53,7 @@ exports[`plugin.ts utilities > squircle-ee-md snapshot 1`] = ` border-end-end-radius: 0.375rem; @supports (corner-shape: superellipse(2)) { border-end-end-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-end-end-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -61,7 +63,7 @@ exports[`plugin.ts utilities > squircle-es-md snapshot 1`] = ` border-end-start-radius: 0.375rem; @supports (corner-shape: superellipse(2)) { border-end-start-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-end-start-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -74,7 +76,8 @@ exports[`plugin.ts utilities > squircle-l-md snapshot 1`] = ` --squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-left-radius: var(--squircle-r); border-bottom-left-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-left-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -98,7 +101,8 @@ exports[`plugin.ts utilities > squircle-r-md snapshot 1`] = ` --squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-right-radius: var(--squircle-r); border-bottom-right-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); + corner-bottom-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -111,7 +115,8 @@ exports[`plugin.ts utilities > squircle-s-md snapshot 1`] = ` --squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-start-start-radius: var(--squircle-r); border-end-start-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-start-shape: superellipse(var(--squircle-amt, 2)); + corner-end-start-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -121,7 +126,7 @@ exports[`plugin.ts utilities > squircle-se-md snapshot 1`] = ` border-start-end-radius: 0.375rem; @supports (corner-shape: superellipse(2)) { border-start-end-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-end-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -131,7 +136,7 @@ exports[`plugin.ts utilities > squircle-ss-md snapshot 1`] = ` border-start-start-radius: 0.375rem; @supports (corner-shape: superellipse(2)) { border-start-start-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-start-start-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -144,7 +149,8 @@ exports[`plugin.ts utilities > squircle-t-md snapshot 1`] = ` --squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); border-top-left-radius: var(--squircle-r); border-top-right-radius: var(--squircle-r); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -154,7 +160,7 @@ exports[`plugin.ts utilities > squircle-tl-md snapshot 1`] = ` border-top-left-radius: 0.375rem; @supports (corner-shape: superellipse(2)) { border-top-left-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-left-shape: superellipse(var(--squircle-amt, 2)); } }" `; @@ -164,7 +170,7 @@ exports[`plugin.ts utilities > squircle-tr-md snapshot 1`] = ` border-top-right-radius: 0.375rem; @supports (corner-shape: superellipse(2)) { border-top-right-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2))))); - corner-shape: superellipse(var(--squircle-amt, 2)); + corner-top-right-shape: superellipse(var(--squircle-amt, 2)); } }" `; diff --git a/package/src/panda.test.ts b/package/src/panda.test.ts index 7a805fc..5c8d1a7 100644 --- a/package/src/panda.test.ts +++ b/package/src/panda.test.ts @@ -70,7 +70,7 @@ describe("panda preset transform output", () => { { "@supports (corner-shape: superellipse(2))": { "borderTopLeftRadius": "calc(var(--radii-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))))", - "cornerShape": "superellipse(var(--squircle-amt, 2))", + "cornerTopLeftShape": "superellipse(var(--squircle-amt, 2))", }, "borderTopLeftRadius": "var(--radii-md)", } @@ -89,7 +89,8 @@ describe("panda preset transform output", () => { "--squircle-r": "calc(var(--radii-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))))", "borderTopLeftRadius": "var(--squircle-r)", "borderTopRightRadius": "var(--squircle-r)", - "cornerShape": "superellipse(var(--squircle-amt, 2))", + "cornerTopLeftShape": "superellipse(var(--squircle-amt, 2))", + "cornerTopRightShape": "superellipse(var(--squircle-amt, 2))", }, "borderTopLeftRadius": "var(--radii-md)", "borderTopRightRadius": "var(--radii-md)", diff --git a/package/src/squircle-css.test.ts b/package/src/squircle-css.test.ts index 9895433..f1d44d6 100644 --- a/package/src/squircle-css.test.ts +++ b/package/src/squircle-css.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { createCompiler, VARIANTS } from "./test-utils"; +import { cornerShapeProp } from "./variants"; const { compileCss } = createCompiler(import.meta.dirname); @@ -23,7 +24,11 @@ describe("squircle.css utilities", () => { it(`${className} applies corrected radius in @supports block`, async () => { const css = await compileCss([className]); expect(css).toContain("@supports (corner-shape: superellipse(2))"); - expect(css).toContain("corner-shape: superellipse(var(--squircle-amt, 2))"); + // Only the corners this variant sets a radius on get reshaped, so the + // all-corners utility uses the shorthand and the rest use longhands. + for (const prop of props) { + expect(css).toContain(`${cornerShapeProp(prop)}: superellipse(var(--squircle-amt, 2))`); + } expect(css).toContain("pow(2, -0.5)"); }); diff --git a/package/src/stylex.ts b/package/src/stylex.ts index 82a8bf5..c226003 100644 --- a/package/src/stylex.ts +++ b/package/src/stylex.ts @@ -79,7 +79,11 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopLeftShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerTopRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -94,7 +98,11 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopRightShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerBottomRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -109,7 +117,11 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerBottomLeftShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerBottomRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -124,7 +136,11 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopLeftShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerBottomLeftShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -141,7 +157,11 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerStartStartShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerEndStartShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -156,7 +176,11 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerStartEndShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerEndEndShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -169,7 +193,7 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopLeftShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -180,7 +204,7 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -191,7 +215,7 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerBottomRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -202,7 +226,7 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerBottomLeftShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -215,7 +239,7 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerStartStartShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -226,7 +250,7 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerStartEndShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -237,7 +261,7 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerEndStartShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -248,7 +272,7 @@ export const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerEndEndShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, diff --git a/package/src/tailwind-corner-shape.test.ts b/package/src/tailwind-corner-shape.test.ts new file mode 100644 index 0000000..53afcf8 --- /dev/null +++ b/package/src/tailwind-corner-shape.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "vitest"; +import { createCompiler, VARIANTS } from "./test-utils"; +import { cornerShapeProp } from "./variants"; + +const { compileCss, compilePlugin } = createCompiler(import.meta.dirname); +const paths = [ + ["css", compileCss], + ["plugin", compilePlugin], +] as const; + +// The declaration form of the shorthand. The `@supports` condition also +// mentions `corner-shape`, so matching the bare property name would always hit. +const SHORTHAND_DECL = "corner-shape: superellipse(var("; + +describe("squircle utilities only reshape the corners they own", () => { + for (const [pathName, compile] of paths) { + describe(pathName, () => { + for (const [suffix, props] of Object.entries(VARIANTS)) { + const className = suffix ? `squircle-${suffix}-md` : "squircle-md"; + const isAllCorners = props[0] === "border-radius"; + + it(`${className} uses ${isAllCorners ? "the shorthand" : "per-corner longhands"}`, async () => { + const css = await compile([className]); + for (const prop of props) { + expect(css).toContain(`${cornerShapeProp(prop)}: superellipse(var(--squircle-amt, 2))`); + } + if (isAllCorners) { + expect(css).toContain(SHORTHAND_DECL); + } else { + // The shorthand would reshape all four corners, including ones + // this utility never set a radius on. + expect(css).not.toContain(SHORTHAND_DECL); + } + }); + } + + it("squircle-t-md leaves the bottom corners' shape alone", async () => { + const css = await compile(["squircle-t-md"]); + expect(css).not.toContain("corner-bottom"); + expect(css).not.toContain(SHORTHAND_DECL); + }); + + it("squircle-tl-md shapes only the top-left corner", async () => { + const css = await compile(["squircle-tl-md"]); + expect(css).toContain("corner-top-left-shape:"); + expect(css).not.toContain("corner-top-right-shape:"); + expect(css).not.toContain("corner-bottom"); + }); + }); + } +}); + +describe("rounded-* utilities reset the corner shape they own", () => { + for (const [pathName, compile] of paths) { + describe(pathName, () => { + it("rounded-tl-lg resets only the top-left corner", async () => { + const css = await compile(["rounded-tl-lg"]); + expect(css).toContain("corner-top-left-shape: round"); + expect(css).not.toContain("corner-top-right-shape: round"); + }); + + it("rounded-t-lg resets both top corners", async () => { + const css = await compile(["rounded-t-lg"]); + expect(css).toContain("corner-top-left-shape: round"); + expect(css).toContain("corner-top-right-shape: round"); + expect(css).not.toContain("corner-bottom-left-shape: round"); + }); + + it("rounded-lg resets all four via the shorthand", async () => { + const css = await compile(["rounded-lg"]); + expect(css).toContain("corner-shape: round"); + }); + + it("rounded-tl-full resets the corner too", async () => { + const css = await compile(["rounded-tl-full"]); + expect(css).toContain("corner-top-left-shape: round"); + }); + + it("rounded-tl-none needs no reset (a zero radius has no visible corner)", async () => { + const css = await compile(["rounded-tl-none"]); + expect(css).toContain("border-top-left-radius: 0"); + expect(css).not.toContain("corner-top-left-shape: round"); + }); + + // Same specificity, so the reset only wins if it is emitted later. + it("the reset is emitted after the squircle rule it has to override", async () => { + const css = await compile(["squircle-lg", "rounded-tl-lg"]); + expect(css.indexOf("corner-top-left-shape: round")).toBeGreaterThan( + css.indexOf(".squircle-lg {"), + ); + }); + }); + } + + // Tailwind's own rounded-* accepts `(--var)` refs. The generated CSS matches + // them with `--value(…, [*])`; the plugin's matchUtilities cannot (adding + // "any" to its type list disables the utility outright), so that one form + // keeps its shape on the plugin path. + it("css path also resets paren-ref values", async () => { + const css = await compileCss(["rounded-tl-(--my-r)"]); + expect(css).toContain("corner-top-left-shape: round"); + }); +}); diff --git a/package/src/tailwind.ts b/package/src/tailwind.ts index 32d83b7..9b5f67b 100644 --- a/package/src/tailwind.ts +++ b/package/src/tailwind.ts @@ -7,7 +7,9 @@ import plugin from "tailwindcss/plugin"; import { DEFAULT_AMOUNT_VAR_NAME, DEFAULT_R_VAR_NAME, + FULL_RADIUS, NONE_RADIUS, + cornerShapeProp, squircleFullCssObj, SUPPORTS_RULE, squircleCssObj, @@ -75,6 +77,31 @@ const squircle: ReturnType> = }, { type: "length", values: radiusValues }, ); + + // Re-declare the matching rounded-* utility so it also resets the + // corners it owns back to `round`. Tailwind keeps its own definition and + // emits it after this one, so the radius still comes from core and this + // only contributes the reset — the initial value, so it is inert unless + // a squircle class set a shape on the same element. Without it a + // rounded-* utility cannot take a corner back from a squircle, since it + // only ever sets a radius. + const roundedName = suffix ? `rounded-${suffix}` : "rounded"; + const reset = Object.fromEntries(props.map((p) => [cornerShapeProp(p), "round"])); + addUtilities({ + [`.${roundedName}-full`]: { + ...Object.fromEntries(props.map((p) => [p, FULL_RADIUS])), + ...reset, + }, + }); + matchUtilities( + { + [roundedName]: (value: string) => ({ + ...Object.fromEntries(props.map((p) => [p, value])), + ...reset, + }), + }, + { type: "length", values: radiusValues }, + ); } }); diff --git a/package/src/variants.ts b/package/src/variants.ts index 554fff7..8a01759 100644 --- a/package/src/variants.ts +++ b/package/src/variants.ts @@ -19,6 +19,17 @@ export function correctedRadius(radius: string, amt: string = DEFAULT_AMT_CSS): return `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt}))))` as const; } +/** + * The corner-shape property matching a radius property: `border-radius` maps to + * the four-corner `corner-shape` shorthand, `border-top-left-radius` to + * `corner-top-left-shape`, and so on. A utility that owns fewer than four + * corners has to use the longhands, or it reshapes corners whose radius it + * never set — `squircle-t-lg rounded-b-lg` would squircle the bottom corners. + */ +export function cornerShapeProp(radiusProp: string): string { + return radiusProp.replace(/^border-/, "corner-").replace(/radius$/, "shape"); +} + /** * CSS object for the static `-full` utilities. The radius stays uncorrected — * an infinite radius times any positive factor is still infinite, and the @@ -31,9 +42,10 @@ export function squircleFullCssObj( ): CssLikeObject { const obj: CssLikeObject = {}; for (const p of props) obj[p] = FULL_RADIUS; - obj[SUPPORTS_RULE] = { - "corner-shape": getCornerShape(options.amtVar ?? DEFAULT_AMOUNT_VAR_NAME), - }; + const shape = getCornerShape(options.amtVar ?? DEFAULT_AMOUNT_VAR_NAME); + const supportsBlock: Record = {}; + for (const p of props) supportsBlock[cornerShapeProp(p)] = shape; + obj[SUPPORTS_RULE] = supportsBlock; return obj; } @@ -230,7 +242,6 @@ export function squircleCssObj( const cornerShape = getCornerShape(amtVar); const corrected = correctedRadius(radius, amtCss); - const cornerShapeKey = keyCase === "camel" ? "cornerShape" : "corner-shape"; const propKey = (p: string) => (keyCase === "camel" ? toCamel(p) : p); const fallback: Record = {}; @@ -243,7 +254,7 @@ export function squircleCssObj( } else { for (const p of props) supportsBlock[propKey(p)] = corrected; } - supportsBlock[cornerShapeKey] = cornerShape; + for (const p of props) supportsBlock[propKey(cornerShapeProp(p))] = cornerShape; return { ...fallback, From 42c2ea5e9e3115a754e55a6312ae6ee535796ff4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 06:31:33 +0000 Subject: [PATCH 08/10] fix: cover every rounded-* value form, and stop squircle-amt reshaping corners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rounded-* resets only reached theme values and literal arbitrary lengths, so rounded-tl-(--my-radius) and rounded-tl-[var(--x)] silently kept their squircle shape. The plugin now registers the reset with type "any" and no radius of its own — its values map still bounds what matches — and the generated CSS widens --value() with [*]. It keeps declaring the radius there because without a --value() the utility would match any suffix at all, turning a typo like rounded-tl-garbage into a real class; the value it emits is the one core emits anyway. squircle-amt-* set the corner-shape shorthand, so it reshaped all four corners even next to a side variant, and it fought the new rounded-* resets depending on emission order. It now only sets the amount, which is exactly what writing --squircle-amt yourself already did; the amount still reaches each variant through the variable. Same change in the Panda preset. --- README.md | 33 +++----------- package/scripts/generate-squircle-css.ts | 29 +++++++------ package/src/panda.test.ts | 12 ++--- package/src/panda.ts | 10 ++--- package/src/squircle-css.test.ts | 6 +-- package/src/tailwind-corner-shape.test.ts | 53 ++++++++++++++++++++--- package/src/tailwind.test.ts | 8 ++-- package/src/tailwind.ts | 41 +++++------------- 8 files changed, 95 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 67ffed3..5a38ed2 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ The conflicts mirror Tailwind's own `rounded` hierarchy: a later all-corners uti | `squircle-es-*` | `rounded-es-*` | End-start corner (logical) | | `squircle-ee-*` | `rounded-ee-*` | End-end corner (logical) | | `squircle-amt-*` | — | Superellipse exponent (default 2) | +| `squircle-none` | `rounded-none` | Remove rounding | +| `squircle-full` | `rounded-full` | Fully rounded (pill) | ### What values are accepted? @@ -115,7 +117,7 @@ Values are validated strictly so typos fail at build time instead of producing i - **`squircle-*` and its variants** accept the same theme values as `rounded-*` (`sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, plus anything you add to `@theme`) and arbitrary lengths like `squircle-[16px]`. Non-length arbitraries (`[50%]`, `[foo]`) and paren refs (`squircle-(--my-radius)`) are rejected — use a theme key instead. - **`squircle-none` and `squircle-full`** (and their side/corner variants) are static utilities defined the same way as `rounded-none` and `rounded-full`: `0` and `calc(infinity * 1px)`. Neither needs the visual radius correction — correcting zero or infinity is a no-op — so `squircle-full` is just `rounded-full` plus the superellipse corner shape, and `squircle-none` is identical to `rounded-none`. -- **`squircle-amt-*`** accepts bare numbers (`squircle-amt-2`), arbitrary numbers (`squircle-amt-[3.5]`), and theme values. Unit-bearing arbitraries (`[1em]`) and paren refs (`(--my-amt)`) are rejected. +- **`squircle-amt-*`** accepts bare numbers (`squircle-amt-2`), arbitrary numbers (`squircle-amt-[3.5]`), and theme values. Unit-bearing arbitraries (`[1em]`) and paren refs (`(--my-amt)`) are rejected. It only sets the amount — exactly what writing `--squircle-amt` yourself does — and applies no corner shape of its own, so it never reshapes a corner no `squircle-*` utility claimed. ### Mixing squircled and rounded corners @@ -133,7 +135,7 @@ Two things make that work. A `squircle-*` utility only shapes the corners it set Note that a corner reclaimed by `rounded-*` uses the plain radius, not the corrected one — the correction exists only to make a squircle look the same size as a rounded corner, so a genuinely round corner doesn't want it. -One gap: Tailwind's `rounded-*` also accepts paren refs like `rounded-tl-(--my-radius)`. The CSS-import path resets those too; the JS plugin cannot, so on that path such a corner keeps its squircle shape. Use a theme key instead. +Every value form `rounded-*` accepts picks the reset up, including `rounded-tl-[3px]` and paren refs like `rounded-tl-(--my-radius)`. ### What does `squircle-amt-*` control? @@ -620,11 +622,11 @@ If you'd rather not add a dependency, copy the source directly. Click to expand /* squircle-* mirrors rounded-* variants: t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */ /* squircle-*-none and squircle-*-full are static, matching rounded-none and rounded-full */ +/* squircle-amt-[n] only sets the amount — the same thing writing + --squircle-amt yourself does. It applies no corner-shape of its own, so it + never reshapes corners that no squircle-* utility claimed. */ @utility squircle-amt-* { --squircle-amt: --value(--squircle-amt-*, number, [number]); - @supports (corner-shape: superellipse(2)) { - corner-shape: superellipse(var(--squircle-amt)); - } } @utility squircle-none { @@ -972,7 +974,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand visible corner to shape. */ @utility rounded-full { - border-radius: calc(infinity * 1px); corner-shape: round; } @@ -982,8 +983,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-t-full { - border-top-left-radius: calc(infinity * 1px); - border-top-right-radius: calc(infinity * 1px); corner-top-left-shape: round; corner-top-right-shape: round; } @@ -996,8 +995,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-r-full { - border-top-right-radius: calc(infinity * 1px); - border-bottom-right-radius: calc(infinity * 1px); corner-top-right-shape: round; corner-bottom-right-shape: round; } @@ -1010,8 +1007,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-b-full { - border-bottom-left-radius: calc(infinity * 1px); - border-bottom-right-radius: calc(infinity * 1px); corner-bottom-left-shape: round; corner-bottom-right-shape: round; } @@ -1024,8 +1019,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-l-full { - border-top-left-radius: calc(infinity * 1px); - border-bottom-left-radius: calc(infinity * 1px); corner-top-left-shape: round; corner-bottom-left-shape: round; } @@ -1038,8 +1031,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-s-full { - border-start-start-radius: calc(infinity * 1px); - border-end-start-radius: calc(infinity * 1px); corner-start-start-shape: round; corner-end-start-shape: round; } @@ -1052,8 +1043,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-e-full { - border-start-end-radius: calc(infinity * 1px); - border-end-end-radius: calc(infinity * 1px); corner-start-end-shape: round; corner-end-end-shape: round; } @@ -1066,7 +1055,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-tl-full { - border-top-left-radius: calc(infinity * 1px); corner-top-left-shape: round; } @@ -1076,7 +1064,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-tr-full { - border-top-right-radius: calc(infinity * 1px); corner-top-right-shape: round; } @@ -1086,7 +1073,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-br-full { - border-bottom-right-radius: calc(infinity * 1px); corner-bottom-right-shape: round; } @@ -1096,7 +1082,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-bl-full { - border-bottom-left-radius: calc(infinity * 1px); corner-bottom-left-shape: round; } @@ -1106,7 +1091,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-ss-full { - border-start-start-radius: calc(infinity * 1px); corner-start-start-shape: round; } @@ -1116,7 +1100,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-se-full { - border-start-end-radius: calc(infinity * 1px); corner-start-end-shape: round; } @@ -1126,7 +1109,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-es-full { - border-end-start-radius: calc(infinity * 1px); corner-end-start-shape: round; } @@ -1136,7 +1118,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-ee-full { - border-end-end-radius: calc(infinity * 1px); corner-end-end-shape: round; } diff --git a/package/scripts/generate-squircle-css.ts b/package/scripts/generate-squircle-css.ts index 96fb08f..ea94d08 100644 --- a/package/scripts/generate-squircle-css.ts +++ b/package/scripts/generate-squircle-css.ts @@ -3,7 +3,6 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { DEFAULT_AMT, - FULL_RADIUS, NONE_RADIUS, cornerShapeProp, squircleFullCssObj, @@ -19,20 +18,22 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // Support arbitrary, bare, and theme values in one --value() call. // https://tailwindcss.com/docs/adding-custom-styles#functional-utilities const value = "--value(--radius-*, [length])"; -// The rounded-* overrides additionally accept `(--var)` refs, which core -// supports; without [*] those classes would silently miss the shape reset. +// The reset re-declares the radius purely so `--value()` still decides what +// matches. Without it the utility would accept any suffix at all, turning a +// typo like `rounded-tl-garbage` into a real class. `[*]` covers the `(--x)` +// and `[var(--x)]` forms core accepts alongside theme keys and `[3px]`; the +// value it emits is identical to the one core emits for the same class. const roundedValue = "--value(--radius-*, [length], [*])"; /** * Re-declare a `rounded-*` utility so it also resets the corners it owns back - * to `round`. Tailwind keeps its own definition too and emits it after this - * one, so the radius comes from core and this only contributes the reset — - * which is the initial value, hence inert unless a squircle class set a shape - * on the same element. + * to `round`. Tailwind keeps its own definition and emits it alongside this + * one, so this only has to contribute the reset — which is the initial value, + * hence inert unless a squircle class set a shape on the same element. */ -function renderRoundedReset(name: string, props: string[], radius: string): string { +function renderRoundedReset(name: string, props: string[], radius?: string): string { const obj: CssLikeObject = {}; - for (const p of props) obj[p] = radius; + if (radius) for (const p of props) obj[p] = radius; for (const p of props) obj[cornerShapeProp(p)] = "round"; return renderUtility(name, obj); } @@ -70,11 +71,11 @@ function generateCss(): string { /* squircle-* mirrors rounded-* variants: t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */ /* squircle-*-none and squircle-*-full are static, matching rounded-none and rounded-full */ +/* squircle-amt-[n] only sets the amount — the same thing writing + --squircle-amt yourself does. It applies no corner-shape of its own, so it + never reshapes corners that no squircle-* utility claimed. */ @utility squircle-amt-* { --squircle-amt: --value(--squircle-amt-*, number, [number]); - ${SUPPORTS_RULE} { - corner-shape: superellipse(var(--squircle-amt)); - } }`); for (const [suffix, entry] of Object.entries(VARIANTS)) { @@ -106,7 +107,9 @@ function generateCss(): string { if (isComment(entry)) continue; const root = suffix ? `rounded-${suffix}` : "rounded"; - blocks.push(renderRoundedReset(`${root}-full`, entry, FULL_RADIUS)); + // The -full name is static, so it needs no value matching and can stay + // radius-free; the functional one has to keep --value() to stay bounded. + blocks.push(renderRoundedReset(`${root}-full`, entry)); blocks.push(renderRoundedReset(`${root}-*`, entry, roundedValue)); } diff --git a/package/src/panda.test.ts b/package/src/panda.test.ts index 5c8d1a7..a9dd8e0 100644 --- a/package/src/panda.test.ts +++ b/package/src/panda.test.ts @@ -98,7 +98,7 @@ describe("panda preset transform output", () => { `); }); - it("squircleAmount sets the variable and gates corner-shape", () => { + it("squircleAmount sets only the variable, applying no shape of its own", () => { const out = preset.utilities!.extend!["squircleAmount"]!.transform!("3", { token: mockToken, raw: "3", @@ -107,9 +107,6 @@ describe("panda preset transform output", () => { expect(out).toMatchInlineSnapshot(` { "--squircle-amt": "3", - "@supports (corner-shape: superellipse(2))": { - "cornerShape": "superellipse(var(--squircle-amt))", - }, } `); }); @@ -142,11 +139,8 @@ describe("panda preset options", () => { mockArgs("3"), ) as Record; expect(amtOut["--my-amt"]).toBe("3"); - const amtSupports = amtOut["@supports (corner-shape: superellipse(2))"] as Record< - string, - string - >; - expect(amtSupports["cornerShape"]).toBe("superellipse(var(--my-amt))"); + // The amount utility contributes no shape, so the variable is all it emits. + expect(Object.keys(amtOut)).toEqual(["--my-amt"]); const allOutput = JSON.stringify({ radiusOut, amtOut }); expect(allOutput).not.toContain("--squircle-amt"); diff --git a/package/src/panda.ts b/package/src/panda.ts index 8c88928..2566f5e 100644 --- a/package/src/panda.ts +++ b/package/src/panda.ts @@ -55,15 +55,13 @@ export function squirclePandaPreset(options: SquirclePandaPresetOptions = {}) { }; } + // Only sets the amount — the same thing writing the custom property yourself + // does. Applying a corner-shape here would reshape all four corners, + // including ones no squircle utility claimed. utilities["squircleAmount"] = { shorthand: "squircleAmt", values: { type: "number" }, - transform: (value) => ({ - [amtVar]: value, - [SUPPORTS_RULE]: { - cornerShape: `superellipse(var(${amtVar}))`, - }, - }), + transform: (value) => ({ [amtVar]: value }), }; return definePreset({ diff --git a/package/src/squircle-css.test.ts b/package/src/squircle-css.test.ts index f1d44d6..3c25090 100644 --- a/package/src/squircle-css.test.ts +++ b/package/src/squircle-css.test.ts @@ -5,10 +5,10 @@ import { cornerShapeProp } from "./variants"; const { compileCss } = createCompiler(import.meta.dirname); describe("squircle.css utilities", () => { - it("squircle-amt-* sets --squircle-amt and corner-shape", async () => { + it("squircle-amt-* sets only --squircle-amt, applying no shape of its own", async () => { const css = await compileCss(["squircle-amt-2"]); expect(css).toContain("--squircle-amt: 2"); - expect(css).toContain("corner-shape: superellipse(var(--squircle-amt))"); + expect(css).not.toContain("corner-shape:"); }); for (const [suffix, props] of Object.entries(VARIANTS)) { @@ -122,7 +122,7 @@ describe("squircle.css utilities", () => { it("squircle-amt-[4.5] accepts arbitrary bare number", async () => { const css = await compileCss(["squircle-amt-[4.5]"]); expect(css).toContain("--squircle-amt: 4.5"); - expect(css).toContain("corner-shape: superellipse(var(--squircle-amt))"); + expect(css).not.toContain("corner-shape:"); }); it("squircle-amt-[1em] is rejected (unit-bearing values are not numbers)", async () => { diff --git a/package/src/tailwind-corner-shape.test.ts b/package/src/tailwind-corner-shape.test.ts index 53afcf8..dd0cdcc 100644 --- a/package/src/tailwind-corner-shape.test.ts +++ b/package/src/tailwind-corner-shape.test.ts @@ -89,15 +89,54 @@ describe("rounded-* utilities reset the corner shape they own", () => { css.indexOf(".squircle-lg {"), ); }); + + // Every value form Tailwind's own rounded-* accepts has to pick the + // reset up, or a corner silently keeps its squircle shape. + for (const value of ["lg", "[3px]", "[var(--my-r)]", "(--my-r)"]) { + it(`rounded-tl-${value} gets the reset`, async () => { + const css = await compile([`rounded-tl-${value}`]); + expect(css).toContain("corner-top-left-shape: round"); + }); + } + + it("a value Tailwind itself rejects stays unmatched", async () => { + const css = await compile(["rounded-tl-garbage"]); + expect(css).not.toContain("rounded-tl-garbage"); + }); }); } - // Tailwind's own rounded-* accepts `(--var)` refs. The generated CSS matches - // them with `--value(…, [*])`; the plugin's matchUtilities cannot (adding - // "any" to its type list disables the utility outright), so that one form - // keeps its shape on the plugin path. - it("css path also resets paren-ref values", async () => { - const css = await compileCss(["rounded-tl-(--my-r)"]); - expect(css).toContain("corner-top-left-shape: round"); + // The plugin bounds matching with its `values` map, so its reset carries no + // radius. The generated CSS has no such map: it keeps a `--value()` radius + // purely so the utility stays bounded, and emits the value core emits anyway. + it("plugin: the reset rule carries no radius of its own", async () => { + const css = await compilePlugin(["rounded-tl-lg"]); + const resetRule = (css.match(/\.rounded-tl-lg \{[^}]*\}/g) ?? []).find((r) => + r.includes("corner-top-left-shape"), + ); + expect(resetRule).toBeDefined(); + expect(resetRule).not.toContain("border-top-left-radius"); }); }); + +describe("squircle-amt only sets the amount", () => { + for (const [pathName, compile] of paths) { + it(`${pathName}: squircle-amt applies no corner-shape of its own`, async () => { + const css = await compile(["squircle-amt-[2]"]); + expect(css).toContain("--squircle-amt: 2"); + expect(css).not.toContain("corner-shape:"); + }); + + it(`${pathName}: it does not reshape corners a side variant left alone`, async () => { + const css = await compile(["squircle-t-md", "squircle-amt-[3]"]); + expect(css).toContain("--squircle-amt: 3"); + expect(css).not.toContain("corner-bottom"); + expect(css).not.toContain(SHORTHAND_DECL); + }); + + it(`${pathName}: the amount still reaches the variants through the variable`, async () => { + const css = await compile(["squircle-t-md"]); + expect(css).toContain("corner-top-left-shape: superellipse(var(--squircle-amt, 2))"); + }); + } +}); diff --git a/package/src/tailwind.test.ts b/package/src/tailwind.test.ts index 107c1ff..7c9d48d 100644 --- a/package/src/tailwind.test.ts +++ b/package/src/tailwind.test.ts @@ -4,10 +4,10 @@ import { createCompiler, VARIANTS } from "./test-utils"; const { compilePlugin } = createCompiler(import.meta.dirname); describe("plugin.ts utilities", () => { - it("squircle-amt-* sets --squircle-amt and corner-shape", async () => { + it("squircle-amt-* sets only --squircle-amt, applying no shape of its own", async () => { const css = await compilePlugin(["squircle-amt-[2]"]); expect(css).toContain("--squircle-amt: 2"); - expect(css).toContain("corner-shape: superellipse(var(--squircle-amt))"); + expect(css).not.toContain("corner-shape:"); }); for (const [suffix, props] of Object.entries(VARIANTS)) { @@ -148,7 +148,7 @@ describe("plugin.ts custom options", () => { it("custom prefix works for amt utility", async () => { const css = await compilePlugin(["se-amt-[3]"], "prefix: se;"); expect(css).toContain(".se-amt-\\[3\\]"); - expect(css).toContain("corner-shape: superellipse"); + expect(css).toContain("--squircle-amt: 3"); }); it("custom amt-var changes the CSS variable name", async () => { @@ -160,7 +160,7 @@ describe("plugin.ts custom options", () => { it("custom amt-var applies to amt utility", async () => { const css = await compilePlugin(["squircle-amt-[3]"], "amt-var: --se-amt;"); expect(css).toContain("--se-amt: 3"); - expect(css).toContain("superellipse(var(--se-amt))"); + expect(css).not.toContain("corner-shape:"); }); it("custom r-var changes the intermediate CSS variable name", async () => { diff --git a/package/src/tailwind.ts b/package/src/tailwind.ts index 9b5f67b..64b41d5 100644 --- a/package/src/tailwind.ts +++ b/package/src/tailwind.ts @@ -7,11 +7,9 @@ import plugin from "tailwindcss/plugin"; import { DEFAULT_AMOUNT_VAR_NAME, DEFAULT_R_VAR_NAME, - FULL_RADIUS, NONE_RADIUS, cornerShapeProp, squircleFullCssObj, - SUPPORTS_RULE, squircleCssObj, variantEntries, } from "./variants"; @@ -43,15 +41,11 @@ const squircle: ReturnType> = // with the same values Tailwind uses for rounded-none/rounded-full. const { none: _none, full: _full, ...radiusValues } = theme("borderRadius") ?? {}; + // Only sets the amount — the same thing writing the custom property + // yourself does. Applying a corner-shape here would reshape all four + // corners, including ones no squircle-* utility claimed. matchUtilities( - { - [`${prefix}-amt`]: (value: string) => ({ - [amtVar]: value, - [SUPPORTS_RULE]: { - "corner-shape": `superellipse(var(${amtVar}))`, - }, - }), - }, + { [`${prefix}-amt`]: (value: string) => ({ [amtVar]: value }) }, { type: "number" }, ); @@ -80,28 +74,17 @@ const squircle: ReturnType> = // Re-declare the matching rounded-* utility so it also resets the // corners it owns back to `round`. Tailwind keeps its own definition and - // emits it after this one, so the radius still comes from core and this - // only contributes the reset — the initial value, so it is inert unless - // a squircle class set a shape on the same element. Without it a + // emits it alongside this one, so the radius still comes from core and + // this contributes only the reset — the initial value, so it is inert + // unless a squircle class set a shape on the same element. Without it a // rounded-* utility cannot take a corner back from a squircle, since it - // only ever sets a radius. + // only ever sets a radius. Emitting no radius keeps `type: "any"` safe + // and lets every form core accepts — theme key, `[3px]`, `[var(--x)]`, + // `(--x)` — pick the reset up. const roundedName = suffix ? `rounded-${suffix}` : "rounded"; const reset = Object.fromEntries(props.map((p) => [cornerShapeProp(p), "round"])); - addUtilities({ - [`.${roundedName}-full`]: { - ...Object.fromEntries(props.map((p) => [p, FULL_RADIUS])), - ...reset, - }, - }); - matchUtilities( - { - [roundedName]: (value: string) => ({ - ...Object.fromEntries(props.map((p) => [p, value])), - ...reset, - }), - }, - { type: "length", values: radiusValues }, - ); + addUtilities({ [`.${roundedName}-full`]: reset }); + matchUtilities({ [roundedName]: () => reset }, { type: "any", values: radiusValues }); } }); From 21e0b1a5b1a4e47b5fd365421021fc718d9b5fc6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 06:36:12 +0000 Subject: [PATCH 09/10] docs: sync README with the per-corner shape output The StyleX block and the table of contents were stale: scripts/sync-readme.sh calls bare `vp`, and under set -e a shell without it on PATH aborts the script after the CSS blocks but before the StyleX one, silently (its stderr is discarded). CI runs with vp on PATH and caught the difference. --- README.md | 52 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5a38ed2..f6cecd1 100644 --- a/README.md +++ b/README.md @@ -1301,7 +1301,11 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopLeftShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerTopRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1315,7 +1319,11 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopRightShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerBottomRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1329,7 +1337,11 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerBottomLeftShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerBottomRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1343,7 +1355,11 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopLeftShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerBottomLeftShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1357,7 +1373,11 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerStartStartShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerEndStartShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1371,7 +1391,11 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerStartEndShape: { + default: null, + "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, + }, + cornerEndEndShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1381,7 +1405,7 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopLeftShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1391,7 +1415,7 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerTopRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1401,7 +1425,7 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerBottomRightShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1411,7 +1435,7 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerBottomLeftShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1421,7 +1445,7 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerStartStartShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1431,7 +1455,7 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerStartEndShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1441,7 +1465,7 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerEndStartShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, @@ -1451,7 +1475,7 @@ const squircle = stylex.create({ default: radius, "@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`, }, - cornerShape: { + cornerEndEndShape: { default: null, "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`, }, From 3c1ac846078edc56033cc9f894f795159e774d2a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 06:38:29 +0000 Subject: [PATCH 10/10] fix: restrict the rounded-* reset to lengths Matches the validation the squircle-* utilities already document: theme keys and arbitrary lengths, not paren refs or non-length arbitraries. Both paths now accept exactly the same set, so a corner named with a paren ref keeps its squircle shape rather than resetting on one path only. --- README.md | 44 +++++++++++------------ package/scripts/generate-squircle-css.ts | 8 ++--- package/src/tailwind-corner-shape.test.ts | 15 ++++++-- package/src/tailwind.ts | 8 ++--- 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index f6cecd1..40963d8 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Two things make that work. A `squircle-*` utility only shapes the corners it set Note that a corner reclaimed by `rounded-*` uses the plain radius, not the corrected one — the correction exists only to make a squircle look the same size as a rounded corner, so a genuinely round corner doesn't want it. -Every value form `rounded-*` accepts picks the reset up, including `rounded-tl-[3px]` and paren refs like `rounded-tl-(--my-radius)`. +The reset accepts the same values the `squircle-*` utilities do — theme keys and arbitrary lengths like `rounded-tl-[3px]`. Tailwind's own `rounded-*` is looser, so a corner named with a paren ref (`rounded-tl-(--my-radius)`) or a non-length arbitrary still gets its radius from Tailwind but keeps its squircle shape. Use a theme key there, as everywhere else in this package. ### What does `squircle-amt-*` control? @@ -978,7 +978,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-* { - border-radius: --value(--radius-*, [length], [*]); + border-radius: --value(--radius-*, [length]); corner-shape: round; } @@ -988,8 +988,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-t-* { - border-top-left-radius: --value(--radius-*, [length], [*]); - border-top-right-radius: --value(--radius-*, [length], [*]); + border-top-left-radius: --value(--radius-*, [length]); + border-top-right-radius: --value(--radius-*, [length]); corner-top-left-shape: round; corner-top-right-shape: round; } @@ -1000,8 +1000,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-r-* { - border-top-right-radius: --value(--radius-*, [length], [*]); - border-bottom-right-radius: --value(--radius-*, [length], [*]); + border-top-right-radius: --value(--radius-*, [length]); + border-bottom-right-radius: --value(--radius-*, [length]); corner-top-right-shape: round; corner-bottom-right-shape: round; } @@ -1012,8 +1012,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-b-* { - border-bottom-left-radius: --value(--radius-*, [length], [*]); - border-bottom-right-radius: --value(--radius-*, [length], [*]); + border-bottom-left-radius: --value(--radius-*, [length]); + border-bottom-right-radius: --value(--radius-*, [length]); corner-bottom-left-shape: round; corner-bottom-right-shape: round; } @@ -1024,8 +1024,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-l-* { - border-top-left-radius: --value(--radius-*, [length], [*]); - border-bottom-left-radius: --value(--radius-*, [length], [*]); + border-top-left-radius: --value(--radius-*, [length]); + border-bottom-left-radius: --value(--radius-*, [length]); corner-top-left-shape: round; corner-bottom-left-shape: round; } @@ -1036,8 +1036,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-s-* { - border-start-start-radius: --value(--radius-*, [length], [*]); - border-end-start-radius: --value(--radius-*, [length], [*]); + border-start-start-radius: --value(--radius-*, [length]); + border-end-start-radius: --value(--radius-*, [length]); corner-start-start-shape: round; corner-end-start-shape: round; } @@ -1048,8 +1048,8 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-e-* { - border-start-end-radius: --value(--radius-*, [length], [*]); - border-end-end-radius: --value(--radius-*, [length], [*]); + border-start-end-radius: --value(--radius-*, [length]); + border-end-end-radius: --value(--radius-*, [length]); corner-start-end-shape: round; corner-end-end-shape: round; } @@ -1059,7 +1059,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-tl-* { - border-top-left-radius: --value(--radius-*, [length], [*]); + border-top-left-radius: --value(--radius-*, [length]); corner-top-left-shape: round; } @@ -1068,7 +1068,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-tr-* { - border-top-right-radius: --value(--radius-*, [length], [*]); + border-top-right-radius: --value(--radius-*, [length]); corner-top-right-shape: round; } @@ -1077,7 +1077,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-br-* { - border-bottom-right-radius: --value(--radius-*, [length], [*]); + border-bottom-right-radius: --value(--radius-*, [length]); corner-bottom-right-shape: round; } @@ -1086,7 +1086,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-bl-* { - border-bottom-left-radius: --value(--radius-*, [length], [*]); + border-bottom-left-radius: --value(--radius-*, [length]); corner-bottom-left-shape: round; } @@ -1095,7 +1095,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-ss-* { - border-start-start-radius: --value(--radius-*, [length], [*]); + border-start-start-radius: --value(--radius-*, [length]); corner-start-start-shape: round; } @@ -1104,7 +1104,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-se-* { - border-start-end-radius: --value(--radius-*, [length], [*]); + border-start-end-radius: --value(--radius-*, [length]); corner-start-end-shape: round; } @@ -1113,7 +1113,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-es-* { - border-end-start-radius: --value(--radius-*, [length], [*]); + border-end-start-radius: --value(--radius-*, [length]); corner-end-start-shape: round; } @@ -1122,7 +1122,7 @@ If you'd rather not add a dependency, copy the source directly. Click to expand } @utility rounded-ee-* { - border-end-end-radius: --value(--radius-*, [length], [*]); + border-end-end-radius: --value(--radius-*, [length]); corner-end-end-shape: round; } ``` diff --git a/package/scripts/generate-squircle-css.ts b/package/scripts/generate-squircle-css.ts index ea94d08..f308009 100644 --- a/package/scripts/generate-squircle-css.ts +++ b/package/scripts/generate-squircle-css.ts @@ -20,10 +20,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const value = "--value(--radius-*, [length])"; // The reset re-declares the radius purely so `--value()` still decides what // matches. Without it the utility would accept any suffix at all, turning a -// typo like `rounded-tl-garbage` into a real class. `[*]` covers the `(--x)` -// and `[var(--x)]` forms core accepts alongside theme keys and `[3px]`; the -// value it emits is identical to the one core emits for the same class. -const roundedValue = "--value(--radius-*, [length], [*])"; +// typo like `rounded-tl-garbage` into a real class. It accepts the same values +// the squircle-* utilities do — theme keys and arbitrary lengths, not paren +// refs — and the radius it emits is the one core emits for the same class. +const roundedValue = value; /** * Re-declare a `rounded-*` utility so it also resets the corners it owns back diff --git a/package/src/tailwind-corner-shape.test.ts b/package/src/tailwind-corner-shape.test.ts index dd0cdcc..d930d57 100644 --- a/package/src/tailwind-corner-shape.test.ts +++ b/package/src/tailwind-corner-shape.test.ts @@ -90,15 +90,24 @@ describe("rounded-* utilities reset the corner shape they own", () => { ); }); - // Every value form Tailwind's own rounded-* accepts has to pick the - // reset up, or a corner silently keeps its squircle shape. - for (const value of ["lg", "[3px]", "[var(--my-r)]", "(--my-r)"]) { + // The reset accepts the same values the squircle-* utilities do: theme + // keys and arbitrary lengths, nothing else. Anything wider would reset + // corners for values this package tells you not to write. + for (const value of ["lg", "[3px]"]) { it(`rounded-tl-${value} gets the reset`, async () => { const css = await compile([`rounded-tl-${value}`]); expect(css).toContain("corner-top-left-shape: round"); }); } + // A corner named this way keeps its squircle shape — use a theme key. + for (const value of ["[var(--my-r)]", "(--my-r)", "[50%]", "[foo]"]) { + it(`rounded-tl-${value} gets no reset (not a length)`, async () => { + const css = await compile([`rounded-tl-${value}`]); + expect(css).not.toContain("corner-top-left-shape: round"); + }); + } + it("a value Tailwind itself rejects stays unmatched", async () => { const css = await compile(["rounded-tl-garbage"]); expect(css).not.toContain("rounded-tl-garbage"); diff --git a/package/src/tailwind.ts b/package/src/tailwind.ts index 64b41d5..cf9bb86 100644 --- a/package/src/tailwind.ts +++ b/package/src/tailwind.ts @@ -78,13 +78,13 @@ const squircle: ReturnType> = // this contributes only the reset — the initial value, so it is inert // unless a squircle class set a shape on the same element. Without it a // rounded-* utility cannot take a corner back from a squircle, since it - // only ever sets a radius. Emitting no radius keeps `type: "any"` safe - // and lets every form core accepts — theme key, `[3px]`, `[var(--x)]`, - // `(--x)` — pick the reset up. + // only ever sets a radius. Restricted to lengths, the same values the + // squircle-* utilities accept: a paren ref keeps its squircle shape, so + // reach for a theme key there as everywhere else in this package. const roundedName = suffix ? `rounded-${suffix}` : "rounded"; const reset = Object.fromEntries(props.map((p) => [cornerShapeProp(p), "round"])); addUtilities({ [`.${roundedName}-full`]: reset }); - matchUtilities({ [roundedName]: () => reset }, { type: "any", values: radiusValues }); + matchUtilities({ [roundedName]: () => reset }, { type: "length", values: radiusValues }); } });