@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 +78 -75
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +52 -17
- package/dist/index.mjs +52 -17
- package/package.json +34 -12
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<a href="https://
|
|
2
|
+
<a href="https://www.spaceui.one/components/squircle" target="_blank">
|
|
3
3
|
<picture>
|
|
4
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://
|
|
5
|
-
<source media="(prefers-color-scheme: light)" srcset="https://
|
|
6
|
-
<img alt="Space UI logo" src="https://
|
|
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://
|
|
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
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
131
|
+
### Configure Tailwind CSS 4
|
|
134
132
|
|
|
135
|
-
|
|
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 '
|
|
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
|
-
|
|
146
|
-
import squirclePlugin from '@usespaceui/squircle'
|
|
144
|
+
#### CommonJS (`tailwind.config.js`)
|
|
147
145
|
|
|
146
|
+
```js
|
|
148
147
|
/** @type {import('tailwindcss').Config} */
|
|
149
|
-
|
|
148
|
+
module.exports = {
|
|
150
149
|
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
|
151
150
|
theme: {
|
|
152
151
|
extend: {},
|
|
153
152
|
},
|
|
154
|
-
plugins: [
|
|
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
|
-
|
|
180
|
+
Apply the `squircle` utility alongside your Tailwind `rounded-*` classes:
|
|
165
181
|
|
|
166
182
|
```html
|
|
167
|
-
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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,
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
198
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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
|
|
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://
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
|
|
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
|
-
"
|
|
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",
|