Skip to content

Commit c417c8b

Browse files
dogmarclaude
andcommitted
WIP: panda preset + stylex investigation
Panda preset: definePreset-style export with the full 15-utility radius matrix, naming follows Panda's borderRadius / rounded convention. Tests + snapshots cover shape, transform output, custom amtVar/rVar options. StyleX: helpers + factory written but parked. Real @stylexjs/babel-plugin probes confirmed the static analyzer rejects every function call and computed @-rule key inside stylex.create(). Boundary tests in squircle.stylex.compile.test.ts pin the failure modes for future re-evaluation. Vanilla CSS sidecar (dist/squircle.css) added as the practical workaround. Full research + recommended paths forward captured in package/docs/stylex-research.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c42a410 commit c417c8b

15 files changed

Lines changed: 1590 additions & 161 deletions

‎README.md‎

Lines changed: 110 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ We're all excited about `corner-shape: squircle`, but we're in a pickle right no
99
## Contents
1010

1111
<!-- BEGIN:toc -->
12-
1312
- [Requirements](#requirements)
1413
- [Install & setup](#install--setup)
1514
- [Utilities](#utilities)
@@ -43,7 +42,7 @@ We're all excited about `corner-shape: squircle`, but we're in a pickle right no
4342
npm install @klinking/squircle
4443
```
4544

46-
Then pick one of two integration paths. But don't pick wrong, else the Integration Ogre might… oh wait, no, just pick the one that suits your needs, they're essentially the same, but one allows more customization, in case my vars and classes conflict with ur existing vars and classes.
45+
Then pick the integration path that fits your project. Tailwind is the original target (paths A and B). Path C is the [Panda CSS](https://panda-css.com/) preset, and path D is the [StyleX](https://stylexjs.com/) helpers — both produce the exact same `@supports`-gated visual correction, just wired through their respective build pipelines.
4746

4847
### Path A: CSS import (recommended)
4948

@@ -88,6 +87,106 @@ const twMerge = extendTailwindMerge(squircleMergeConfig, {
8887
});
8988
```
9089

90+
### Path C: Panda CSS preset
91+
92+
For [Panda CSS](https://panda-css.com/) projects, register the preset in `panda.config.ts`:
93+
94+
```ts
95+
import { defineConfig } from "@pandacss/dev";
96+
import squirclePreset from "@klinking/squircle/panda-preset";
97+
98+
export default defineConfig({
99+
presets: ["@pandacss/dev/presets", squirclePreset()],
100+
// ...
101+
});
102+
```
103+
104+
Then use the utilities anywhere `css(...)` accepts properties:
105+
106+
```tsx
107+
<div className={css({ squircle: "md", padding: "4" })} />
108+
<div className={css({ squircleTopLeft: "lg", squircleAmt: 3 })} />
109+
```
110+
111+
The naming follows Panda's own border-radius convention exactly — substitute `border` ↔ `squircle` and `rounded` ↔ `squircle` (the shorthand) and the table is identical to Panda's:
112+
113+
| Full property name | Shorthand | CSS targets |
114+
| ------------------------------- | ---------------------- | ---------------------------------------- |
115+
| `squircleRadius` | `squircle` | `border-radius` (all four corners) |
116+
| `squircleTopRadius` | `squircleTop` | top corners |
117+
| `squircleRightRadius` | `squircleRight` | right corners |
118+
| `squircleBottomRadius` | `squircleBottom` | bottom corners |
119+
| `squircleLeftRadius` | `squircleLeft` | left corners |
120+
| `squircleStartRadius` | `squircleStart` | inline-start corners (logical) |
121+
| `squircleEndRadius` | `squircleEnd` | inline-end corners (logical) |
122+
| `squircleTopLeftRadius` | `squircleTopLeft` | top-left corner |
123+
| `squircleTopRightRadius` | `squircleTopRight` | top-right corner |
124+
| `squircleBottomRightRadius` | `squircleBottomRight` | bottom-right corner |
125+
| `squircleBottomLeftRadius` | `squircleBottomLeft` | bottom-left corner |
126+
| `squircleStartStartRadius` | `squircleStartStart` | start-start corner (logical) |
127+
| `squircleStartEndRadius` | `squircleStartEnd` | start-end corner (logical) |
128+
| `squircleEndStartRadius` | `squircleEndStart` | end-start corner (logical) |
129+
| `squircleEndEndRadius` | `squircleEndEnd` | end-end corner (logical) |
130+
| `squircleAmount` | `squircleAmt` | superellipse exponent (default 2) |
131+
132+
All radius utilities resolve through your `radii` theme tokens, so `squircle: "md"` reads the same `--radii-md` your `borderRadius: "md"` does. The preset also registers a `_squircleSupported` condition (`@supports (corner-shape: superellipse(2))`) for one-off overrides.
133+
134+
The preset accepts the same `amtVar` / `rVar` options as the Tailwind plugin if you need to rename the underlying CSS variables:
135+
136+
```ts
137+
squirclePreset({ amtVar: "--my-amt", rVar: "--my-r" });
138+
```
139+
140+
Panda is usage-driven — utilities only appear in the output for properties found in scanned source. If you want every variant emitted unconditionally, opt in via Panda's [`staticCss`](https://panda-css.com/docs/guides/static-css).
141+
142+
### Path D: StyleX
143+
144+
For [StyleX](https://stylexjs.com/) projects, the package exposes parametric helpers and a factory — there's no baked-in radius set; you bring your own theme via `stylex.defineVars(...)`.
145+
146+
**Bundler setup:** add the package path to your StyleX bundler plugin's `include` config so the helpers are processed:
147+
148+
```ts
149+
// vite.config.ts (or your StyleX plugin equivalent)
150+
stylexPlugin({
151+
include: ["src/**/*", /node_modules\/@klinking\/squircle/],
152+
});
153+
```
154+
155+
**Factory pattern** — point at your own `defineVars` output and `stylex.create` the result:
156+
157+
```ts
158+
import * as stylex from "@stylexjs/stylex";
159+
import { defineSquircleVariants } from "@klinking/squircle/stylex";
160+
161+
const radii = stylex.defineVars({
162+
sm: "0.25rem",
163+
md: "0.375rem",
164+
lg: "0.5rem",
165+
xl: "0.75rem",
166+
});
167+
168+
const squircleStyles = stylex.create(defineSquircleVariants(radii));
169+
170+
<div {...stylex.props(squircleStyles.md)} />;
171+
```
172+
173+
Pass `{ side: "topLeft" }` (or `"top"`, `"start"`, etc.) to target a per-side or per-corner property instead of `borderRadius`.
174+
175+
**Parametric helpers** — for ad-hoc use, spread the helper output into any `stylex.create(...)` block:
176+
177+
```ts
178+
import { squircle, squircleTopLeft } from "@klinking/squircle/stylex";
179+
180+
const styles = stylex.create({
181+
card: { ...squircle("1rem"), padding: 16 },
182+
badge: squircleTopLeft("0.5rem"),
183+
});
184+
```
185+
186+
The full helper set mirrors the Panda table above (`squircle`, `squircleTop`, …, `squircleEndEnd`). Each one returns a StyleX style object whose `borderRadius` (or per-corner equivalent) and `cornerShape` properties carry per-property `@supports (corner-shape: superellipse(2))` conditional values, so unsupported browsers fall back to plain `border-radius`.
187+
188+
Override the superellipse exponent at call time by passing a second argument: `squircle("1rem", 3)` (number) or `squircle("1rem", "var(--my-amt, 2)")` (var expression). The CSS variable name read by default (`--squircle-amt`) is exported as `SQUIRCLE_AMOUNT_VAR`.
189+
91190
## Utilities
92191

93192
| Utility | Equivalent | Description |
@@ -355,7 +454,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
355454
<summary><strong><code>tw-utils.css</code></strong> — the Tailwind utilities</summary>
356455

357456
<!-- BEGIN:dist/tw-utils.css -->
358-
359457
```css
360458
/* ── Squircle utilities ─────────────────────────────────────── */
361459
/* squircle-amt-[n] sets the superellipse amount (default 2) */
@@ -515,7 +613,6 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
515613
}
516614
}
517615
```
518-
519616
<!-- END:dist/tw-utils.css -->
520617

521618
</details>
@@ -524,90 +621,26 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
524621
<summary><strong><code>tw-plugin.mjs</code></strong> — the JS plugin</summary>
525622

526623
<!-- BEGIN:dist/tw-plugin.mjs -->
527-
528-
````js
624+
```js
625+
import { c as variantEntries, i as SUPPORTS_RULE, s as squircleCssObj } from "./variants-vQRRK8yy.mjs";
529626
import plugin from "tailwindcss/plugin";
530-
const DEFAULT_AMOUNT_VAR_NAME = "--squircle-amt";
531-
const DEFAULT_AMT_CSS = `var(${DEFAULT_AMOUNT_VAR_NAME}, 2)`;
532-
const getCornerShape = (varName = DEFAULT_AMOUNT_VAR_NAME) => `superellipse(var(${varName}, 2))`;
533-
function correctedRadius(radius, amt = DEFAULT_AMT_CSS) {
534-
return `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt}))))`;
535-
}
536-
function isComment(entry) {
537-
return !Array.isArray(entry);
538-
}
539-
const SUPPORTS_RULE = "@supports (corner-shape: superellipse(2))";
540-
const VARIANTS = {
541-
"": ["border-radius"],
542-
"$comment-physical-sides": { comment: "/* --- Per-side physical variants --- */" },
543-
t: ["border-top-left-radius", "border-top-right-radius"],
544-
r: ["border-top-right-radius", "border-bottom-right-radius"],
545-
b: ["border-bottom-left-radius", "border-bottom-right-radius"],
546-
l: ["border-top-left-radius", "border-bottom-left-radius"],
547-
"$comment-logical-sides": { comment: "/* --- Per-side logical variants --- */" },
548-
s: ["border-start-start-radius", "border-end-start-radius"],
549-
e: ["border-start-end-radius", "border-end-end-radius"],
550-
"$comment-physical-corners": { comment: "/* --- Per-corner physical variants --- */" },
551-
tl: ["border-top-left-radius"],
552-
tr: ["border-top-right-radius"],
553-
br: ["border-bottom-right-radius"],
554-
bl: ["border-bottom-left-radius"],
555-
"$comment-logical-corners": { comment: "/* --- Per-corner logical variants --- */" },
556-
ss: ["border-start-start-radius"],
557-
se: ["border-start-end-radius"],
558-
es: ["border-end-start-radius"],
559-
ee: ["border-end-end-radius"]
560-
};
561-
function variantEntries() {
562-
return Object.entries(VARIANTS).filter((entry) => !isComment(entry[1]));
563-
}
564-
function usesIntermediateVar(suffix) {
565-
const entry = VARIANTS[suffix];
566-
if (!entry || isComment(entry)) return false;
567-
return suffix === "" || entry.length > 1;
568-
}
569-
//#endregion
570627
//#region src/tw-plugin.ts
571628
const squircle = plugin.withOptions((options = {}) => ({ matchUtilities, theme }) => {
572629
const amtVar = options.amtVar ?? options["amt-var"] ?? "--squircle-amt";
573630
const rVar = options.rVar ?? options["r-var"] ?? "--squircle-r";
574631
const prefix = options.prefix ?? "squircle";
575632
const radiusValues = theme("borderRadius");
576-
const amtCss = `var(${amtVar}, 2)`;
577-
const rCss = `var(${rVar})`;
578-
const cornerShape = getCornerShape(amtVar);
579633
matchUtilities({ [`${prefix}-amt`]: (value) => ({
580634
[amtVar]: value,
581635
[SUPPORTS_RULE]: { "corner-shape": `superellipse(var(${amtVar}))` }
582636
}) }, { type: "number" });
583-
for (const [suffix, props] of variantEntries()) {
584-
const name = suffix ? `${prefix}-${suffix}` : prefix;
585-
if (usesIntermediateVar(suffix)) matchUtilities({ [name]: (value) => ({
586-
...Object.fromEntries(props.map((p) => [p, value])),
587-
[SUPPORTS_RULE]: {
588-
[rVar]: correctedRadius(value, amtCss),
589-
...Object.fromEntries(props.map((p) => [p, rCss])),
590-
"corner-shape": cornerShape
591-
}
592-
}) }, {
593-
type: "length",
594-
values: radiusValues
595-
});
596-
else {
597-
const prop = props[0];
598-
matchUtilities({ [name]: (value) => {
599-
const result = { [prop]: value };
600-
result[SUPPORTS_RULE] = {
601-
[prop]: correctedRadius(value, amtCss),
602-
"corner-shape": cornerShape
603-
};
604-
return result;
605-
} }, {
606-
type: "length",
607-
values: radiusValues
608-
});
609-
}
610-
}
637+
for (const [suffix, props] of variantEntries()) matchUtilities({ [suffix ? `${prefix}-${suffix}` : prefix]: (value) => squircleCssObj(props, value, {
638+
amtVar,
639+
rVar
640+
}) }, {
641+
type: "length",
642+
values: radiusValues
643+
});
611644
});
612645
//#endregion
613646
export { squircle as default };

‎package/docs/stylex-research.md‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# StyleX support — research notes
2+
3+
Status: **paused**, picked back up on this branch (`feat/stylex-support`). The Panda preset shipped on the parent branch; StyleX needs more design thought before merging.
4+
5+
## Why this is paused
6+
7+
StyleX's babel/swc plugin requires `stylex.create(...)` to receive a fully-static object literal. **Every "factory" or "helper that returns a style object" pattern is rejected at compile time.** Verified directly against `@stylexjs/babel-plugin@0.18.3` — see [`squircle.stylex.compile.test.ts`](../src/squircle.stylex.compile.test.ts) for the live boundary tests. Specifically:
8+
9+
| Pattern | Result | Plugin error |
10+
| ------------------------------------------------------------------------------ | --------- | ------------------------------------------- |
11+
| `stylex.create({ a: { borderRadius: { default: '1rem', '@supports …': '…' } } })` | ✅ works | — |
12+
| `stylex.create(buildVariants())` (factory in arg position) | ❌ rejects | `create() can only accept an object` |
13+
| `stylex.create({ a: { ...squircle('1rem') } })` (spread imported call) | ❌ rejects | internal `func.fn is not a function` |
14+
| `stylex.create({ a: { borderRadius: { [SUPPORTS]: '…' } } })` (imported string as computed `@`-rule key) | ❌ rejects | `Invalid pseudo or at-rule` |
15+
| `stylex.create({ a: { padding: corrected('1rem') } })` (function call in value) | ❌ rejects | same as above |
16+
17+
The `defineSquircleVariants(stylex.defineVars(...))` API the original plan called for is therefore impossible to ship as a working StyleX-native API today. This was discovered after the JS module was already written and unit-tested in isolation — the unit tests passed because they only verified the plain-JS return shape, not the babel plugin's acceptance of those calls inside `stylex.create`.
18+
19+
## What ships on this branch
20+
21+
The work-in-progress for the StyleX entry. All files exist; the unit tests pass; the **boundary tests pass** (proving the patterns DO fail as expected); but no consumer-visible API on this branch is wired up to actually solve a real StyleX user's problem yet.
22+
23+
- [`package/src/squircle.stylex.ts`](../src/squircle.stylex.ts) — JS helpers (`squircle()`, `squircleTopLeft()`, …, `defineSquircleVariants()`) + constants (`SQUIRCLE_AMOUNT_VAR`, `SQUIRCLE_SUPPORTS_QUERY`) + `correctedRadiusCalc()`. Helpers return plain object literals in StyleX's per-property conditional-value shape. Module docstring **already documents** that they cannot be used inside `stylex.create()`. Useful for: (a) build-time codegen, (b) emotion / styled-components / vanilla-extract / inline styles where function calls in style values are allowed.
24+
- [`package/src/squircle.stylex.test.ts`](../src/squircle.stylex.test.ts) — 11 unit tests on helper return shape (kept passing).
25+
- [`package/src/squircle.stylex.compile.test.ts`](../src/squircle.stylex.compile.test.ts) — real `@stylexjs/babel-plugin` integration tests. Pin both the working "hand-written literal" pattern and the rejected boundary patterns. Resume here when StyleX ships changes.
26+
- [`package/src/squircle-css-sidecar.test.ts`](../src/squircle-css-sidecar.test.ts) — validates the vanilla CSS sidecar.
27+
- [`package/scripts/generate-squircle-css.ts`](../scripts/generate-squircle-css.ts) — added `generateVanillaCss()` that emits `dist/squircle.css` with plain `.squircle-md` etc. classes (no `@utility` directive). Default Tailwind-ish radii (xs/sm/md/lg/xl/2xl/3xl/4xl/full) baked in.
28+
- `package.json` — added `./squircle.css` export, `@stylexjs/{stylex,babel-plugin}` peer (optional), `@babel/{core,preset-typescript}` devDep.
29+
- `pnpm-workspace.yaml` — catalog entries for the above.
30+
- `vite.config.ts` — `stylex` pack entry, `test:stylex-compile` and `test:sidecar` tasks.
31+
32+
## Recommended path forward
33+
34+
Three options, in rough order of how StyleX-native they feel:
35+
36+
### Option 1 — Static CSS sidecar (simplest, probably ship this)
37+
38+
Already built on this branch. Consumers do:
39+
40+
```ts
41+
import '@klinking/squircle/squircle.css';
42+
// <div className="squircle-md" /> alongside any stylex.props(...) styles
43+
```
44+
45+
The class names are inert — they don't go through StyleX's compiler at all, so the static-analysis problem is avoided entirely. The only down-side is class-name collision risk if a consumer also defines `.squircle-md` somewhere; the radii set is fixed to a Tailwind-ish default; and it doesn't compose with `stylex.defineVars` / `stylex.createTheme` for theming.
46+
47+
### Option 2 — Codegen CLI (most StyleX-native)
48+
49+
Ship a one-shot script (e.g. `npx @klinking/squircle/codegen --radii=sm:0.25rem,md:0.375rem --side=all --out=src/squircle.stylex.ts`) that emits a literal `*.stylex.ts` file the consumer checks in. The emitted file is a hand-written `stylex.create({...})` literal so StyleX's compiler is happy.
50+
51+
This is the only path that lets a StyleX consumer use *their own* `defineVars` radii and still get the full StyleX-native experience (`stylex.props(squircleStyles.md)`). The cost is meaningful: we'd ship a CLI, document a "regenerate after changing your radii" workflow, and the generated file would be a new artifact in the consumer's repo.
52+
53+
The existing `defineSquircleVariants()` helper is *exactly* what such a CLI would call internally — that's its real reason for existing. Resume here by:
54+
55+
1. Add a `bin` entry in `package.json`.
56+
2. Wrap `defineSquircleVariants` in a small CLI that JSON-stringifies the output and writes it as a `stylex.create(...)` literal source file.
57+
3. Test the generated file by running it through `@stylexjs/babel-plugin` (the test scaffold already exists in `squircle.stylex.compile.test.ts`).
58+
59+
### Option 3 — Babel macro
60+
61+
A `babel-plugin-macros`-style import that inlines the `stylex.create({...})` literal at consumer compile time. Works around the static-analysis problem by transforming the source before StyleX sees it. Cost: depends on `babel-plugin-macros` (not universally installed), adds a runtime dependency, more complex to debug. Probably not worth it given Option 2 covers the same use case more transparently.
62+
63+
### Option that does **not** work — pre-baked `stylex.create`
64+
65+
Could ship `stylex.create({...})` calls in the package's own source, processed by `@stylexjs/babel-plugin` at our build time. Two blockers: (a) we don't know the consumer's radii, and (b) atomic class names hash with version coupling, so consumers' bundlers re-process the package source anyway. The user's earlier instruction — "no default export with tailwind radii" — also rules this out by preference.
66+
67+
## How to resume
68+
69+
```bash
70+
git checkout feat/stylex-support
71+
cd package
72+
pnpm install
73+
vp run @klinking/squircle#test:stylex-compile # runs the boundary tests
74+
```
75+
76+
The boundary tests are the canary: re-run them against newer `@stylexjs/babel-plugin` versions to see if any of the rejected patterns become accepted. If they do, the factory API may become viable.
77+
78+
When picking a direction:
79+
80+
- For Option 1 (sidecar): just merge this branch as-is, write the README path, done.
81+
- For Option 2 (codegen): keep everything on this branch, add a `bin/squircle-stylex-codegen.ts`, add CLI args parsing, generate a sample output file, snapshot-test it, run it through the babel plugin to verify, document.
82+
- For Option 3 (macro): start a fresh branch — different dependency surface, different mental model.
83+
84+
## Key files
85+
86+
- `package/src/squircle.stylex.ts:46` — module docstring with the "do not call inside `stylex.create`" warning.
87+
- `package/src/squircle.stylex.compile.test.ts:60` — verified working pattern (hand-written literal).
88+
- `package/src/squircle.stylex.compile.test.ts:91` — boundary tests pinning the failure modes.
89+
- `package/scripts/generate-squircle-css.ts:46` — vanilla-CSS sidecar generator.
90+
91+
## Test plan when resuming
92+
93+
1. `vp run @klinking/squircle#build` — must produce `dist/squircle.css` and `dist/stylex.mjs`.
94+
2. `vp run @klinking/squircle#test:sidecar` — sidecar shape.
95+
3. `vp run @klinking/squircle#test:stylex` — JS helper unit tests.
96+
4. `vp run @klinking/squircle#test:stylex-compile` — real babel-plugin integration.
97+
5. Manual smoke in `website/` — add a tiny example component, check DevTools that `@supports (corner-shape: superellipse(2))` activates in supporting browsers and falls back cleanly elsewhere.

0 commit comments

Comments
 (0)