@usespaceui/squircle 0.1.1 → 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,7 +17,7 @@
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>
@@ -58,7 +58,7 @@
58
58
  ## ✨ Overview
59
59
 
60
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.
61
- 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.
62
62
 
63
63
  ---
64
64
 
@@ -72,7 +72,7 @@ npm install @usespaceui/squircle
72
72
  yarn add @usespaceui/squircle
73
73
  ```
74
74
 
75
- 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).
76
76
 
77
77
  ---
78
78
 
@@ -90,11 +90,12 @@ Since the worklet interacts with the browser DOM, initialize it inside a Client
90
90
  import * as React from 'react'
91
91
  import { initSquircle } from '@usespaceui/squircle'
92
92
 
93
- export function SquircleProvider({ children }: { children: React.ReactNode }) {
94
- React.useEffect(() => {
95
- initSquircle()
96
- }, [])
93
+ // Initialize immediately as client bundle loads before paint
94
+ if (typeof window !== 'undefined') {
95
+ initSquircle()
96
+ }
97
97
 
98
+ export function SquircleProvider({ children }: { children: React.ReactNode }) {
98
99
  return <>{children}</>
99
100
  }
100
101
  ```
@@ -127,29 +128,47 @@ if (typeof window !== 'undefined') {
127
128
  }
128
129
  ```
129
130
 
130
- ## Configure Tailwind CSS 4
131
+ ### Configure Tailwind CSS 4
131
132
 
132
- 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`):
133
134
 
134
135
  ```css
135
- @import '@usespaceui/squircle';
136
+ @import 'tailwindcss';
137
+ @plugin "@usespaceui/squircle";
136
138
  ```
137
139
 
138
140
  ### Configure Tailwind CSS 3
139
141
 
140
- 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`:
141
143
 
142
- ```ts
143
- import squirclePlugin from '@usespaceui/squircle'
144
+ #### CommonJS (`tailwind.config.js`)
144
145
 
146
+ ```js
145
147
  /** @type {import('tailwindcss').Config} */
146
- export default {
148
+ module.exports = {
149
+ content: ['./src/**/*.{js,ts,jsx,tsx}'],
150
+ theme: {
151
+ extend: {},
152
+ },
153
+ plugins: [require('@usespaceui/squircle')],
154
+ }
155
+ ```
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 = {
147
164
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
148
165
  theme: {
149
166
  extend: {},
150
167
  },
151
- plugins: [squirclePlugin],
168
+ plugins: [squircle],
152
169
  }
170
+
171
+ export default config
153
172
  ```
154
173
 
155
174
  ---
@@ -158,76 +177,63 @@ export default {
158
177
 
159
178
  ### 1. Sizing
160
179
 
161
- 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:
162
181
 
163
182
  ```html
164
- <div class="squircle-md p-4 bg-black text-white">Space UI Squircle!</div>
165
- <div class="squircle-2xl">...</div>
166
- <div class="squircle-full">...</div>
167
- <!-- 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>
168
187
 
169
- <!-- Arbitrary values are fully supported -->
170
- <div class="squircle-[40px]">...</div>
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>
195
+
196
+ <!-- Arbitrary values -->
197
+ <div class="squircle rounded-[40px]">...</div>
171
198
  ```
172
199
 
200
+ ---
201
+
173
202
  ### 2. Smoothing Intensity
174
203
 
175
- 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.
176
205
 
177
- 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`):
178
207
 
179
208
  ```html
180
- <!-- Maximum smoothing -->
181
- <div class="squircle-2xl/100">...</div>
209
+ <!-- Maximum smoothing (pronounced superellipse) -->
210
+ <button class="squircle/100 rounded-3xl">...</button>
182
211
 
183
212
  <!-- Minimal smoothing (closer to a plain rounded rectangle) -->
184
- <div class="squircle-2xl/20">...</div>
213
+ <button class="squircle/20 rounded-3xl">...</button>
185
214
 
186
- <!-- Arbitrary smoothing -->
187
- <div class="squircle-2xl/[25]">...</div>
215
+ <!-- Default is 60% (matches Apple iOS & Figma perfectly) -->
216
+ <button class="squircle rounded-3xl">...</button>
188
217
  ```
189
218
 
190
- ### 3. Native Borders (The Magic)
191
-
192
- Use standard Tailwind border utilities. The Houdini worklet reads your native CSS border classes and draws the squircle border perfectly. No workarounds needed.
193
-
194
- ```html
195
- <!-- 8px red border -->
196
- <div class="squircle-2xl border-8 border-red-500 bg-black">...</div>
197
-
198
- <!-- 1px semi-transparent blue border -->
199
- <div class="squircle-[30px] border border-blue-500/50 bg-white">...</div>
200
- ```
201
-
202
- > **Note on border placement:**
203
- > 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:
204
- > `<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.
205
220
 
206
221
  ---
207
222
 
208
- ## 🛠 Advanced: Using with `tailwind-merge`
209
-
210
- 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
211
224
 
212
- Extend your merge configuration to group `squircle` into the `rounded` class group:
213
-
214
- ```ts
215
- import { clsx, type ClassValue } from 'clsx'
216
- import { extendTailwindMerge } from 'tailwind-merge'
217
-
218
- const customTwMerge = extendTailwindMerge({
219
- extend: {
220
- classGroups: {
221
- rounded: [
222
- { squircle: [() => true] }, // Merges any squircle-* class into the rounded group
223
- ],
224
- },
225
- },
226
- })
225
+ Use standard Tailwind border utilities (`border`, `border-2`, `border-8`, etc.). The squircle contour shapes both the background and the border:
227
226
 
228
- export function cn(...inputs: ClassValue[]) {
229
- return customTwMerge(clsx(inputs))
230
- }
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>
231
237
  ```
232
238
 
233
239
  ---
@@ -238,13 +244,13 @@ export function cn(...inputs: ClassValue[]) {
238
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.
239
245
 
240
246
  - `plugin(tailwindcss)`
241
- 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.
242
248
 
243
249
  ---
244
250
 
245
251
  ### Features
246
252
 
247
- - 💃 **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.
248
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.
249
255
  - 👌 **Framework Agnostic:** Works in React, Vue, Svelte, Solid, or plain HTML.
250
256
 
@@ -267,7 +273,7 @@ MIT — Free for commercial and personal use.
267
273
 
268
274
  ## 📚 Resources
269
275
 
270
- - 🔍 [Explore the squircles & Playground](https://squircle.spaceui.one)
276
+ - 🔍 [Explore the squircles & Playground](https://www.spaceui.one/components/squircle)
271
277
  - 🌍 [Space UI Official Site](https://www.spaceui.one)
272
278
 
273
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@usespaceui/squircle",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Flawless math-driven squircle masking and geometry rendering for modern UIs.",
5
5
  "author": {
6
6
  "name": "SpaceUI",
@@ -19,7 +19,7 @@
19
19
  "url": "https://github.com/sponsors/usespaceui"
20
20
  },
21
21
  "license": "MIT",
22
- "homepage": "https://squircle.spaceui.one",
22
+ "homepage": "https://www.spaceui.one/components/squircle",
23
23
  "repository": {
24
24
  "type": "git",
25
25
  "url": "git+https://github.com/usespaceui/squircle.git"