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
533 changes: 470 additions & 63 deletions README.md

Large diffs are not rendered by default.

72 changes: 63 additions & 9 deletions package/scripts/generate-squircle-css.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
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,
NONE_RADIUS,
cornerShapeProp,
squircleFullCssObj,
SUPPORTS_RULE,
VARIANTS,
isComment,
squircleCssObj,
type CssLikeObject,
} from "../src/variants";

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 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. 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;

function renderUtility(name: string, props: string[]): string {
const obj = squircleCssObj(props, value);
/**
* Re-declare a `rounded-*` utility so it also resets the corners it owns back
* 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 {
const obj: CssLikeObject = {};
if (radius) 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} {`];

for (const [key, val] of Object.entries(obj)) {
Expand Down Expand Up @@ -40,13 +68,14 @@ 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 */

/* 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)) {
Expand All @@ -55,8 +84,33 @@ 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`, squircleFullCssObj(entry)));
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";
// 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));
}

return blocks.join("\n\n") + "\n";
Expand Down
29 changes: 23 additions & 6 deletions package/scripts/generate-stylex.ts
Original file line number Diff line number Diff line change
@@ -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));

Expand Down Expand Up @@ -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.
Expand All @@ -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");
}
Expand Down
34 changes: 20 additions & 14 deletions package/src/__snapshots__/squircle-css.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Expand All @@ -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));
}
}"
`;
Loading
Loading