Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 Klink
Copyright (c) 2026 Chris Klink

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,11 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
<!-- BEGIN:dist/tailwind/utils.css -->

```css
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/

/* ── 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 */
Expand Down Expand Up @@ -758,6 +763,11 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
<!-- BEGIN:dist/radius-function.css -->

```css
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/

/* ── squircle-radius() ────────────────────────────────────────
* Computes the visually-corrected border-radius for use with
* corner-shape: superellipse(). A superellipse corner appears
Expand Down Expand Up @@ -790,6 +800,11 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
<summary><strong><code>squircleMergeConfig</code></strong> — tailwind-merge conflict config</summary>

```js
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/

import { extendTailwindMerge } from "tailwind-merge";

const allRoundedGroups = [
Expand Down Expand Up @@ -837,6 +852,10 @@ export const twMerge = extendTailwindMerge(squircleMergeConfig);

````js
import * as stylex from "@stylexjs/stylex";
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/
/**
* StyleX squircle utilities — generated from this template by
* `scripts/generate-stylex.ts`.
Expand Down
6 changes: 6 additions & 0 deletions package/scripts/generate-squircle-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ function renderUtility(name: string, props: string[]): string {
function generateCss(): string {
const blocks: string[] = [];

blocks.push(`\
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/`);

blocks.push(`\
/* ── Squircle utilities ─────────────────────────────────────── */
/* squircle-amt-[n] sets the superellipse amount (default ${DEFAULT_AMT}) */
Expand Down
55 changes: 32 additions & 23 deletions package/src/panda.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ describe("panda preset shape", () => {

it("registers a utility for every CAMEL_VARIANTS entry plus squircleAmount", () => {
const keys = Object.keys(preset.utilities!.extend!).sort();
const expected = [
...CAMEL_VARIANTS.map((v) => v.property),
"squircleAmount",
].sort();
const expected = [...CAMEL_VARIANTS.map((v) => v.property), "squircleAmount"].sort();
expect(keys).toEqual(expected);
});

Expand Down Expand Up @@ -119,23 +116,35 @@ describe("panda preset transform output", () => {

describe("panda preset options", () => {
const mockToken = Object.assign(() => "1rem", { raw: () => undefined }) as any;
const mockArgs = (raw: string) => ({ token: mockToken, raw, utils: { colorMix: () => ({ invalid: true, value: "" }) } });
const mockArgs = (raw: string) => ({
token: mockToken,
raw,
utils: { colorMix: () => ({ invalid: true, value: "" }) },
});

it("custom amtVar lands in every place the default --squircle-amt does", () => {
const preset = squirclePandaPreset({ amtVar: "--my-amt" });

const radiusOut = preset.utilities!.extend!["squircleRadius"]!.transform!("1rem", mockArgs("1rem")) as Record<string, unknown>;
const radiusSupports = radiusOut[
"@supports (corner-shape: superellipse(2))"
] as Record<string, string>;
const radiusOut = preset.utilities!.extend!["squircleRadius"]!.transform!(
"1rem",
mockArgs("1rem"),
) as Record<string, unknown>;
const radiusSupports = radiusOut["@supports (corner-shape: superellipse(2))"] as Record<
string,
string
>;
expect(radiusSupports["--squircle-r"]).toContain("var(--my-amt, 2)");
expect(radiusSupports["cornerShape"]).toBe("superellipse(var(--my-amt, 2))");

const amtOut = preset.utilities!.extend!["squircleAmount"]!.transform!("3", mockArgs("3")) as Record<string, unknown>;
const amtOut = preset.utilities!.extend!["squircleAmount"]!.transform!(
"3",
mockArgs("3"),
) as Record<string, unknown>;
expect(amtOut["--my-amt"]).toBe("3");
const amtSupports = amtOut[
"@supports (corner-shape: superellipse(2))"
] as Record<string, string>;
const amtSupports = amtOut["@supports (corner-shape: superellipse(2))"] as Record<
string,
string
>;
expect(amtSupports["cornerShape"]).toBe("superellipse(var(--my-amt))");

const allOutput = JSON.stringify({ radiusOut, amtOut });
Expand All @@ -144,11 +153,11 @@ describe("panda preset options", () => {

it("custom rVar threads through multi-prop side transforms", () => {
const preset = squirclePandaPreset({ rVar: "--my-r" });
const out = preset.utilities!.extend!["squircleTopRadius"]!.transform!("1rem", mockArgs("1rem")) as Record<string, unknown>;
const supports = out["@supports (corner-shape: superellipse(2))"] as Record<
string,
string
>;
const out = preset.utilities!.extend!["squircleTopRadius"]!.transform!(
"1rem",
mockArgs("1rem"),
) as Record<string, unknown>;
const supports = out["@supports (corner-shape: superellipse(2))"] as Record<string, string>;
expect(supports["--my-r"]).toContain("calc(1rem");
expect(supports["borderTopLeftRadius"]).toBe("var(--my-r)");
expect(supports["borderTopRightRadius"]).toBe("var(--my-r)");
Expand All @@ -157,11 +166,11 @@ describe("panda preset options", () => {

it("amtVar and rVar can both be overridden together", () => {
const preset = squirclePandaPreset({ amtVar: "--a", rVar: "--r" });
const out = preset.utilities!.extend!["squircleRadius"]!.transform!("1rem", mockArgs("1rem")) as Record<string, unknown>;
const supports = out["@supports (corner-shape: superellipse(2))"] as Record<
string,
string
>;
const out = preset.utilities!.extend!["squircleRadius"]!.transform!(
"1rem",
mockArgs("1rem"),
) as Record<string, unknown>;
const supports = out["@supports (corner-shape: superellipse(2))"] as Record<string, string>;
expect(supports["--r"]).toContain("var(--a, 2)");
expect(supports["borderRadius"]).toBe("var(--r)");
expect(supports["cornerShape"]).toBe("superellipse(var(--a, 2))");
Expand Down
12 changes: 7 additions & 5 deletions package/src/panda.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/

import { definePreset, type PropertyConfig } from "@pandacss/dev";
import {
CAMEL_VARIANTS,
Expand Down Expand Up @@ -31,9 +36,7 @@ export interface SquirclePandaPresetOptions {
* `squircleTopLeft` mirror `roundedTopLeft`. The shape table is identical to
* Panda's built-in radius utilities.
*/
export function squirclePandaPreset(
options: SquirclePandaPresetOptions = {},
) {
export function squirclePandaPreset(options: SquirclePandaPresetOptions = {}) {
const amtVar = options.amtVar ?? DEFAULT_AMOUNT_VAR_NAME;
const rVar = options.rVar ?? "--squircle-r";

Expand All @@ -48,8 +51,7 @@ export function squirclePandaPreset(
utilities[variant.property] = {
shorthand: variant.shorthand,
values: "radii",
transform: (value) =>
squircleCssObj(props, value, { amtVar, rVar, case: "camel" }) as any,
transform: (value) => squircleCssObj(props, value, { amtVar, rVar, case: "camel" }) as any,
};
}

Expand Down
1 change: 0 additions & 1 deletion package/src/squircle-css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,4 @@ describe("squircle.css utilities", () => {
expect(css).not.toContain("squircle-amt-");
});
});

});
5 changes: 5 additions & 0 deletions package/src/squircle-radius.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/

/* ── squircle-radius() ────────────────────────────────────────
* Computes the visually-corrected border-radius for use with
* corner-shape: superellipse(). A superellipse corner appears
Expand Down
5 changes: 5 additions & 0 deletions package/src/stylex.template.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/

import * as stylex from "@stylexjs/stylex";

/**
Expand Down
5 changes: 5 additions & 0 deletions package/src/stylex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
// To modify variant shape, edit `renderVariant` in the generator.
// To add/rename variants, edit `CAMEL_VARIANTS` and `VARIANTS` in src/variants.ts.

/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/

import * as stylex from "@stylexjs/stylex";

/**
Expand Down
57 changes: 31 additions & 26 deletions package/src/tailwind.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/

import plugin from "tailwindcss/plugin";
import {
DEFAULT_AMOUNT_VAR_NAME,
Expand Down Expand Up @@ -26,37 +31,37 @@ const squircle: ReturnType<typeof plugin.withOptions<SquirclePluginOptions>> =
plugin.withOptions<SquirclePluginOptions>((options = {}) =>
// eslint-disable-next-line @typescript-eslint/unbound-method
({ 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");
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");

matchUtilities(
{
[`${prefix}-amt`]: (value: string) => ({
[amtVar]: value,
[SUPPORTS_RULE]: {
"corner-shape": `superellipse(var(${amtVar}))`,
},
}),
},
{ type: "number" },
);

for (const [suffix, props] of variantEntries()) {
const name = suffix ? `${prefix}-${suffix}` : prefix;
matchUtilities(
{
[`${prefix}-amt`]: (value: string) => ({
[amtVar]: value,
[SUPPORTS_RULE]: {
"corner-shape": `superellipse(var(${amtVar}))`,
},
}),
[name]: (value: string) =>
squircleCssObj(props, value, { amtVar, rVar }) as Record<
string,
string | Record<string, string>
>,
},
{ type: "number" },
{ type: "length", values: radiusValues },
);

for (const [suffix, props] of variantEntries()) {
const name = suffix ? `${prefix}-${suffix}` : prefix;
matchUtilities(
{
[name]: (value: string) =>
squircleCssObj(props, value, { amtVar, rVar }) as Record<
string,
string | Record<string, string>
>,
},
{ type: "length", values: radiusValues },
);
}
});
}
});

export default squircle;

Expand Down
3 changes: 2 additions & 1 deletion package/src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ async function loadModule(id: string, base: string, _resourceHint: "plugin" | "c
}

function extractUtilitiesLayer(css: string): string {
const match = /@layer utilities \{([\s\S]*)\}\s*$/.exec(css);
// Tailwind preserves /*! ... */ license comments after the final layer.
const match = /@layer utilities \{([\s\S]*)\}\s*(?:\/\*![\s\S]*?\*\/\s*)*$/.exec(css);
return match?.[1]?.trim() ?? "";
}

Expand Down
10 changes: 9 additions & 1 deletion package/src/variants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*!
* @klinking/squircle — MIT License — Copyright (c) 2026 Chris Klink
* https://squircle.klink.ing/ · https://github.com/klink-ing/squircle
*/

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;
Expand Down Expand Up @@ -153,7 +158,10 @@ function toCamel(kebab: string): string {
return camel;
}

export type CssLikeObject = Record<string, string | number | null | Record<string, string | number | null>>;
export type CssLikeObject = Record<
string,
string | number | null | Record<string, string | number | null>
>;

export interface SquircleCssObjOptions {
/** Override the `--squircle-amt` variable name (default `--squircle-amt`). */
Expand Down
8 changes: 1 addition & 7 deletions package/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,7 @@ export default defineConfig({
},
test: {
command: "echo 'All tests passed'",
dependsOn: [
"test:tailwind",
"test:css",
"test:radius",
"test:panda",
"test:stylex",
],
dependsOn: ["test:tailwind", "test:css", "test:radius", "test:panda", "test:stylex"],
},
"generate:stylex": {
command: "tsx scripts/generate-stylex.ts",
Expand Down
3 changes: 1 addition & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ export default defineConfig({
cache: true,
tasks: {
fmt: {
command:
'bash -c \'OUT=$(vp fmt --check 2>&1); echo "$OUT"; ! echo "$OUT" | grep -q "Unable to load plugin"\'',
command: "vp fmt --check",
dependsOn: ["@klinking/squircle#build"],
},
lint: {
Expand Down
5 changes: 1 addition & 4 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ function stylexForExternalModules() {
enforce: "pre",
async transform(code, id) {
if (!/\.m?jsx?$|\.tsx?$/.test(id)) return null;
if (
!id.includes("/package/dist/stylex/index.mjs") &&
!id.includes(".stylex.")
) {
if (!id.includes("/package/dist/stylex/index.mjs") && !id.includes(".stylex.")) {
return null;
}
const result = await babel.transformAsync(code, {
Expand Down
8 changes: 2 additions & 6 deletions website/examples/stylex/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ export default function App() {
return (
<div className="min-h-screen bg-zinc-950 p-10 text-zinc-100">
<h1 className="mb-2 text-2xl font-bold">squircle StyleX Demo</h1>
<p className="mb-8 text-zinc-400">
Squircle utilities authored as StyleX dynamic styles.
</p>
<p className="mb-8 text-zinc-400">Squircle utilities authored as StyleX dynamic styles.</p>

<div className="space-y-10">
<section>
Expand Down Expand Up @@ -87,9 +85,7 @@ export default function App() {

<section>
<h2 className="mb-4 text-lg font-semibold text-zinc-300">Squircle amount</h2>
<p className="mb-4 text-sm text-zinc-500">
Higher = more square. Default is 2.
</p>
<p className="mb-4 text-sm text-zinc-500">Higher = more square. Default is 2.</p>
<div className="flex flex-wrap gap-6">
<Box
label="squircle.all(radii['3xl'], 1)"
Expand Down
Loading