@usespaceui/squircle 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <p align="center">
2
- <a href="https://squircle.spaceui.one" target="_blank">
2
+ <a href="https://www.spaceui.one/components/squircle" target="_blank">
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="https://squircle.spaceui.one/logo.svg">
5
- <source media="(prefers-color-scheme: light)" srcset="https://squircle.spaceui.one/logo.svg">
6
- <img alt="Space UI logo" src="https://squircle.spaceui.one/logo.svg" width="100" />
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://www.spaceui.one/logo.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="https://www.spaceui.one/logo.svg">
6
+ <img alt="Space UI logo" src="https://www.spaceui.one/logo.svg" width="100" />
7
7
  </picture>
8
8
  </a>
9
9
  </p>
@@ -17,15 +17,12 @@
17
17
  </p>
18
18
 
19
19
  <p align="center">
20
- <a href="https://squircle.spaceui.one">Preview</a> •
20
+ <a href="https://www.spaceui.one/components/squircle">Preview</a> •
21
21
  <a href="https://github.com/usespaceui/squircle">Source Code</a> •
22
22
  <a href="https://www.spaceui.one">SpaceUI.one</a>
23
23
  </p>
24
24
 
25
25
  <p align="center">
26
- <a href="https://github.com/usespaceui/squircle/blob/main/LICENSE">
27
- <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License" />
28
- </a>
29
26
  <a href="https://twitter.com/intent/follow?screen_name=usespaceui">
30
27
  <img src="https://img.shields.io/twitter/follow/usespaceui.svg?label=Follow%20@usespaceui" alt="Follow @usespaceui" />
31
28
  </a>
@@ -61,7 +58,7 @@
61
58
  ## ✨ Overview
62
59
 
63
60
  `@usespaceui/squircle` brings iOS-style squircles (mathematically perfect superellipses) to your Tailwind CSS projects. It mimics the continuous curvature corners found in Figma and Apple devices.
64
- By utilizing the CSS Houdini Paint Worklet API, it provides a native-like performance and seamlessly integrates with Tailwind CSS.
61
+ By combining native CSS `corner-shape: squircle` with progressive CSS Houdini Paint Worklet fallbacks, it provides instantaneous zero-lag rendering and seamlessly integrates with Tailwind CSS.
65
62
 
66
63
  ---
67
64
 
@@ -75,7 +72,7 @@ npm install @usespaceui/squircle
75
72
  yarn add @usespaceui/squircle
76
73
  ```
77
74
 
78
- Zero dependencies. Add the plugin to your Tailwind configuration or import it directly (Tailwind).
75
+ Zero dependencies. Add the plugin to your Tailwind configuration (v3) or your CSS entry file (v4).
79
76
 
80
77
  ---
81
78
 
@@ -93,11 +90,12 @@ Since the worklet interacts with the browser DOM, initialize it inside a Client
93
90
  import * as React from 'react'
94
91
  import { initSquircle } from '@usespaceui/squircle'
95
92
 
96
- export function SquircleProvider({ children }: { children: React.ReactNode }) {
97
- React.useEffect(() => {
98
- initSquircle()
99
- }, [])
93
+ // Initialize immediately as client bundle loads before paint
94
+ if (typeof window !== 'undefined') {
95
+ initSquircle()
96
+ }
100
97
 
98
+ export function SquircleProvider({ children }: { children: React.ReactNode }) {
101
99
  return <>{children}</>
102
100
  }
103
101
  ```
@@ -130,107 +128,112 @@ if (typeof window !== 'undefined') {
130
128
  }
131
129
  ```
132
130
 
133
- ## Configure Tailwind CSS 4
131
+ ### Configure Tailwind CSS 4
134
132
 
135
- Add the squircle plugin to your `tailwind.config.ts`.
133
+ In Tailwind CSS v4, add the plugin directly to your CSS entry point (e.g. `globals.css` or `styles.css`):
136
134
 
137
135
  ```css
138
- @import '@usespaceui/squircle';
136
+ @import 'tailwindcss';
137
+ @plugin "@usespaceui/squircle";
139
138
  ```
140
139
 
141
140
  ### Configure Tailwind CSS 3
142
141
 
143
- Add the squircle plugin to your `tailwind.config.js` or `tailwind.config.ts`.
142
+ Add the squircle plugin to your `tailwind.config.js` or `tailwind.config.ts`:
144
143
 
145
- ```ts
146
- import squirclePlugin from '@usespaceui/squircle'
144
+ #### CommonJS (`tailwind.config.js`)
147
145
 
146
+ ```js
148
147
  /** @type {import('tailwindcss').Config} */
149
- export default {
148
+ module.exports = {
150
149
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
151
150
  theme: {
152
151
  extend: {},
153
152
  },
154
- plugins: [squirclePlugin],
153
+ plugins: [require('@usespaceui/squircle')],
155
154
  }
156
155
  ```
157
156
 
157
+ #### ESM / TypeScript (`tailwind.config.ts` or `tailwind.config.mjs`)
158
+
159
+ ```ts
160
+ import type { Config } from 'tailwindcss'
161
+ import squircle from '@usespaceui/squircle'
162
+
163
+ const config: Config = {
164
+ content: ['./src/**/*.{js,ts,jsx,tsx}'],
165
+ theme: {
166
+ extend: {},
167
+ },
168
+ plugins: [squircle],
169
+ }
170
+
171
+ export default config
172
+ ```
173
+
158
174
  ---
159
175
 
160
176
  ## 🎨 Usage
161
177
 
162
178
  ### 1. Sizing
163
179
 
164
- The plugin generates squircle utilities based on your existing Tailwind `borderRadius` theme. If your Tailwind config has `rounded-2xl`, you automatically have `squircle-2xl`.
180
+ Apply the `squircle` utility alongside your Tailwind `rounded-*` classes:
165
181
 
166
182
  ```html
167
- <div class="squircle-md p-4 bg-black text-white">Space UI Squircle!</div>
168
- <div class="squircle-2xl">...</div>
169
- <div class="squircle-full">...</div>
170
- <!-- Automatically clamped to pill/circle -->
183
+ <!-- Standard Tailwind sizes -->
184
+ <button class="squircle rounded-2xl bg-black text-white px-6 py-3">
185
+ Space UI Squircle
186
+ </button>
187
+
188
+ <!-- Smooth hover transitions between radii -->
189
+ <button class="squircle rounded-7xl hover:rounded-3xl transition-all duration-300">
190
+ Interactive Squircle
191
+ </button>
192
+
193
+ <!-- Pill / Circle -->
194
+ <div class="squircle rounded-full">...</div>
171
195
 
172
- <!-- Arbitrary values are fully supported -->
173
- <div class="squircle-[40px]">...</div>
196
+ <!-- Arbitrary values -->
197
+ <div class="squircle rounded-[40px]">...</div>
174
198
  ```
175
199
 
200
+ ---
201
+
176
202
  ### 2. Smoothing Intensity
177
203
 
178
- By default, the plugin applies a **60% smooth transition** between the straight edge and the corner arc, perfectly matching Apple and Figma's native smoothing.
204
+ By default, `squircle` applies a **60% smooth transition**, perfectly matching Apple (iOS) and Figma's native continuous curvature.
179
205
 
180
- You can adjust this intensity using modifiers (`/0`, `/20`, `/40`, `/60`, `/80`, `/100`).
206
+ You can adjust this intensity using modifiers (`/0`, `/20`, `/40`, `/60`, `/80`, `/100`):
181
207
 
182
208
  ```html
183
- <!-- Maximum smoothing -->
184
- <div class="squircle-2xl/100">...</div>
209
+ <!-- Maximum smoothing (pronounced superellipse) -->
210
+ <button class="squircle/100 rounded-3xl">...</button>
185
211
 
186
212
  <!-- Minimal smoothing (closer to a plain rounded rectangle) -->
187
- <div class="squircle-2xl/20">...</div>
188
-
189
- <!-- Arbitrary smoothing -->
190
- <div class="squircle-2xl/[25]">...</div>
191
- ```
192
-
193
- ### 3. Native Borders (The Magic)
194
-
195
- Use standard Tailwind border utilities. The Houdini worklet reads your native CSS border classes and draws the squircle border perfectly. No workarounds needed.
213
+ <button class="squircle/20 rounded-3xl">...</button>
196
214
 
197
- ```html
198
- <!-- 8px red border -->
199
- <div class="squircle-2xl border-8 border-red-500 bg-black">...</div>
200
-
201
- <!-- 1px semi-transparent blue border -->
202
- <div class="squircle-[30px] border border-blue-500/50 bg-white">...</div>
215
+ <!-- Default is 60% (matches Apple iOS & Figma perfectly) -->
216
+ <button class="squircle rounded-3xl">...</button>
203
217
  ```
204
218
 
205
- > **Note on border placement:**
206
- > By default, the plugin draws the border **outside** the element (like an outline or a ring) to preserve the interior space for your content. If you want the border drawn inward (standard CSS border-box behavior), you can override the custom CSS variable:
207
- > `<div class="squircle-2xl border-4 border-red-500 [--tw-squircle-outset:0]">...</div>`
219
+ > **Note on custom smoothing:** Custom smoothing modifiers are processed dynamically by the CSS Houdini engine. In browsers with native `corner-shape: squircle`, the native CSS superellipse curve is used by default.
208
220
 
209
221
  ---
210
222
 
211
- ## 🛠 Advanced: Using with `tailwind-merge`
212
-
213
- If you use `tailwind-merge` (standard in UI libraries like `shadcn/ui` for their `cn()` utility), you should configure it so that `squircle-*` classes properly override standard `rounded-*` classes when both are applied.
223
+ ### 3. Borders & Outlines
214
224
 
215
- Extend your merge configuration to group `squircle` into the `rounded` class group:
225
+ Use standard Tailwind border utilities (`border`, `border-2`, `border-8`, etc.). The squircle contour shapes both the background and the border:
216
226
 
217
- ```ts
218
- import { clsx, type ClassValue } from 'clsx'
219
- import { extendTailwindMerge } from 'tailwind-merge'
220
-
221
- const customTwMerge = extendTailwindMerge({
222
- extend: {
223
- classGroups: {
224
- rounded: [
225
- { squircle: [() => true] }, // Merges any squircle-* class into the rounded group
226
- ],
227
- },
228
- },
229
- })
230
-
231
- export function cn(...inputs: ClassValue[]) {
232
- return customTwMerge(clsx(inputs))
233
- }
227
+ ```html
228
+ <!-- 8px border following squircle curvature -->
229
+ <button class="squircle rounded-7xl border-8 border-red-500 bg-black text-white">
230
+ Bordered Squircle
231
+ </button>
232
+
233
+ <!-- 1px subtle border -->
234
+ <div class="squircle rounded-3xl border border-border bg-card">
235
+ Card with Squircle Border
236
+ </div>
234
237
  ```
235
238
 
236
239
  ---
@@ -241,13 +244,13 @@ export function cn(...inputs: ClassValue[]) {
241
244
  Registers the CSS Houdini Paint Worklet and sets up the custom CSS properties (`--tw-squircle-w` and `--tw-squircle-smooth`). It is safe to call multiple times and fails gracefully in environments where Houdini is not supported.
242
245
 
243
246
  - `plugin(tailwindcss)`
244
- The default export is a Tailwind CSS plugin that provides the `squircle-*` utility classes. It automatically provides fallback `border-radius` styles for browsers that don't support the Paint API.
247
+ The default export is a Tailwind CSS plugin that provides the `squircle` utility class. It automatically provides fallback `border-radius` styles for browsers that don't support the Paint API.
245
248
 
246
249
  ---
247
250
 
248
251
  ### Features
249
252
 
250
- - 💃 **Native Tailwind Integration:** Use utility classes like `squircle-2xl` just like you would `rounded-2xl`. No framework-specific wrappers required and if the navigator don"'t support squricle the rounded value is applies.
253
+ - 💃 **Native Tailwind Integration:** Use the `squircle` utility alongside standard `rounded-*` classes. No framework-specific wrappers required, and if the browser doesn't support the Paint API, standard rounded fallback values apply automatically.
251
254
  - ⚡ **Zero-JS Layout Thrashing:** Driven by the native CSS Paint API (Houdini). No `ResizeObserver`, no DOM manipulation, no SVG paths. Repaints happen instantly on the compositor thread.
252
255
  - 👌 **Framework Agnostic:** Works in React, Vue, Svelte, Solid, or plain HTML.
253
256
 
@@ -270,7 +273,7 @@ MIT — Free for commercial and personal use.
270
273
 
271
274
  ## 📚 Resources
272
275
 
273
- - 🔍 [Explore the squircles & Playground](https://squircle.spaceui.one)
276
+ - 🔍 [Explore the squircles & Playground](https://www.spaceui.one/components/squircle)
274
277
  - 🌍 [Space UI Official Site](https://www.spaceui.one)
275
278
 
276
279
  ---
package/dist/index.d.mts CHANGED
@@ -1,6 +1,9 @@
1
1
  import * as tailwindcss_plugin from 'tailwindcss/plugin';
2
2
 
3
+ declare const workletCode = "\nclass SquirclePainter {\n static get inputProperties() {\n return [\n '--tw-squircle-w',\n '--tw-squircle-smooth',\n 'border-top-left-radius',\n 'border-radius'\n ];\n }\n \n paint(ctx, size, properties) {\n const getProp = (name) => {\n const p = properties.get(name);\n if (!p) return '';\n if (typeof p.value === 'number') return p.value + (p.unit || 'px');\n return p.toString().trim();\n };\n \n const radiusStr = getProp('--tw-squircle-w') || getProp('border-top-left-radius') || getProp('border-radius');\n let radius = 0;\n if (radiusStr) {\n const firstVal = radiusStr.trim().split(/s+/)[0];\n if (firstVal.endsWith('%')) {\n radius = (Math.min(size.width, size.height) * parseFloat(firstVal)) / 100;\n } else {\n const parsed = parseFloat(firstVal);\n if (firstVal.endsWith('rem')) {\n radius = parsed * 16;\n } else {\n radius = parsed || 0;\n }\n }\n }\n const smoothStr = getProp('--tw-squircle-smooth');\n let smooth = parseFloat(smoothStr);\n if (isNaN(smooth)) smooth = 0.6;\n \n radius = Math.min(radius, size.width / 2, size.height / 2);\n \n this.drawSquircle(ctx, size.width, size.height, radius, smooth);\n ctx.fillStyle = 'black';\n ctx.fill();\n }\n \n drawSquircle(ctx, width, height, radius, smooth) {\n const n = 2 + smooth * 2;\n const steps = 30;\n const step = Math.PI / 2 / steps;\n ctx.beginPath();\n for (let i = 0; i <= steps; i++) {\n const t = Math.PI / 2 - i * step;\n const X = radius * Math.pow(Math.cos(t), 2 / n);\n const Y = radius * Math.pow(Math.sin(t), 2 / n);\n const x = width - radius + X;\n const y = radius - Y;\n if (i === 0) ctx.moveTo(x, y);\n else ctx.lineTo(x, y);\n }\n for (let i = 0; i <= steps; i++) {\n const t = i * step;\n const X = radius * Math.pow(Math.cos(t), 2 / n);\n const Y = radius * Math.pow(Math.sin(t), 2 / n);\n ctx.lineTo(width - radius + X, height - radius + Y);\n }\n for (let i = 0; i <= steps; i++) {\n const t = Math.PI / 2 - i * step;\n const X = radius * Math.pow(Math.cos(t), 2 / n);\n const Y = radius * Math.pow(Math.sin(t), 2 / n);\n ctx.lineTo(radius - X, height - radius + Y);\n }\n for (let i = 0; i <= steps; i++) {\n const t = i * step;\n const X = radius * Math.pow(Math.cos(t), 2 / n);\n const Y = radius * Math.pow(Math.sin(t), 2 / n);\n ctx.lineTo(radius - X, radius - Y);\n }\n ctx.closePath();\n }\n}\n\nclass SquircleBorderPainter extends SquirclePainter {\n static get inputProperties() {\n return [\n '--tw-squircle-w', \n '--tw-squircle-smooth',\n 'border-top-left-radius',\n 'border-radius',\n 'border-top-width',\n 'border-top-color'\n ];\n }\n \n paint(ctx, size, properties) {\n const getProp = (name) => {\n const p = properties.get(name);\n if (!p) return '';\n if (typeof p.value === 'number') return p.value + (p.unit || 'px');\n return p.toString().trim();\n };\n\n const radiusStr = getProp('--tw-squircle-w') || getProp('border-top-left-radius') || getProp('border-radius');\n let radius = 0;\n if (radiusStr) {\n const firstVal = radiusStr.trim().split(/s+/)[0];\n if (firstVal.endsWith('%')) {\n radius = (Math.min(size.width, size.height) * parseFloat(firstVal)) / 100;\n } else {\n const parsed = parseFloat(firstVal);\n if (firstVal.endsWith('rem')) {\n radius = parsed * 16;\n } else {\n radius = parsed || 0;\n }\n }\n }\n const smoothStr = getProp('--tw-squircle-smooth');\n let smooth = parseFloat(smoothStr);\n if (isNaN(smooth)) smooth = 0.6;\n \n radius = Math.min(radius, size.width / 2, size.height / 2);\n \n const bwStr = getProp('border-top-width');\n const bw = parseFloat(bwStr) || 0;\n \n if (bw <= 0) return;\n \n let color = getProp('border-top-color');\n if (!color) color = 'black';\n \n // Inset stroke by half border-width so it stays 100% within the element bounds\n const halfBw = bw / 2;\n const innerW = Math.max(0, size.width - bw);\n const innerH = Math.max(0, size.height - bw);\n const innerR = Math.max(0, radius - halfBw);\n \n ctx.save();\n ctx.translate(halfBw, halfBw);\n this.drawSquircle(ctx, innerW, innerH, innerR, smooth);\n ctx.lineWidth = bw;\n ctx.strokeStyle = color;\n ctx.stroke();\n ctx.restore();\n }\n}\n\nregisterPaint('squircle', SquirclePainter);\nregisterPaint('squircle-border', SquircleBorderPainter);\n";
4
+
3
5
  declare function initSquircle(): void;
6
+
4
7
  declare const _default: tailwindcss_plugin.PluginWithConfig;
5
8
 
6
- export { _default as default, initSquircle };
9
+ export { _default as default, initSquircle, workletCode };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import * as tailwindcss_plugin from 'tailwindcss/plugin';
2
2
 
3
+ declare const workletCode = "\nclass SquirclePainter {\n static get inputProperties() {\n return [\n '--tw-squircle-w',\n '--tw-squircle-smooth',\n 'border-top-left-radius',\n 'border-radius'\n ];\n }\n \n paint(ctx, size, properties) {\n const getProp = (name) => {\n const p = properties.get(name);\n if (!p) return '';\n if (typeof p.value === 'number') return p.value + (p.unit || 'px');\n return p.toString().trim();\n };\n \n const radiusStr = getProp('--tw-squircle-w') || getProp('border-top-left-radius') || getProp('border-radius');\n let radius = 0;\n if (radiusStr) {\n const firstVal = radiusStr.trim().split(/s+/)[0];\n if (firstVal.endsWith('%')) {\n radius = (Math.min(size.width, size.height) * parseFloat(firstVal)) / 100;\n } else {\n const parsed = parseFloat(firstVal);\n if (firstVal.endsWith('rem')) {\n radius = parsed * 16;\n } else {\n radius = parsed || 0;\n }\n }\n }\n const smoothStr = getProp('--tw-squircle-smooth');\n let smooth = parseFloat(smoothStr);\n if (isNaN(smooth)) smooth = 0.6;\n \n radius = Math.min(radius, size.width / 2, size.height / 2);\n \n this.drawSquircle(ctx, size.width, size.height, radius, smooth);\n ctx.fillStyle = 'black';\n ctx.fill();\n }\n \n drawSquircle(ctx, width, height, radius, smooth) {\n const n = 2 + smooth * 2;\n const steps = 30;\n const step = Math.PI / 2 / steps;\n ctx.beginPath();\n for (let i = 0; i <= steps; i++) {\n const t = Math.PI / 2 - i * step;\n const X = radius * Math.pow(Math.cos(t), 2 / n);\n const Y = radius * Math.pow(Math.sin(t), 2 / n);\n const x = width - radius + X;\n const y = radius - Y;\n if (i === 0) ctx.moveTo(x, y);\n else ctx.lineTo(x, y);\n }\n for (let i = 0; i <= steps; i++) {\n const t = i * step;\n const X = radius * Math.pow(Math.cos(t), 2 / n);\n const Y = radius * Math.pow(Math.sin(t), 2 / n);\n ctx.lineTo(width - radius + X, height - radius + Y);\n }\n for (let i = 0; i <= steps; i++) {\n const t = Math.PI / 2 - i * step;\n const X = radius * Math.pow(Math.cos(t), 2 / n);\n const Y = radius * Math.pow(Math.sin(t), 2 / n);\n ctx.lineTo(radius - X, height - radius + Y);\n }\n for (let i = 0; i <= steps; i++) {\n const t = i * step;\n const X = radius * Math.pow(Math.cos(t), 2 / n);\n const Y = radius * Math.pow(Math.sin(t), 2 / n);\n ctx.lineTo(radius - X, radius - Y);\n }\n ctx.closePath();\n }\n}\n\nclass SquircleBorderPainter extends SquirclePainter {\n static get inputProperties() {\n return [\n '--tw-squircle-w', \n '--tw-squircle-smooth',\n 'border-top-left-radius',\n 'border-radius',\n 'border-top-width',\n 'border-top-color'\n ];\n }\n \n paint(ctx, size, properties) {\n const getProp = (name) => {\n const p = properties.get(name);\n if (!p) return '';\n if (typeof p.value === 'number') return p.value + (p.unit || 'px');\n return p.toString().trim();\n };\n\n const radiusStr = getProp('--tw-squircle-w') || getProp('border-top-left-radius') || getProp('border-radius');\n let radius = 0;\n if (radiusStr) {\n const firstVal = radiusStr.trim().split(/s+/)[0];\n if (firstVal.endsWith('%')) {\n radius = (Math.min(size.width, size.height) * parseFloat(firstVal)) / 100;\n } else {\n const parsed = parseFloat(firstVal);\n if (firstVal.endsWith('rem')) {\n radius = parsed * 16;\n } else {\n radius = parsed || 0;\n }\n }\n }\n const smoothStr = getProp('--tw-squircle-smooth');\n let smooth = parseFloat(smoothStr);\n if (isNaN(smooth)) smooth = 0.6;\n \n radius = Math.min(radius, size.width / 2, size.height / 2);\n \n const bwStr = getProp('border-top-width');\n const bw = parseFloat(bwStr) || 0;\n \n if (bw <= 0) return;\n \n let color = getProp('border-top-color');\n if (!color) color = 'black';\n \n // Inset stroke by half border-width so it stays 100% within the element bounds\n const halfBw = bw / 2;\n const innerW = Math.max(0, size.width - bw);\n const innerH = Math.max(0, size.height - bw);\n const innerR = Math.max(0, radius - halfBw);\n \n ctx.save();\n ctx.translate(halfBw, halfBw);\n this.drawSquircle(ctx, innerW, innerH, innerR, smooth);\n ctx.lineWidth = bw;\n ctx.strokeStyle = color;\n ctx.stroke();\n ctx.restore();\n }\n}\n\nregisterPaint('squircle', SquirclePainter);\nregisterPaint('squircle-border', SquircleBorderPainter);\n";
4
+
3
5
  declare function initSquircle(): void;
6
+
4
7
  declare const _default: tailwindcss_plugin.PluginWithConfig;
5
8
 
6
- export { _default as default, initSquircle };
9
+ export { _default as default, initSquircle, workletCode };
package/dist/index.js CHANGED
@@ -1,21 +1,36 @@
1
- "use strict";var o=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var d=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var h=(r,t)=>{for(var i in t)o(r,i,{get:t[i],enumerable:!0})},m=(r,t,i,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of d(t))!p.call(r,s)&&s!==i&&o(r,s,{get:()=>t[s],enumerable:!(e=l(t,s))||e.enumerable});return r};var w=r=>m(o({},"__esModule",{value:!0}),r);var f={};h(f,{default:()=>S,initSquircle:()=>g});module.exports=w(f);function n(r,t){return{handler:r,config:t}}n.withOptions=function(r,t=()=>({})){function i(e){return{handler:r(e),config:t(e)}}return i.__isOptionsFunction=!0,i};var c=n;var u=`
1
+ "use strict";var n=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var f=(e,t)=>{for(var s in t)n(e,s,{get:t[s],enumerable:!0})},w=(e,t,s,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of h(t))!m.call(e,r)&&r!==s&&n(e,r,{get:()=>t[r],enumerable:!(i=p(t,r))||i.enumerable});return e};var g=e=>w(n({},"__esModule",{value:!0}),e);var S={};f(S,{default:()=>q,initSquircle:()=>b,workletCode:()=>c});module.exports=g(S);function u(e,t){return{handler:e,config:t}}u.withOptions=function(e,t=()=>({})){function s(i){return{handler:e(i),config:t(i)}}return s.__isOptionsFunction=!0,s};var l=u;var c=`
2
2
  class SquirclePainter {
3
3
  static get inputProperties() {
4
- return ['--tw-squircle-w', '--tw-squircle-smooth'];
4
+ return [
5
+ '--tw-squircle-w',
6
+ '--tw-squircle-smooth',
7
+ 'border-top-left-radius',
8
+ 'border-radius'
9
+ ];
5
10
  }
6
11
 
7
12
  paint(ctx, size, properties) {
8
13
  const getProp = (name) => {
9
14
  const p = properties.get(name);
10
- return p ? p.toString().trim() : '';
15
+ if (!p) return '';
16
+ if (typeof p.value === 'number') return p.value + (p.unit || 'px');
17
+ return p.toString().trim();
11
18
  };
12
19
 
13
- const radiusStr = getProp('--tw-squircle-w');
20
+ const radiusStr = getProp('--tw-squircle-w') || getProp('border-top-left-radius') || getProp('border-radius');
14
21
  let radius = 0;
15
- if (radiusStr.endsWith('%')) {
16
- radius = (Math.min(size.width, size.height) * parseFloat(radiusStr)) / 100;
17
- } else {
18
- radius = parseFloat(radiusStr) || 0;
22
+ if (radiusStr) {
23
+ const firstVal = radiusStr.trim().split(/s+/)[0];
24
+ if (firstVal.endsWith('%')) {
25
+ radius = (Math.min(size.width, size.height) * parseFloat(firstVal)) / 100;
26
+ } else {
27
+ const parsed = parseFloat(firstVal);
28
+ if (firstVal.endsWith('rem')) {
29
+ radius = parsed * 16;
30
+ } else {
31
+ radius = parsed || 0;
32
+ }
33
+ }
19
34
  }
20
35
  const smoothStr = getProp('--tw-squircle-smooth');
21
36
  let smooth = parseFloat(smoothStr);
@@ -69,6 +84,8 @@ class SquircleBorderPainter extends SquirclePainter {
69
84
  return [
70
85
  '--tw-squircle-w',
71
86
  '--tw-squircle-smooth',
87
+ 'border-top-left-radius',
88
+ 'border-radius',
72
89
  'border-top-width',
73
90
  'border-top-color'
74
91
  ];
@@ -77,15 +94,25 @@ class SquircleBorderPainter extends SquirclePainter {
77
94
  paint(ctx, size, properties) {
78
95
  const getProp = (name) => {
79
96
  const p = properties.get(name);
80
- return p ? p.toString().trim() : '';
97
+ if (!p) return '';
98
+ if (typeof p.value === 'number') return p.value + (p.unit || 'px');
99
+ return p.toString().trim();
81
100
  };
82
101
 
83
- const radiusStr = getProp('--tw-squircle-w');
102
+ const radiusStr = getProp('--tw-squircle-w') || getProp('border-top-left-radius') || getProp('border-radius');
84
103
  let radius = 0;
85
- if (radiusStr.endsWith('%')) {
86
- radius = (Math.min(size.width, size.height) * parseFloat(radiusStr)) / 100;
87
- } else {
88
- radius = parseFloat(radiusStr) || 0;
104
+ if (radiusStr) {
105
+ const firstVal = radiusStr.trim().split(/s+/)[0];
106
+ if (firstVal.endsWith('%')) {
107
+ radius = (Math.min(size.width, size.height) * parseFloat(firstVal)) / 100;
108
+ } else {
109
+ const parsed = parseFloat(firstVal);
110
+ if (firstVal.endsWith('rem')) {
111
+ radius = parsed * 16;
112
+ } else {
113
+ radius = parsed || 0;
114
+ }
115
+ }
89
116
  }
90
117
  const smoothStr = getProp('--tw-squircle-smooth');
91
118
  let smooth = parseFloat(smoothStr);
@@ -101,14 +128,22 @@ class SquircleBorderPainter extends SquirclePainter {
101
128
  let color = getProp('border-top-color');
102
129
  if (!color) color = 'black';
103
130
 
104
- this.drawSquircle(ctx, size.width, size.height, radius, smooth);
131
+ // Inset stroke by half border-width so it stays 100% within the element bounds
132
+ const halfBw = bw / 2;
133
+ const innerW = Math.max(0, size.width - bw);
134
+ const innerH = Math.max(0, size.height - bw);
135
+ const innerR = Math.max(0, radius - halfBw);
105
136
 
106
- ctx.lineWidth = bw * 2;
137
+ ctx.save();
138
+ ctx.translate(halfBw, halfBw);
139
+ this.drawSquircle(ctx, innerW, innerH, innerR, smooth);
140
+ ctx.lineWidth = bw;
107
141
  ctx.strokeStyle = color;
108
142
  ctx.stroke();
143
+ ctx.restore();
109
144
  }
110
145
  }
111
146
 
112
147
  registerPaint('squircle', SquirclePainter);
113
148
  registerPaint('squircle-border', SquircleBorderPainter);
114
- `;function g(){if(typeof window<"u"){if("registerProperty"in CSS)try{CSS.registerProperty({name:"--tw-squircle-w",syntax:"<length> | <percentage>",inherits:!0,initialValue:"0px"}),CSS.registerProperty({name:"--tw-squircle-smooth",syntax:"<number>",inherits:!0,initialValue:"0.6"})}catch{}if("paintWorklet"in CSS){let r=`data:application/javascript;charset=utf8,${encodeURIComponent(u)}`;CSS.paintWorklet.addModule(r).catch(t=>console.error("Squircle worklet failed",t))}}}var S=c(function({matchUtilities:r,theme:t}){r({squircle:(i,{modifier:e})=>{let s=.6;if(e){let a=parseFloat(e);isNaN(a)||(s=a/100)}return{"border-radius":i,"--tw-squircle-w":i,"--tw-squircle-smooth":s.toString(),"@supports (mask-image: paint(squircle))":{"border-radius":"0","mask-image":"paint(squircle)"},'&[class~="border"], &[class*=" border-"], &[class^="border-"]':{"@supports (mask-image: paint(squircle))":{"border-image-source":"paint(squircle-border)","border-image-slice":"0 fill","border-image-outset":"var(--tw-squircle-outset, 1)"}},"a&, input&, a &, input &":{"@supports (mask-image: paint(squircle))":{"border-radius":i,"mask-image":"none","background-image":"paint(squircle)","border-image-source":"none"}}}}},{values:t("borderRadius"),modifiers:{0:"0",20:"20",40:"40",60:"60",80:"80",100:"100"}})});0&&(module.exports={initSquircle});
149
+ `;var d=!1;function b(){if(typeof window<"u"&&"CSS"in window){if(d)return;if(d=!0,typeof CSS.supports=="function"&&CSS.supports("corner-shape","squircle")){document&&document.documentElement&&document.documentElement.classList.add("squircle-native");return}if("registerProperty"in CSS)try{CSS.registerProperty({name:"--tw-squircle-w",syntax:"<length> | <percentage>",inherits:!0,initialValue:"0px"}),CSS.registerProperty({name:"--tw-squircle-smooth",syntax:"<number>",inherits:!0,initialValue:"0.6"})}catch{}if("paintWorklet"in CSS){let e=`data:application/javascript;charset=utf8,${encodeURIComponent(c)}`;CSS.paintWorklet.addModule(e).then(()=>{document&&document.documentElement&&document.documentElement.classList.add("squircle-ready")}).catch(t=>console.error("Squircle worklet failed",t))}}}var q=l(function({addUtilities:e,matchUtilities:t,theme:s}){t({squircle:(i,{modifier:r})=>{let a=.6;if(r){let o=parseFloat(r);isNaN(o)||(a=o>1?o/100:o)}return{"--tw-squircle-smooth":a.toString(),"@supports (corner-shape: squircle)":{"corner-shape":"squircle"},"@supports (not (corner-shape: squircle)) and (mask-image: paint(squircle))":{":where(html.squircle-ready) &":{"mask-image":"paint(squircle)","-webkit-mask-image":"paint(squircle)",'&[class~="border"], &[class*=" border-"], &[class^="border-"]':{"border-image-source":"paint(squircle-border)","border-image-slice":"0 fill","border-image-outset":"var(--tw-squircle-outset, 0)"},"a&, input&, a &, input &":{"mask-image":"none","-webkit-mask-image":"none","background-image":"paint(squircle)","border-image-source":"none"}}}}}},{values:{DEFAULT:"DEFAULT"},modifiers:{0:"0",20:"20",40:"40",60:"60",80:"80",100:"100"}}),t({squircle:(i,{modifier:r})=>{let a=.6;if(r){let o=parseFloat(r);isNaN(o)||(a=o/100)}return{"border-radius":i,"--tw-squircle-w":i,"--tw-squircle-smooth":a.toString(),"@supports (corner-shape: squircle)":{"corner-shape":"squircle"},"@supports (not (corner-shape: squircle)) and (mask-image: paint(squircle))":{":where(html.squircle-ready) &":{"border-radius":"0","mask-image":"paint(squircle)","-webkit-mask-image":"paint(squircle)",'&[class~="border"], &[class*=" border-"], &[class^="border-"]':{"border-image-source":"paint(squircle-border)","border-image-slice":"0 fill","border-image-outset":"var(--tw-squircle-outset, 0)"},"a&, input&, a &, input &":{"border-radius":i,"mask-image":"none","-webkit-mask-image":"none","background-image":"paint(squircle)","border-image-source":"none"}}}}}},{values:s("borderRadius"),modifiers:{0:"0",20:"20",40:"40",60:"60",80:"80",100:"100"}})});0&&(module.exports={initSquircle,workletCode});
package/dist/index.mjs CHANGED
@@ -1,21 +1,36 @@
1
- function a(t,r){return{handler:t,config:r}}a.withOptions=function(t,r=()=>({})){function i(e){return{handler:t(e),config:r(e)}}return i.__isOptionsFunction=!0,i};var n=a;var c=`
1
+ function n(e,t){return{handler:e,config:t}}n.withOptions=function(e,t=()=>({})){function a(r){return{handler:e(r),config:t(r)}}return a.__isOptionsFunction=!0,a};var c=n;var u=`
2
2
  class SquirclePainter {
3
3
  static get inputProperties() {
4
- return ['--tw-squircle-w', '--tw-squircle-smooth'];
4
+ return [
5
+ '--tw-squircle-w',
6
+ '--tw-squircle-smooth',
7
+ 'border-top-left-radius',
8
+ 'border-radius'
9
+ ];
5
10
  }
6
11
 
7
12
  paint(ctx, size, properties) {
8
13
  const getProp = (name) => {
9
14
  const p = properties.get(name);
10
- return p ? p.toString().trim() : '';
15
+ if (!p) return '';
16
+ if (typeof p.value === 'number') return p.value + (p.unit || 'px');
17
+ return p.toString().trim();
11
18
  };
12
19
 
13
- const radiusStr = getProp('--tw-squircle-w');
20
+ const radiusStr = getProp('--tw-squircle-w') || getProp('border-top-left-radius') || getProp('border-radius');
14
21
  let radius = 0;
15
- if (radiusStr.endsWith('%')) {
16
- radius = (Math.min(size.width, size.height) * parseFloat(radiusStr)) / 100;
17
- } else {
18
- radius = parseFloat(radiusStr) || 0;
22
+ if (radiusStr) {
23
+ const firstVal = radiusStr.trim().split(/s+/)[0];
24
+ if (firstVal.endsWith('%')) {
25
+ radius = (Math.min(size.width, size.height) * parseFloat(firstVal)) / 100;
26
+ } else {
27
+ const parsed = parseFloat(firstVal);
28
+ if (firstVal.endsWith('rem')) {
29
+ radius = parsed * 16;
30
+ } else {
31
+ radius = parsed || 0;
32
+ }
33
+ }
19
34
  }
20
35
  const smoothStr = getProp('--tw-squircle-smooth');
21
36
  let smooth = parseFloat(smoothStr);
@@ -69,6 +84,8 @@ class SquircleBorderPainter extends SquirclePainter {
69
84
  return [
70
85
  '--tw-squircle-w',
71
86
  '--tw-squircle-smooth',
87
+ 'border-top-left-radius',
88
+ 'border-radius',
72
89
  'border-top-width',
73
90
  'border-top-color'
74
91
  ];
@@ -77,15 +94,25 @@ class SquircleBorderPainter extends SquirclePainter {
77
94
  paint(ctx, size, properties) {
78
95
  const getProp = (name) => {
79
96
  const p = properties.get(name);
80
- return p ? p.toString().trim() : '';
97
+ if (!p) return '';
98
+ if (typeof p.value === 'number') return p.value + (p.unit || 'px');
99
+ return p.toString().trim();
81
100
  };
82
101
 
83
- const radiusStr = getProp('--tw-squircle-w');
102
+ const radiusStr = getProp('--tw-squircle-w') || getProp('border-top-left-radius') || getProp('border-radius');
84
103
  let radius = 0;
85
- if (radiusStr.endsWith('%')) {
86
- radius = (Math.min(size.width, size.height) * parseFloat(radiusStr)) / 100;
87
- } else {
88
- radius = parseFloat(radiusStr) || 0;
104
+ if (radiusStr) {
105
+ const firstVal = radiusStr.trim().split(/s+/)[0];
106
+ if (firstVal.endsWith('%')) {
107
+ radius = (Math.min(size.width, size.height) * parseFloat(firstVal)) / 100;
108
+ } else {
109
+ const parsed = parseFloat(firstVal);
110
+ if (firstVal.endsWith('rem')) {
111
+ radius = parsed * 16;
112
+ } else {
113
+ radius = parsed || 0;
114
+ }
115
+ }
89
116
  }
90
117
  const smoothStr = getProp('--tw-squircle-smooth');
91
118
  let smooth = parseFloat(smoothStr);
@@ -101,14 +128,22 @@ class SquircleBorderPainter extends SquirclePainter {
101
128
  let color = getProp('border-top-color');
102
129
  if (!color) color = 'black';
103
130
 
104
- this.drawSquircle(ctx, size.width, size.height, radius, smooth);
131
+ // Inset stroke by half border-width so it stays 100% within the element bounds
132
+ const halfBw = bw / 2;
133
+ const innerW = Math.max(0, size.width - bw);
134
+ const innerH = Math.max(0, size.height - bw);
135
+ const innerR = Math.max(0, radius - halfBw);
105
136
 
106
- ctx.lineWidth = bw * 2;
137
+ ctx.save();
138
+ ctx.translate(halfBw, halfBw);
139
+ this.drawSquircle(ctx, innerW, innerH, innerR, smooth);
140
+ ctx.lineWidth = bw;
107
141
  ctx.strokeStyle = color;
108
142
  ctx.stroke();
143
+ ctx.restore();
109
144
  }
110
145
  }
111
146
 
112
147
  registerPaint('squircle', SquirclePainter);
113
148
  registerPaint('squircle-border', SquircleBorderPainter);
114
- `;function h(){if(typeof window<"u"){if("registerProperty"in CSS)try{CSS.registerProperty({name:"--tw-squircle-w",syntax:"<length> | <percentage>",inherits:!0,initialValue:"0px"}),CSS.registerProperty({name:"--tw-squircle-smooth",syntax:"<number>",inherits:!0,initialValue:"0.6"})}catch{}if("paintWorklet"in CSS){let t=`data:application/javascript;charset=utf8,${encodeURIComponent(c)}`;CSS.paintWorklet.addModule(t).catch(r=>console.error("Squircle worklet failed",r))}}}var m=n(function({matchUtilities:t,theme:r}){t({squircle:(i,{modifier:e})=>{let s=.6;if(e){let o=parseFloat(e);isNaN(o)||(s=o/100)}return{"border-radius":i,"--tw-squircle-w":i,"--tw-squircle-smooth":s.toString(),"@supports (mask-image: paint(squircle))":{"border-radius":"0","mask-image":"paint(squircle)"},'&[class~="border"], &[class*=" border-"], &[class^="border-"]':{"@supports (mask-image: paint(squircle))":{"border-image-source":"paint(squircle-border)","border-image-slice":"0 fill","border-image-outset":"var(--tw-squircle-outset, 1)"}},"a&, input&, a &, input &":{"@supports (mask-image: paint(squircle))":{"border-radius":i,"mask-image":"none","background-image":"paint(squircle)","border-image-source":"none"}}}}},{values:r("borderRadius"),modifiers:{0:"0",20:"20",40:"40",60:"60",80:"80",100:"100"}})});export{m as default,h as initSquircle};
149
+ `;var l=!1;function f(){if(typeof window<"u"&&"CSS"in window){if(l)return;if(l=!0,typeof CSS.supports=="function"&&CSS.supports("corner-shape","squircle")){document&&document.documentElement&&document.documentElement.classList.add("squircle-native");return}if("registerProperty"in CSS)try{CSS.registerProperty({name:"--tw-squircle-w",syntax:"<length> | <percentage>",inherits:!0,initialValue:"0px"}),CSS.registerProperty({name:"--tw-squircle-smooth",syntax:"<number>",inherits:!0,initialValue:"0.6"})}catch{}if("paintWorklet"in CSS){let e=`data:application/javascript;charset=utf8,${encodeURIComponent(u)}`;CSS.paintWorklet.addModule(e).then(()=>{document&&document.documentElement&&document.documentElement.classList.add("squircle-ready")}).catch(t=>console.error("Squircle worklet failed",t))}}}var w=c(function({addUtilities:e,matchUtilities:t,theme:a}){t({squircle:(r,{modifier:s})=>{let o=.6;if(s){let i=parseFloat(s);isNaN(i)||(o=i>1?i/100:i)}return{"--tw-squircle-smooth":o.toString(),"@supports (corner-shape: squircle)":{"corner-shape":"squircle"},"@supports (not (corner-shape: squircle)) and (mask-image: paint(squircle))":{":where(html.squircle-ready) &":{"mask-image":"paint(squircle)","-webkit-mask-image":"paint(squircle)",'&[class~="border"], &[class*=" border-"], &[class^="border-"]':{"border-image-source":"paint(squircle-border)","border-image-slice":"0 fill","border-image-outset":"var(--tw-squircle-outset, 0)"},"a&, input&, a &, input &":{"mask-image":"none","-webkit-mask-image":"none","background-image":"paint(squircle)","border-image-source":"none"}}}}}},{values:{DEFAULT:"DEFAULT"},modifiers:{0:"0",20:"20",40:"40",60:"60",80:"80",100:"100"}}),t({squircle:(r,{modifier:s})=>{let o=.6;if(s){let i=parseFloat(s);isNaN(i)||(o=i/100)}return{"border-radius":r,"--tw-squircle-w":r,"--tw-squircle-smooth":o.toString(),"@supports (corner-shape: squircle)":{"corner-shape":"squircle"},"@supports (not (corner-shape: squircle)) and (mask-image: paint(squircle))":{":where(html.squircle-ready) &":{"border-radius":"0","mask-image":"paint(squircle)","-webkit-mask-image":"paint(squircle)",'&[class~="border"], &[class*=" border-"], &[class^="border-"]':{"border-image-source":"paint(squircle-border)","border-image-slice":"0 fill","border-image-outset":"var(--tw-squircle-outset, 0)"},"a&, input&, a &, input &":{"border-radius":r,"mask-image":"none","-webkit-mask-image":"none","background-image":"paint(squircle)","border-image-source":"none"}}}}}},{values:a("borderRadius"),modifiers:{0:"0",20:"20",40:"40",60:"60",80:"80",100:"100"}})});export{w as default,f as initSquircle,u as workletCode};
package/package.json CHANGED
@@ -1,10 +1,33 @@
1
1
  {
2
2
  "name": "@usespaceui/squircle",
3
- "version": "0.1.0",
4
- "files": [
5
- "dist"
3
+ "version": "0.1.2",
4
+ "description": "Flawless math-driven squircle masking and geometry rendering for modern UIs.",
5
+ "author": {
6
+ "name": "SpaceUI",
7
+ "email": "hello@spaceui.one",
8
+ "url": "https://www.spaceui.one"
9
+ },
10
+ "contributors": [
11
+ {
12
+ "name": "SpaceUI Team",
13
+ "email": "hello@spaceui.one",
14
+ "url": "https://www.spaceui.one"
15
+ }
6
16
  ],
7
- "description": "Squircle geometry generator",
17
+ "funding": {
18
+ "type": "github",
19
+ "url": "https://github.com/sponsors/usespaceui"
20
+ },
21
+ "license": "MIT",
22
+ "homepage": "https://www.spaceui.one/components/squircle",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/usespaceui/squircle.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/usespaceui/squircle/issues",
29
+ "email": "hello@spaceui.one"
30
+ },
8
31
  "keywords": [
9
32
  "react",
10
33
  "squircle",
@@ -15,9 +38,16 @@
15
38
  "corners",
16
39
  "smooth"
17
40
  ],
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "sideEffects": false,
18
45
  "main": "./dist/index.js",
19
46
  "module": "./dist/index.mjs",
20
47
  "types": "./dist/index.d.ts",
48
+ "files": [
49
+ "dist"
50
+ ],
21
51
  "exports": {
22
52
  ".": {
23
53
  "import": {
@@ -39,14 +69,6 @@
39
69
  "publishConfig": {
40
70
  "access": "public"
41
71
  },
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/usespaceui/squircle.git"
45
- },
46
- "homepage": "https://squircle.spaceui.one",
47
- "bugs": {
48
- "url": "https://github.com/usespaceui/squircle/issues"
49
- },
50
72
  "devDependencies": {
51
73
  "@types/react": "^19.2.18",
52
74
  "react": "^19.2.8",