@@ -36,6 +36,60 @@ export interface ResizeObserverWithCleanup extends ResizeObserver {
3636 * FR-018 to FR-022: SVG clip-path implementation with ResizeObserver
3737 */
3838export class ClipPathRenderer {
39+ private static borderStylesInjected = false ;
40+
41+ /**
42+ * Inject global CSS styles for squircle borders (once per page)
43+ * Uses ::before for border layer, ::after for content background
44+ * Main element has NO clip-path to allow pseudo-elements to show
45+ */
46+ private static injectBorderStyles ( ) : void {
47+ if ( this . borderStylesInjected || typeof document === 'undefined' ) {
48+ return ;
49+ }
50+
51+ const style = document . createElement ( 'style' ) ;
52+ style . id = 'cornerkit-border-styles' ;
53+ style . textContent = `
54+ [data-squircle-border]::before {
55+ content: '';
56+ position: absolute;
57+ top: calc(var(--squircle-border-width, 0px) * -1);
58+ left: calc(var(--squircle-border-width, 0px) * -1);
59+ width: calc(100% + var(--squircle-border-width, 0px) * 2);
60+ height: calc(100% + var(--squircle-border-width, 0px) * 2);
61+ background: var(--squircle-border-color, transparent);
62+ clip-path: var(--squircle-border-path);
63+ z-index: 0;
64+ pointer-events: none;
65+ border-radius: 0;
66+ }
67+ [data-squircle-border]::after {
68+ content: '';
69+ position: absolute;
70+ top: 0;
71+ left: 0;
72+ width: 100%;
73+ height: 100%;
74+ background-color: var(--squircle-content-bg-color, transparent);
75+ background-image: var(--squircle-content-bg-image, none);
76+ background-size: var(--squircle-content-bg-size, auto);
77+ background-position: var(--squircle-content-bg-position, 0% 0%);
78+ background-repeat: var(--squircle-content-bg-repeat, repeat);
79+ clip-path: var(--squircle-content-path);
80+ z-index: 1;
81+ pointer-events: none;
82+ border-radius: 0;
83+ }
84+ [data-squircle-border] > * {
85+ position: relative;
86+ z-index: 2;
87+ }
88+ ` ;
89+ document . head . appendChild ( style ) ;
90+ this . borderStylesInjected = true ;
91+ }
92+
3993 /**
4094 * FR-018: Apply squircle clip-path to an element
4195 * Generates SVG path and sets element.style.clipPath
@@ -101,6 +155,9 @@ export class ClipPathRenderer {
101155 remove ( element : HTMLElement , originalTransition ?: string ) : void {
102156 element . style . clipPath = '' ;
103157
158+ // Remove border properties
159+ this . removeBorderProperties ( element ) ;
160+
104161 // Restore original transition if provided
105162 if ( originalTransition !== undefined ) {
106163 element . style . transition = originalTransition ;
@@ -115,7 +172,7 @@ export class ClipPathRenderer {
115172 * @param config - Squircle configuration
116173 */
117174 private updateClipPath ( element : HTMLElement , config : SquircleConfig ) : void {
118- const { radius, smoothing } = config ;
175+ const { radius, smoothing, borderWidth , borderColor } = config ;
119176
120177 // Get current element dimensions
121178 const width = element . offsetWidth ;
@@ -129,8 +186,83 @@ export class ClipPathRenderer {
129186 // Generate SVG path string
130187 const path = generateSquirclePath ( width , height , radius , smoothing ) ;
131188
132- // Apply clip-path CSS property
133- element . style . clipPath = `path('${ path } ')` ;
189+ // Handle border rendering via ::before and ::after pseudo-elements
190+ if ( borderWidth && borderWidth > 0 && borderColor ) {
191+ // Inject global border styles (once per page)
192+ ClipPathRenderer . injectBorderStyles ( ) ;
193+
194+ // DON'T apply clip-path to main element - let pseudo-elements handle it
195+ // This prevents the parent's clip-path from cutting off the border
196+ element . style . clipPath = 'none' ;
197+
198+ // Calculate border path (for the ::before element which is larger)
199+ const borderElementWidth = width + borderWidth * 2 ;
200+ const borderElementHeight = height + borderWidth * 2 ;
201+ const borderPath = generateSquirclePath (
202+ borderElementWidth ,
203+ borderElementHeight ,
204+ radius + borderWidth , // Increase radius proportionally
205+ smoothing
206+ ) ;
207+
208+ // Capture original background ONLY ONCE (before we set it to transparent)
209+ // This prevents recapturing the transparent value on subsequent updates
210+ if ( ! element . dataset [ 'squircleOriginalBg' ] ) {
211+ const computedStyle = getComputedStyle ( element ) ;
212+
213+ // Store individual background properties in CSS variables
214+ element . style . setProperty ( '--squircle-content-bg-color' , computedStyle . backgroundColor ) ;
215+ element . style . setProperty ( '--squircle-content-bg-image' , computedStyle . backgroundImage ) ;
216+ element . style . setProperty ( '--squircle-content-bg-size' , computedStyle . backgroundSize ) ;
217+ element . style . setProperty ( '--squircle-content-bg-position' , computedStyle . backgroundPosition ) ;
218+ element . style . setProperty ( '--squircle-content-bg-repeat' , computedStyle . backgroundRepeat ) ;
219+
220+ // Mark as captured
221+ element . dataset [ 'squircleOriginalBg' ] = 'captured' ;
222+ }
223+
224+ // Set CSS custom properties for pseudo-elements (border path updates on resize)
225+ element . style . setProperty ( '--squircle-border-width' , `${ borderWidth } px` ) ;
226+ element . style . setProperty ( '--squircle-border-color' , borderColor ) ;
227+ element . style . setProperty ( '--squircle-border-path' , `path('${ borderPath } ')` ) ;
228+ element . style . setProperty ( '--squircle-content-path' , `path('${ path } ')` ) ;
229+
230+ // Make main element's background transparent (::after will show it)
231+ element . style . background = 'transparent' ;
232+
233+ // Mark element for border styling and ensure position context
234+ element . dataset [ 'squircleBorder' ] = 'true' ;
235+ const computedStyle = getComputedStyle ( element ) ;
236+ const computedPosition = computedStyle . position ;
237+ if ( computedPosition === 'static' ) {
238+ element . style . position = 'relative' ;
239+ }
240+ } else {
241+ // No borders - apply clip-path directly to element
242+ element . style . clipPath = `path('${ path } ')` ;
243+ // Remove border properties if not configured
244+ this . removeBorderProperties ( element ) ;
245+ }
246+ }
247+
248+ /**
249+ * Remove border-related CSS properties from element
250+ * @param element - Target HTMLElement
251+ */
252+ private removeBorderProperties ( element : HTMLElement ) : void {
253+ element . style . removeProperty ( '--squircle-border-width' ) ;
254+ element . style . removeProperty ( '--squircle-border-color' ) ;
255+ element . style . removeProperty ( '--squircle-border-path' ) ;
256+ element . style . removeProperty ( '--squircle-content-path' ) ;
257+ element . style . removeProperty ( '--squircle-content-bg-color' ) ;
258+ element . style . removeProperty ( '--squircle-content-bg-image' ) ;
259+ element . style . removeProperty ( '--squircle-content-bg-size' ) ;
260+ element . style . removeProperty ( '--squircle-content-bg-position' ) ;
261+ element . style . removeProperty ( '--squircle-content-bg-repeat' ) ;
262+ delete element . dataset [ 'squircleBorder' ] ;
263+ delete element . dataset [ 'squircleOriginalBg' ] ;
264+ // Note: We don't restore element.style.background here because
265+ // the element will get the standard clip-path instead
134266 }
135267
136268 /**
0 commit comments