serpentine-border 1.0.0 → 1.0.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,6 +1,6 @@
1
1
  # serpentine-border
2
2
 
3
- A multi-stroke serpentine (wavy) border drawn with SVG. Use from vanilla JS or as a React component.
3
+ A multi-stroke serpentine (wavy) border drawn with SVG. Usable as a helper function for building the border in vanillaJS (see example) or as a React component.
4
4
 
5
5
  ## Install
6
6
 
@@ -15,24 +15,26 @@ Pass a wrapper element; the border is computed from the measured heights of its
15
15
  ```js
16
16
  import { serpentineBorder } from 'serpentine-border'
17
17
 
18
- const wrapper = document.getElementById('wrapper')
19
- const result = serpentineBorder({ wrapperEl: wrapper })
20
- if (result) {
21
- Object.assign(wrapper.style, result.wrapperStyle)
22
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
23
- if (result.svgAttributes.class) svg.setAttribute('class', result.svgAttributes.class)
24
- svg.setAttribute('viewBox', result.svgAttributes.viewBox)
25
- Object.assign(svg.style, result.svgAttributes.style)
26
- result.paths.forEach((p) => {
27
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
28
- path.setAttribute('d', p.d)
29
- path.setAttribute('stroke', p.stroke)
30
- path.setAttribute('stroke-width', String(p.strokeWidth))
31
- path.setAttribute('fill', p.fill)
32
- svg.appendChild(path)
33
- })
34
- wrapper.insertBefore(svg, wrapper.firstChild)
18
+ function setAttributes(el, attrs) {
19
+ for (const [key, value] of Object.entries(attrs)) {
20
+ if (value == null) continue
21
+ el.setAttribute(key, String(value))
22
+ }
23
+ }
24
+
25
+ const wrapperEl = document.getElementById('wrapper')
26
+ const { wrapperStyle, svgAttributes, paths } = serpentineBorder({ wrapperEl })
27
+ Object.assign(wrapperEl.style, wrapperStyle)
28
+
29
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
30
+ setAttributes(svg, svgAttributes)
31
+
32
+ for (const p of paths) {
33
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
34
+ setAttributes(path, p)
35
+ svg.appendChild(path)
35
36
  }
37
+ wrapperEl.insertBefore(svg, wrapperEl.firstChild)
36
38
  ```
37
39
 
38
40
  ## API
@@ -51,12 +53,10 @@ Returns `wrapperStyle`, `svgAttributes` (class, viewBox, style), and `paths`. Pa
51
53
  | `radius` | `number` | `50` | Radius of the wavy turns in px. |
52
54
  | `horizontalOverlap` | `number \| 'borderWidth' \| 'halfBorderWidth'` | `0` | Extra width per side (px or keyword). |
53
55
  | `colors` | `string[]` | `['#ffffff', '#000000']` | Stroke colors (hex/CSS). |
54
- | `layoutMode` | `'content' \| 'border'` | `'content'` | `'content'`: layout from content; `'border'`: outer border edge defines box. |
56
+ | `layoutMode` | `'content' \| 'border'` | `'border'` | See note below. |
55
57
  | `svgClassName` | `string` | `'serpentine-border-svg'` | Class applied to the SVG (and used to exclude it when measuring). |
56
58
 
57
- ### measureSections(wrapperEl, options)
58
-
59
- Measures a wrapper and its section children; returns `{ width, sectionBottomYs }` or `null`. Options: `layoutMode`, `horizontalOverlap`, `strokeCount`, `strokeWidth`, and optional `excludeClassName` (default: same as `serpentineBorder`’s `svgClassName`). Use when you want to measure once and pass dimensions into `serpentineBorder`, or in environments without a DOM.
59
+ **Layout mode:** In some instances, you may want the border to be an overlay that doesn't affect flow and content size. With `'content'`, the wrapper’s size follows its content and the border is drawn around it (the SVG can extend outside). With `'border'`, the outer edge of the border defines the box: the full border fits inside the layout, and content sits inside that box. This mode avoids the border spilling out and overlapping neighboring elements.
60
60
 
61
61
  ### React: SerpentineBorder
62
62
 
@@ -81,31 +81,13 @@ function App() {
81
81
  }
82
82
  ```
83
83
 
84
- ## Explicit dimensions and SSR
84
+ ## SSR
85
85
 
86
- When you need to measure once and reuse, or run without a DOM (SSR, workers), use `measureSections` then call `serpentineBorder` with `width` and `sectionBottomYs`:
86
+ When DOM is unavailable (e.g. server-side), pass `width` and `sectionBottomYs` into `serpentineBorder({ width, sectionBottomYs, ... })` instead of `wrapperEl`.
87
87
 
88
- ```js
89
- import { measureSections, serpentineBorder } from 'serpentine-border'
90
-
91
- const wrapper = document.getElementById('wrapper')
92
- const measured = measureSections(wrapper, {
93
- layoutMode: 'content',
94
- horizontalOverlap: 20,
95
- strokeCount: 5,
96
- strokeWidth: 8,
97
- })
98
- if (measured) {
99
- const result = serpentineBorder({
100
- width: measured.width,
101
- sectionBottomYs: measured.sectionBottomYs,
102
- horizontalOverlap: 20,
103
- })
104
- if (result) {
105
- // Apply result.wrapperStyle, result.svgAttributes, result.paths
106
- }
107
- }
108
- ```
88
+ ## Tests
89
+
90
+ Run `npm run test` for e2e tests. If Playwright browsers are not installed yet, run `npm run test:install-browsers` first. On Linux you may also need to install system dependencies (e.g. `sudo npx playwright install-deps`).
109
91
 
110
92
  ## License
111
93
 
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const W=require("react/jsx-runtime"),z=require("react"),J=["#ffffff","#000000"];function G(t,c,o){if(typeof t=="number")return t;const s=c*o;return t==="borderWidth"?s:t==="halfBorderWidth"?s/2:0}function Q(t,c,o,s,u,g,l,n,r,h){const e=u*(o-1),a=u/2,$=c.length-1,i=[];for(let f=0;f<o;f++){const d=f*u,m=(o-1-f)*u,v=s-d,B=s-m,C=e-m,k=r-h+a,x=d-r+k,L=h,b=t+L-m-a,y=x+(s-d),N=t+L-s-a,A=t+L-e-a,D=r+n,p=[`M ${b} ${D-e-u/2-l}`,`L ${b} ${D-e-l}`,`A ${C} ${C} 0 0 1 ${A} ${D-m-l}`,`L ${y} ${d+n-l}`,`A ${v} ${v} 0 0 0 ${x} ${s+n-l}`,`L ${x} ${s+n}`];for(let M=0;M<$-1;M++){const w=c[M+1],X=c[M+2],q=w+s-r+n;M%2===0?(p.push(`L ${x} ${w-s+n}`),p.push(`A ${v} ${v} 0 0 0 ${y} ${w-d+n}`),p.push(`L ${N} ${w-d+n}`),p.push(`A ${B} ${B} 0 0 1 ${b} ${q}`),p.push(`L ${b} ${X-s+n}`)):(p.push(`L ${b} ${w-s+n}`),p.push(`A ${B} ${B} 0 0 1 ${N} ${w-m+n}`),p.push(`L ${y} ${w-m+n}`),p.push(`A ${v} ${v} 0 0 0 ${x} ${q}`),p.push(`L ${x} ${X-s+n}`))}const U=c[$];($-2)%2===0?p.push(`L ${b} ${U}`):p.push(`L ${x} ${U}`),i.push({d:p.join(" "),stroke:g[f%g.length],strokeWidth:u,fill:"none"})}return i}const P="serpentine-border-svg",j={strokeCount:5,strokeWidth:8,radius:50,horizontalOverlap:0,layoutMode:"content"};function V(t){const c=t.strokeCount??j.strokeCount,o=t.strokeWidth??j.strokeWidth,s=t.radius??j.radius,u=t.horizontalOverlap??j.horizontalOverlap,g=t.colors??J,l=t.layoutMode??j.layoutMode,n=t.svgClassName??P;let r,h;if(t.wrapperEl!=null){const y=t.wrapperEl;if(!(typeof document<"u"&&typeof y.getBoundingClientRect=="function"))return null;const A=R(y,{layoutMode:l,horizontalOverlap:u,strokeCount:c,strokeWidth:o,excludeClassName:n});if(!A)return null;r=A.width,h=A.sectionBottomYs}else{if(t.width==null||t.sectionBottomYs==null)return null;r=t.width,h=t.sectionBottomYs}const e=G(u,c,o),a=(c-1)*o,$=c*o,i=2*o,f=a/2,d=(c-1)/2*o+f,S=l==="border"?{boxSizing:"border-box",position:"relative",marginTop:$/2,...e>0&&{paddingLeft:e,paddingRight:e}}:{position:"relative",boxSizing:"border-box"},m=l==="border"?{position:"absolute",overflow:"hidden",width:"100%",left:0,top:-(i+d),height:`calc(100% + ${i+d}px)`}:{position:"absolute",overflow:"hidden",width:`calc(100% + ${2*e}px)`,left:-e,top:-(i+d),height:`calc(100% + ${i+d}px)`},v=Q(r,h,c,s,o,g,d,f,a,e),B=h[h.length-1]??0,C=Math.max(1,r+2*e),k=B+i+d,x=e>0?-e:0,L=-o*2-d,b=`${x} ${L} ${C} ${k}`;return{wrapperStyle:S,svgAttributes:{class:n,viewBox:b,style:m},paths:v}}function R(t,c){const{layoutMode:o,horizontalOverlap:s=0,strokeCount:u,strokeWidth:g,excludeClassName:l=P}=c,n=G(s,u,g);if(!t)return null;const r=l?Array.from(t.children).filter(i=>!i.classList.contains(l)):Array.from(t.children);if(r.length===0)return null;const h=t.getBoundingClientRect(),e=h.width,a=o==="border"?Math.max(1,e-2*n):Math.max(1,e),$=[0];for(let i=0;i<r.length;i++){const f=r[i].getBoundingClientRect();$.push(f.top-h.top+f.height)}return{width:a,sectionBottomYs:$}}function Z({children:t,strokeCount:c,strokeWidth:o,radius:s,horizontalOverlap:u,colors:g,layoutMode:l}){const n=z.useRef(null),[r,h]=z.useState(null);return z.useEffect(()=>{const e=n.current;if(!e)return;const a=()=>{const i=R(e,{layoutMode:l,horizontalOverlap:u,strokeCount:c,strokeWidth:o});if(!i)return;const f=V({width:i.width,sectionBottomYs:i.sectionBottomYs,strokeCount:c,strokeWidth:o,radius:s,horizontalOverlap:u,colors:g,layoutMode:l});h(f)};a();const $=new ResizeObserver(a);return $.observe(e),()=>$.disconnect()},[c,o,s,u,g,l]),W.jsxs("div",{ref:n,className:"serpentine-wrapper",style:(r==null?void 0:r.wrapperStyle)??{position:"relative",boxSizing:"border-box"},"data-testid":"serpentine-wrapper",children:[r&&(()=>{const{class:e,...a}=r.svgAttributes;return W.jsx("svg",{"data-testid":"serpentine-svg",className:e,...a,children:r.paths.map(($,i)=>W.jsx("path",{...$},i))})})(),t]})}exports.SerpentineBorder=Z;exports.measureSections=R;exports.serpentineBorder=V;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const S=require("react/jsx-runtime"),D=require("react"),Z=["#ffffff","#000000"];function q(t,s,e){if(typeof t=="number")return t;const n=s*e;return t==="borderWidth"?n:t==="halfBorderWidth"?n/2:0}function J(t){return Object.entries(t).map(([s,e])=>{const n=s.replace(/([A-Z])/g,"-$1").toLowerCase(),i=typeof e=="number"&&!Number.isNaN(e)?`${e}px`:String(e);return`${n}: ${i}`}).join("; ")}function Q(t,s,e,n,i,g,u,o,c,f){const r=i*(e-1),a=i/2,p=s.length-1,l=[];for(let h=0;h<e;h++){const d=h*i,b=(e-1-h)*i,m=n-d,B=n-b,k=r-b,N=c-f+a,x=d-c+N,L=f,y=t+L-b-a,j=x+(n-d),A=t+L-n-a,z=t+L-r-a,w=c+o,$=[`M ${y} ${w-r-i/2-u}`,`L ${y} ${w-r-u}`,`A ${k} ${k} 0 0 1 ${z} ${w-b-u}`,`L ${j} ${d+o-u}`,`A ${m} ${m} 0 0 0 ${x} ${n+o-u}`,`L ${x} ${n+o}`];for(let C=0;C<p-1;C++){const v=s[C+1],U=s[C+2],X=v+n-c+o;C%2===0?($.push(`L ${x} ${v-n+o}`),$.push(`A ${m} ${m} 0 0 0 ${j} ${v-d+o}`),$.push(`L ${A} ${v-d+o}`),$.push(`A ${B} ${B} 0 0 1 ${y} ${X}`),$.push(`L ${y} ${U-n+o}`)):($.push(`L ${y} ${v-n+o}`),$.push(`A ${B} ${B} 0 0 1 ${A} ${v-b+o}`),$.push(`L ${j} ${v-b+o}`),$.push(`A ${m} ${m} 0 0 0 ${x} ${X}`),$.push(`L ${x} ${U-n+o}`))}const R=s[p];(p-2)%2===0?$.push(`L ${y} ${R}`):$.push(`L ${x} ${R}`),l.push({d:$.join(" "),stroke:g[h%g.length],"stroke-width":i,fill:"none"})}return l}const G="serpentine-border-svg",M={strokeCount:5,strokeWidth:8,radius:50,horizontalOverlap:0,layoutMode:"border"};function P(t){const s=t.strokeCount??M.strokeCount,e=t.strokeWidth??M.strokeWidth,n=t.radius??M.radius,i=t.horizontalOverlap??M.horizontalOverlap,g=t.colors??Z,u=t.layoutMode??M.layoutMode,o=t.svgClassName??G;let c,f;if(t.wrapperEl!=null){const A=t.wrapperEl;if(!(typeof document<"u"&&typeof A.getBoundingClientRect=="function"))return null;const w=V(A,{layoutMode:u,horizontalOverlap:i,strokeCount:s,strokeWidth:e,excludeClassName:o});if(!w)return null;c=w.width,f=w.sectionBottomYs}else{if(t.width==null||t.sectionBottomYs==null)return null;c=t.width,f=t.sectionBottomYs}const r=q(i,s,e),a=(s-1)*e,p=s*e,l=2*e,h=a/2,d=(s-1)/2*e+h,W=u==="border"?{boxSizing:"border-box",position:"relative",marginTop:`${p/2}px`,...r>0&&{paddingLeft:`${r}px`,paddingRight:`${r}px`}}:{position:"relative",boxSizing:"border-box"},b=u==="border"?{position:"absolute",overflow:"hidden",width:"100%",left:0,top:-(l+d),height:`calc(100% + ${l+d}px)`}:{position:"absolute",overflow:"hidden",width:`calc(100% + ${2*r}px)`,left:-r,top:-(l+d),height:`calc(100% + ${l+d}px)`},m=J(b),B=Q(c,f,s,n,e,g,d,h,a,r),k=f[f.length-1]??0,N=Math.max(1,c+2*r),x=k+l+d,L=r>0?-r:0,y=-e*2-d,j=`${L} ${y} ${N} ${x}`;return{wrapperStyle:W,svgAttributes:{class:o,viewBox:j,style:m},paths:B}}function V(t,s){const{layoutMode:e,horizontalOverlap:n=0,strokeCount:i,strokeWidth:g,excludeClassName:u=G}=s,o=q(n,i,g);if(!t)return null;const c=u?Array.from(t.children).filter(l=>!l.classList.contains(u)):Array.from(t.children);if(c.length===0)return null;const f=t.getBoundingClientRect(),r=f.width,a=e==="border"?Math.max(1,r-2*o):Math.max(1,r),p=[0];for(let l=0;l<c.length;l++){const h=c[l].getBoundingClientRect();p.push(h.top-f.top+h.height)}return{width:a,sectionBottomYs:p}}function E(t){const s={};if(!t||typeof t!="string")return s;for(const e of t.split(";")){const n=e.indexOf(":");if(n===-1)continue;const i=e.slice(0,n).trim().replace(/-([a-z])/g,(u,o)=>o.toUpperCase()),g=e.slice(n+1).trim();i&&(s[i]=g)}return s}function O(t){return Object.fromEntries(Object.entries(t).map(([s,e])=>[s==="stroke-width"?"strokeWidth":s,e]))}function H({children:t,strokeCount:s,strokeWidth:e,radius:n,horizontalOverlap:i,colors:g,layoutMode:u}){const o=D.useRef(null),[c,f]=D.useState(null);return D.useEffect(()=>{const r=o.current;if(!r)return;const a=()=>{const l=V(r,{layoutMode:u,horizontalOverlap:i,strokeCount:s,strokeWidth:e});if(!l)return;const h=P({width:l.width,sectionBottomYs:l.sectionBottomYs,strokeCount:s,strokeWidth:e,radius:n,horizontalOverlap:i,colors:g,layoutMode:u});f(h)};a();const p=new ResizeObserver(a);return p.observe(r),()=>p.disconnect()},[s,e,n,i,g,u]),S.jsxs("div",{ref:o,className:"serpentine-wrapper",style:(c==null?void 0:c.wrapperStyle)??{position:"relative",boxSizing:"border-box"},"data-testid":"serpentine-wrapper",children:[c&&(()=>{const{class:r,style:a,...p}=c.svgAttributes,l=typeof a=="string"?E(a):a;return S.jsx("svg",{"data-testid":"serpentine-svg",className:r,style:l,...p,children:c.paths.map((h,d)=>S.jsx("path",{...O(h)},d))})})(),t]})}exports.SerpentineBorder=H;exports.serpentineBorder=P;
2
2
  //# sourceMappingURL=serpentine-border.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"serpentine-border.cjs","sources":["../src/constants.js","../src/serpentineCore.js","../src/SerpentineBorder.jsx"],"sourcesContent":["export const DEFAULT_COLORS = ['#ffffff', '#000000']\n","/**\n * Vanilla JS core for serpentine border SVG generation.\n * Single export: call with measured dimensions and options to get everything needed to render.\n */\n\nimport { DEFAULT_COLORS } from './constants.js'\n\nfunction resolveOverlapToPixels(horizontalOverlap, N, STROKE_WIDTH) {\n if (typeof horizontalOverlap === 'number') return horizontalOverlap\n const totalBorderWidth = N * STROKE_WIDTH\n if (horizontalOverlap === 'borderWidth') return totalBorderWidth\n if (horizontalOverlap === 'halfBorderWidth') return totalBorderWidth / 2\n return 0\n}\n\nfunction buildPathD(W, Y, N, R, STROKE_WIDTH, COLORS, TOP_ARC_SHIFT, Y_OFFSET, O_TOTAL, BORDER_EXTRA) {\n const R1 = STROKE_WIDTH * (N - 1)\n const RIGHT_EXTEND = STROKE_WIDTH / 2\n const n = Y.length - 1\n const parts = []\n for (let i = 0; i < N; i++) {\n const o = i * STROKE_WIDTH\n const j = N - 1 - i\n const oj = j * STROKE_WIDTH\n const r = R - o\n const rj = R - oj\n const r1 = R1 - o\n const rj1 = R1 - oj\n\n const leftOffset = O_TOTAL - BORDER_EXTRA + RIGHT_EXTEND\n const xLeft = o - O_TOTAL + leftOffset\n const rightExt = BORDER_EXTRA\n const xRight = W + rightExt - oj - RIGHT_EXTEND\n const xLeftArc = xLeft + (R - o)\n const xRightArc = W + rightExt - R - RIGHT_EXTEND\n const xRightR1 = W + rightExt - R1 - RIGHT_EXTEND\n\n const yCurrTop = O_TOTAL + Y_OFFSET\n const segs = [\n `M ${xRight} ${yCurrTop - R1 - STROKE_WIDTH / 2 - TOP_ARC_SHIFT}`,\n `L ${xRight} ${yCurrTop - R1 - TOP_ARC_SHIFT}`,\n `A ${rj1} ${rj1} 0 0 1 ${xRightR1} ${yCurrTop - oj - TOP_ARC_SHIFT}`,\n `L ${xLeftArc} ${o + Y_OFFSET - TOP_ARC_SHIFT}`,\n `A ${r} ${r} 0 0 0 ${xLeft} ${R + Y_OFFSET - TOP_ARC_SHIFT}`,\n `L ${xLeft} ${R + Y_OFFSET}`,\n ]\n\n for (let t = 0; t < n - 1; t++) {\n const yCurr = Y[t + 1]\n const yNext = Y[t + 2]\n const yExit = yCurr + R - O_TOTAL + Y_OFFSET\n\n if (t % 2 === 0) {\n segs.push(`L ${xLeft} ${yCurr - R + Y_OFFSET}`)\n segs.push(`A ${r} ${r} 0 0 0 ${xLeftArc} ${yCurr - o + Y_OFFSET}`)\n segs.push(`L ${xRightArc} ${yCurr - o + Y_OFFSET}`)\n segs.push(`A ${rj} ${rj} 0 0 1 ${xRight} ${yExit}`)\n segs.push(`L ${xRight} ${yNext - R + Y_OFFSET}`)\n } else {\n segs.push(`L ${xRight} ${yCurr - R + Y_OFFSET}`)\n segs.push(`A ${rj} ${rj} 0 0 1 ${xRightArc} ${yCurr - oj + Y_OFFSET}`)\n segs.push(`L ${xLeftArc} ${yCurr - oj + Y_OFFSET}`)\n segs.push(`A ${r} ${r} 0 0 0 ${xLeft} ${yExit}`)\n segs.push(`L ${xLeft} ${yNext - R + Y_OFFSET}`)\n }\n }\n\n const lastY = Y[n]\n if ((n - 2) % 2 === 0) {\n segs.push(`L ${xRight} ${lastY}`)\n } else {\n segs.push(`L ${xLeft} ${lastY}`)\n }\n parts.push({\n d: segs.join(' '),\n stroke: COLORS[i % COLORS.length],\n strokeWidth: STROKE_WIDTH,\n fill: 'none',\n })\n }\n return parts\n}\n\nconst DEFAULT_SVG_CLASS = 'serpentine-border-svg'\n\nconst DEFAULTS = {\n strokeCount: 5,\n strokeWidth: 8,\n radius: 50,\n horizontalOverlap: 0,\n layoutMode: 'content',\n}\n\n/**\n * Compute everything needed to render the serpentine border.\n * Accepts either (width + sectionBottomYs) for pure/custom use, or wrapperEl to measure from the DOM.\n * When using wrapperEl, returns null in non-DOM environments (e.g. SSR) or when measurement fails.\n *\n * @param {{\n * width?: number\n * sectionBottomYs?: number[]\n * wrapperEl?: HTMLElement\n * strokeCount?: number\n * strokeWidth?: number\n * radius?: number\n * horizontalOverlap?: number | 'borderWidth' | 'halfBorderWidth'\n * colors?: string[]\n * layoutMode?: 'content' | 'border'\n * svgClassName?: string\n * }} options\n * @returns {{\n * wrapperStyle: Record<string, unknown>\n * svgAttributes: { class?: string, viewBox: string, style: Record<string, unknown> }\n * paths: Array<{ d: string, stroke: string, strokeWidth: number, fill: string }>\n * } | null}\n */\nexport function serpentineBorder(options) {\n const N = options.strokeCount ?? DEFAULTS.strokeCount\n const STROKE_WIDTH = options.strokeWidth ?? DEFAULTS.strokeWidth\n const R = options.radius ?? DEFAULTS.radius\n const horizontalOverlap = options.horizontalOverlap ?? DEFAULTS.horizontalOverlap\n const COLORS = options.colors ?? DEFAULT_COLORS\n const layoutMode = options.layoutMode ?? DEFAULTS.layoutMode\n const svgClassName = options.svgClassName ?? DEFAULT_SVG_CLASS\n\n let W, Y\n if (options.wrapperEl != null) {\n const wrapperEl = options.wrapperEl\n const hasDOM = typeof document !== 'undefined' && typeof wrapperEl.getBoundingClientRect === 'function'\n if (!hasDOM) return null\n const measured = measureSections(wrapperEl, {\n layoutMode,\n horizontalOverlap,\n strokeCount: N,\n strokeWidth: STROKE_WIDTH,\n excludeClassName: svgClassName,\n })\n if (!measured) return null\n W = measured.width\n Y = measured.sectionBottomYs\n } else {\n if (options.width == null || options.sectionBottomYs == null) return null\n W = options.width\n Y = options.sectionBottomYs\n }\n\n const BORDER_EXTRA = resolveOverlapToPixels(horizontalOverlap, N, STROKE_WIDTH)\n const O_TOTAL = (N - 1) * STROKE_WIDTH\n const TOTAL_BORDER_WIDTH = N * STROKE_WIDTH\n const TOP_OFFSET = 2 * STROKE_WIDTH\n const Y_OFFSET = O_TOTAL / 2\n const TOP_ARC_SHIFT = ((N - 1) / 2) * STROKE_WIDTH + Y_OFFSET\n\n const wrapperStyle =\n layoutMode === 'border'\n ? {\n boxSizing: 'border-box',\n position: 'relative',\n marginTop: TOTAL_BORDER_WIDTH / 2,\n ...(BORDER_EXTRA > 0 && {\n paddingLeft: BORDER_EXTRA,\n paddingRight: BORDER_EXTRA,\n }),\n }\n : {\n position: 'relative',\n boxSizing: 'border-box',\n }\n\n const svgStyle =\n layoutMode === 'border'\n ? {\n position: 'absolute',\n overflow: 'hidden',\n width: '100%',\n left: 0,\n top: -(TOP_OFFSET + TOP_ARC_SHIFT),\n height: `calc(100% + ${TOP_OFFSET + TOP_ARC_SHIFT}px)`,\n }\n : {\n position: 'absolute',\n overflow: 'hidden',\n width: `calc(100% + ${2 * BORDER_EXTRA}px)`,\n left: -BORDER_EXTRA,\n top: -(TOP_OFFSET + TOP_ARC_SHIFT),\n height: `calc(100% + ${TOP_OFFSET + TOP_ARC_SHIFT}px)`,\n }\n\n const paths = buildPathD(W, Y, N, R, STROKE_WIDTH, COLORS, TOP_ARC_SHIFT, Y_OFFSET, O_TOTAL, BORDER_EXTRA)\n\n const totalHeight = Y[Y.length - 1] ?? 0\n const totalWidth = Math.max(1, W + 2 * BORDER_EXTRA)\n const viewBoxHeight = totalHeight + TOP_OFFSET + TOP_ARC_SHIFT\n const viewBoxMinX = BORDER_EXTRA > 0 ? -BORDER_EXTRA : 0\n const viewBoxMinY = -STROKE_WIDTH * 2 - TOP_ARC_SHIFT\n const viewBoxStr = `${viewBoxMinX} ${viewBoxMinY} ${totalWidth} ${viewBoxHeight}`\n\n return {\n wrapperStyle,\n svgAttributes: {\n class: svgClassName,\n viewBox: viewBoxStr,\n style: svgStyle,\n },\n paths,\n }\n}\n\n/**\n * Measure wrapper and section elements to get width and section bottom Ys.\n * Children with the excludeClassName (default: same class used on the SVG by serpentineBorder) are excluded.\n * horizontalOverlap is resolved to pixels using strokeCount and strokeWidth.\n *\n * @param {HTMLElement} wrapperEl\n * @param {{\n * layoutMode: 'content' | 'border'\n * horizontalOverlap?: number | 'borderWidth' | 'halfBorderWidth'\n * strokeCount: number\n * strokeWidth: number\n * excludeClassName?: string\n * }} options\n * @returns {{ width: number, sectionBottomYs: number[] } | null}\n */\nexport function measureSections(wrapperEl, options) {\n const { layoutMode, horizontalOverlap = 0, strokeCount, strokeWidth, excludeClassName = DEFAULT_SVG_CLASS } = options\n const BORDER_EXTRA = resolveOverlapToPixels(horizontalOverlap, strokeCount, strokeWidth)\n if (!wrapperEl) return null\n\n const sectionEls = excludeClassName\n ? Array.from(wrapperEl.children).filter((el) => !el.classList.contains(excludeClassName))\n : Array.from(wrapperEl.children)\n\n if (sectionEls.length === 0) return null\n\n const rect = wrapperEl.getBoundingClientRect()\n const baseWidth = rect.width\n\n const W =\n layoutMode === 'border'\n ? Math.max(1, baseWidth - 2 * BORDER_EXTRA)\n : Math.max(1, baseWidth)\n\n const Y = [0]\n for (let i = 0; i < sectionEls.length; i++) {\n const r = sectionEls[i].getBoundingClientRect()\n Y.push(r.top - rect.top + r.height)\n }\n\n return { width: W, sectionBottomYs: Y }\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { measureSections, serpentineBorder } from './serpentineCore.js'\n\nfunction SerpentineBorder({\n children,\n strokeCount,\n strokeWidth,\n radius,\n horizontalOverlap,\n colors,\n layoutMode,\n}) {\n const wrapperRef = useRef(null)\n const [borderData, setBorderData] = useState(null)\n\n useEffect(() => {\n const wrapper = wrapperRef.current\n if (!wrapper) return\n\n const measure = () => {\n const measured = measureSections(wrapper, {\n layoutMode,\n horizontalOverlap,\n strokeCount,\n strokeWidth,\n })\n if (!measured) return\n\n const data = serpentineBorder({\n width: measured.width,\n sectionBottomYs: measured.sectionBottomYs,\n strokeCount,\n strokeWidth,\n radius,\n horizontalOverlap,\n colors,\n layoutMode,\n })\n setBorderData(data)\n }\n\n measure()\n const ro = new ResizeObserver(measure)\n ro.observe(wrapper)\n return () => ro.disconnect()\n }, [strokeCount, strokeWidth, radius, horizontalOverlap, colors, layoutMode])\n\n return (\n <div\n ref={wrapperRef}\n className=\"serpentine-wrapper\"\n style={borderData?.wrapperStyle ?? { position: 'relative', boxSizing: 'border-box' }}\n data-testid=\"serpentine-wrapper\"\n >\n {borderData && (() => {\n const { class: className, ...restSvgAttrs } = borderData.svgAttributes\n return (\n <svg\n data-testid=\"serpentine-svg\"\n className={className}\n {...restSvgAttrs}\n >\n {borderData.paths.map((pathAttributes, i) => (\n <path key={i} {...pathAttributes} />\n ))}\n </svg>\n )\n })()}\n {children}\n </div>\n )\n}\n\nexport default SerpentineBorder\n"],"names":["DEFAULT_COLORS","resolveOverlapToPixels","horizontalOverlap","N","STROKE_WIDTH","totalBorderWidth","buildPathD","W","Y","R","COLORS","TOP_ARC_SHIFT","Y_OFFSET","O_TOTAL","BORDER_EXTRA","R1","RIGHT_EXTEND","n","parts","i","o","oj","r","rj","rj1","leftOffset","xLeft","rightExt","xRight","xLeftArc","xRightArc","xRightR1","yCurrTop","segs","t","yCurr","yNext","yExit","lastY","DEFAULT_SVG_CLASS","DEFAULTS","serpentineBorder","options","layoutMode","svgClassName","wrapperEl","measured","measureSections","TOTAL_BORDER_WIDTH","TOP_OFFSET","wrapperStyle","svgStyle","paths","totalHeight","totalWidth","viewBoxHeight","viewBoxMinX","viewBoxMinY","viewBoxStr","strokeCount","strokeWidth","excludeClassName","sectionEls","el","rect","baseWidth","SerpentineBorder","children","radius","colors","wrapperRef","useRef","borderData","setBorderData","useState","useEffect","wrapper","measure","data","ro","jsxs","className","restSvgAttrs","jsx","pathAttributes"],"mappings":"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"}
1
+ {"version":3,"file":"serpentine-border.cjs","sources":["../src/constants.js","../src/serpentineCore.js","../src/SerpentineBorder.jsx"],"sourcesContent":["export const DEFAULT_COLORS = ['#ffffff', '#000000']\n","/**\n * Vanilla JS core for serpentine border SVG generation.\n * Single export: call with measured dimensions and options to get everything needed to render.\n */\n\nimport { DEFAULT_COLORS } from './constants.js'\n\nfunction resolveOverlapToPixels(horizontalOverlap, N, STROKE_WIDTH) {\n if (typeof horizontalOverlap === 'number') return horizontalOverlap\n const totalBorderWidth = N * STROKE_WIDTH\n if (horizontalOverlap === 'borderWidth') return totalBorderWidth\n if (horizontalOverlap === 'halfBorderWidth') return totalBorderWidth / 2\n return 0\n}\n\nfunction styleObjectToCss(obj) {\n return Object.entries(obj)\n .map(([k, v]) => {\n const key = k.replace(/([A-Z])/g, '-$1').toLowerCase()\n const val = typeof v === 'number' && !Number.isNaN(v) ? `${v}px` : String(v)\n return `${key}: ${val}`\n })\n .join('; ')\n}\n\nfunction buildPathD(W, Y, N, R, STROKE_WIDTH, COLORS, TOP_ARC_SHIFT, Y_OFFSET, O_TOTAL, BORDER_EXTRA) {\n const R1 = STROKE_WIDTH * (N - 1)\n const RIGHT_EXTEND = STROKE_WIDTH / 2\n const n = Y.length - 1\n const parts = []\n for (let i = 0; i < N; i++) {\n const o = i * STROKE_WIDTH\n const j = N - 1 - i\n const oj = j * STROKE_WIDTH\n const r = R - o\n const rj = R - oj\n const r1 = R1 - o\n const rj1 = R1 - oj\n\n const leftOffset = O_TOTAL - BORDER_EXTRA + RIGHT_EXTEND\n const xLeft = o - O_TOTAL + leftOffset\n const rightExt = BORDER_EXTRA\n const xRight = W + rightExt - oj - RIGHT_EXTEND\n const xLeftArc = xLeft + (R - o)\n const xRightArc = W + rightExt - R - RIGHT_EXTEND\n const xRightR1 = W + rightExt - R1 - RIGHT_EXTEND\n\n const yCurrTop = O_TOTAL + Y_OFFSET\n const segs = [\n `M ${xRight} ${yCurrTop - R1 - STROKE_WIDTH / 2 - TOP_ARC_SHIFT}`,\n `L ${xRight} ${yCurrTop - R1 - TOP_ARC_SHIFT}`,\n `A ${rj1} ${rj1} 0 0 1 ${xRightR1} ${yCurrTop - oj - TOP_ARC_SHIFT}`,\n `L ${xLeftArc} ${o + Y_OFFSET - TOP_ARC_SHIFT}`,\n `A ${r} ${r} 0 0 0 ${xLeft} ${R + Y_OFFSET - TOP_ARC_SHIFT}`,\n `L ${xLeft} ${R + Y_OFFSET}`,\n ]\n\n for (let t = 0; t < n - 1; t++) {\n const yCurr = Y[t + 1]\n const yNext = Y[t + 2]\n const yExit = yCurr + R - O_TOTAL + Y_OFFSET\n\n if (t % 2 === 0) {\n segs.push(`L ${xLeft} ${yCurr - R + Y_OFFSET}`)\n segs.push(`A ${r} ${r} 0 0 0 ${xLeftArc} ${yCurr - o + Y_OFFSET}`)\n segs.push(`L ${xRightArc} ${yCurr - o + Y_OFFSET}`)\n segs.push(`A ${rj} ${rj} 0 0 1 ${xRight} ${yExit}`)\n segs.push(`L ${xRight} ${yNext - R + Y_OFFSET}`)\n } else {\n segs.push(`L ${xRight} ${yCurr - R + Y_OFFSET}`)\n segs.push(`A ${rj} ${rj} 0 0 1 ${xRightArc} ${yCurr - oj + Y_OFFSET}`)\n segs.push(`L ${xLeftArc} ${yCurr - oj + Y_OFFSET}`)\n segs.push(`A ${r} ${r} 0 0 0 ${xLeft} ${yExit}`)\n segs.push(`L ${xLeft} ${yNext - R + Y_OFFSET}`)\n }\n }\n\n const lastY = Y[n]\n if ((n - 2) % 2 === 0) {\n segs.push(`L ${xRight} ${lastY}`)\n } else {\n segs.push(`L ${xLeft} ${lastY}`)\n }\n parts.push({\n d: segs.join(' '),\n stroke: COLORS[i % COLORS.length],\n 'stroke-width': STROKE_WIDTH,\n fill: 'none',\n })\n }\n return parts\n}\n\nconst DEFAULT_SVG_CLASS = 'serpentine-border-svg'\n\nconst DEFAULTS = {\n strokeCount: 5,\n strokeWidth: 8,\n radius: 50,\n horizontalOverlap: 0,\n layoutMode: 'border',\n}\n\n/**\n * Compute everything needed to render the serpentine border.\n * Accepts either (width + sectionBottomYs) for pure/custom use, or wrapperEl to measure from the DOM.\n * When using wrapperEl, returns null in non-DOM environments (e.g. SSR) or when measurement fails.\n *\n * @param {{\n * width?: number\n * sectionBottomYs?: number[]\n * wrapperEl?: HTMLElement\n * strokeCount?: number\n * strokeWidth?: number\n * radius?: number\n * horizontalOverlap?: number | 'borderWidth' | 'halfBorderWidth'\n * colors?: string[]\n * layoutMode?: 'content' | 'border'\n * svgClassName?: string\n * }} options\n * @returns {{\n * wrapperStyle: Record<string, unknown>\n * svgAttributes: { class?: string, viewBox: string, style: string }\n * paths: Array<{ d: string, stroke: string, 'stroke-width': number, fill: string }>\n * } | null}\n */\nexport function serpentineBorder(options) {\n const N = options.strokeCount ?? DEFAULTS.strokeCount\n const STROKE_WIDTH = options.strokeWidth ?? DEFAULTS.strokeWidth\n const R = options.radius ?? DEFAULTS.radius\n const horizontalOverlap = options.horizontalOverlap ?? DEFAULTS.horizontalOverlap\n const COLORS = options.colors ?? DEFAULT_COLORS\n const layoutMode = options.layoutMode ?? DEFAULTS.layoutMode\n const svgClassName = options.svgClassName ?? DEFAULT_SVG_CLASS\n\n let W, Y\n if (options.wrapperEl != null) {\n const wrapperEl = options.wrapperEl\n const hasDOM = typeof document !== 'undefined' && typeof wrapperEl.getBoundingClientRect === 'function'\n if (!hasDOM) return null\n const measured = measureSections(wrapperEl, {\n layoutMode,\n horizontalOverlap,\n strokeCount: N,\n strokeWidth: STROKE_WIDTH,\n excludeClassName: svgClassName,\n })\n if (!measured) return null\n W = measured.width\n Y = measured.sectionBottomYs\n } else {\n if (options.width == null || options.sectionBottomYs == null) return null\n W = options.width\n Y = options.sectionBottomYs\n }\n\n const BORDER_EXTRA = resolveOverlapToPixels(horizontalOverlap, N, STROKE_WIDTH)\n const O_TOTAL = (N - 1) * STROKE_WIDTH\n const TOTAL_BORDER_WIDTH = N * STROKE_WIDTH\n const TOP_OFFSET = 2 * STROKE_WIDTH\n const Y_OFFSET = O_TOTAL / 2\n const TOP_ARC_SHIFT = ((N - 1) / 2) * STROKE_WIDTH + Y_OFFSET\n\n const wrapperStyle =\n layoutMode === 'border'\n ? {\n boxSizing: 'border-box',\n position: 'relative',\n marginTop: `${TOTAL_BORDER_WIDTH / 2}px`,\n ...(BORDER_EXTRA > 0 && {\n paddingLeft: `${BORDER_EXTRA}px`,\n paddingRight: `${BORDER_EXTRA}px`,\n }),\n }\n : {\n position: 'relative',\n boxSizing: 'border-box',\n }\n\n const svgStyleObj =\n layoutMode === 'border'\n ? {\n position: 'absolute',\n overflow: 'hidden',\n width: '100%',\n left: 0,\n top: -(TOP_OFFSET + TOP_ARC_SHIFT),\n height: `calc(100% + ${TOP_OFFSET + TOP_ARC_SHIFT}px)`,\n }\n : {\n position: 'absolute',\n overflow: 'hidden',\n width: `calc(100% + ${2 * BORDER_EXTRA}px)`,\n left: -BORDER_EXTRA,\n top: -(TOP_OFFSET + TOP_ARC_SHIFT),\n height: `calc(100% + ${TOP_OFFSET + TOP_ARC_SHIFT}px)`,\n }\n const svgStyle = styleObjectToCss(svgStyleObj)\n\n const paths = buildPathD(W, Y, N, R, STROKE_WIDTH, COLORS, TOP_ARC_SHIFT, Y_OFFSET, O_TOTAL, BORDER_EXTRA)\n\n const totalHeight = Y[Y.length - 1] ?? 0\n const totalWidth = Math.max(1, W + 2 * BORDER_EXTRA)\n const viewBoxHeight = totalHeight + TOP_OFFSET + TOP_ARC_SHIFT\n const viewBoxMinX = BORDER_EXTRA > 0 ? -BORDER_EXTRA : 0\n const viewBoxMinY = -STROKE_WIDTH * 2 - TOP_ARC_SHIFT\n const viewBoxStr = `${viewBoxMinX} ${viewBoxMinY} ${totalWidth} ${viewBoxHeight}`\n\n return {\n wrapperStyle,\n svgAttributes: {\n class: svgClassName,\n viewBox: viewBoxStr,\n style: svgStyle,\n },\n paths,\n }\n}\n\n/**\n * Measure wrapper and section elements to get width and section bottom Ys.\n * Children with the excludeClassName (default: same class used on the SVG by serpentineBorder) are excluded.\n * horizontalOverlap is resolved to pixels using strokeCount and strokeWidth.\n *\n * @param {HTMLElement} wrapperEl\n * @param {{\n * layoutMode: 'content' | 'border'\n * horizontalOverlap?: number | 'borderWidth' | 'halfBorderWidth'\n * strokeCount: number\n * strokeWidth: number\n * excludeClassName?: string\n * }} options\n * @returns {{ width: number, sectionBottomYs: number[] } | null}\n */\nexport function measureSections(wrapperEl, options) {\n const { layoutMode, horizontalOverlap = 0, strokeCount, strokeWidth, excludeClassName = DEFAULT_SVG_CLASS } = options\n const BORDER_EXTRA = resolveOverlapToPixels(horizontalOverlap, strokeCount, strokeWidth)\n if (!wrapperEl) return null\n\n const sectionEls = excludeClassName\n ? Array.from(wrapperEl.children).filter((el) => !el.classList.contains(excludeClassName))\n : Array.from(wrapperEl.children)\n\n if (sectionEls.length === 0) return null\n\n const rect = wrapperEl.getBoundingClientRect()\n const baseWidth = rect.width\n\n const W =\n layoutMode === 'border'\n ? Math.max(1, baseWidth - 2 * BORDER_EXTRA)\n : Math.max(1, baseWidth)\n\n const Y = [0]\n for (let i = 0; i < sectionEls.length; i++) {\n const r = sectionEls[i].getBoundingClientRect()\n Y.push(r.top - rect.top + r.height)\n }\n\n return { width: W, sectionBottomYs: Y }\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { measureSections, serpentineBorder } from './serpentineCore.js'\n\nfunction cssStringToStyleObject(css) {\n const obj = {}\n if (!css || typeof css !== 'string') return obj\n for (const decl of css.split(';')) {\n const colon = decl.indexOf(':')\n if (colon === -1) continue\n const key = decl.slice(0, colon).trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase())\n const value = decl.slice(colon + 1).trim()\n if (key) obj[key] = value\n }\n return obj\n}\n\nfunction pathAttrsForReact(attrs) {\n return Object.fromEntries(\n Object.entries(attrs).map(([k, v]) => [k === 'stroke-width' ? 'strokeWidth' : k, v])\n )\n}\n\nfunction SerpentineBorder({\n children,\n strokeCount,\n strokeWidth,\n radius,\n horizontalOverlap,\n colors,\n layoutMode,\n}) {\n const wrapperRef = useRef(null)\n const [borderData, setBorderData] = useState(null)\n\n useEffect(() => {\n const wrapper = wrapperRef.current\n if (!wrapper) return\n\n const measure = () => {\n const measured = measureSections(wrapper, {\n layoutMode,\n horizontalOverlap,\n strokeCount,\n strokeWidth,\n })\n if (!measured) return\n\n const data = serpentineBorder({\n width: measured.width,\n sectionBottomYs: measured.sectionBottomYs,\n strokeCount,\n strokeWidth,\n radius,\n horizontalOverlap,\n colors,\n layoutMode,\n })\n setBorderData(data)\n }\n\n measure()\n const ro = new ResizeObserver(measure)\n ro.observe(wrapper)\n return () => ro.disconnect()\n }, [strokeCount, strokeWidth, radius, horizontalOverlap, colors, layoutMode])\n\n return (\n <div\n ref={wrapperRef}\n className=\"serpentine-wrapper\"\n style={borderData?.wrapperStyle ?? { position: 'relative', boxSizing: 'border-box' }}\n data-testid=\"serpentine-wrapper\"\n >\n {borderData && (() => {\n const { class: className, style: styleStr, ...restSvgAttrs } = borderData.svgAttributes\n const style = typeof styleStr === 'string' ? cssStringToStyleObject(styleStr) : styleStr\n return (\n <svg\n data-testid=\"serpentine-svg\"\n className={className}\n style={style}\n {...restSvgAttrs}\n >\n {borderData.paths.map((pathAttributes, i) => (\n <path key={i} {...pathAttrsForReact(pathAttributes)} />\n ))}\n </svg>\n )\n })()}\n {children}\n </div>\n )\n}\n\nexport default SerpentineBorder\n"],"names":["DEFAULT_COLORS","resolveOverlapToPixels","horizontalOverlap","N","STROKE_WIDTH","totalBorderWidth","styleObjectToCss","obj","k","v","key","val","buildPathD","W","Y","R","COLORS","TOP_ARC_SHIFT","Y_OFFSET","O_TOTAL","BORDER_EXTRA","R1","RIGHT_EXTEND","n","parts","i","o","oj","r","rj","rj1","leftOffset","xLeft","rightExt","xRight","xLeftArc","xRightArc","xRightR1","yCurrTop","segs","t","yCurr","yNext","yExit","lastY","DEFAULT_SVG_CLASS","DEFAULTS","serpentineBorder","options","layoutMode","svgClassName","wrapperEl","measured","measureSections","TOTAL_BORDER_WIDTH","TOP_OFFSET","wrapperStyle","svgStyleObj","svgStyle","paths","totalHeight","totalWidth","viewBoxHeight","viewBoxMinX","viewBoxMinY","viewBoxStr","strokeCount","strokeWidth","excludeClassName","sectionEls","el","rect","baseWidth","cssStringToStyleObject","css","decl","colon","_","c","value","pathAttrsForReact","attrs","SerpentineBorder","children","radius","colors","wrapperRef","useRef","borderData","setBorderData","useState","useEffect","wrapper","measure","data","ro","jsxs","className","styleStr","restSvgAttrs","style","jsx","pathAttributes"],"mappings":"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"}
@@ -1,163 +1,186 @@
1
- import { jsxs as J, jsx as X } from "react/jsx-runtime";
2
- import { useRef as P, useState as Q, useEffect as Z } from "react";
3
- const S = ["#ffffff", "#000000"];
4
- function G(t, i, o) {
1
+ import { jsxs as Z, jsx as U } from "react/jsx-runtime";
2
+ import { useRef as q, useState as J, useEffect as P } from "react";
3
+ const Q = ["#ffffff", "#000000"];
4
+ function X(t, s, e) {
5
5
  if (typeof t == "number") return t;
6
- const s = i * o;
7
- return t === "borderWidth" ? s : t === "halfBorderWidth" ? s / 2 : 0;
6
+ const n = s * e;
7
+ return t === "borderWidth" ? n : t === "halfBorderWidth" ? n / 2 : 0;
8
8
  }
9
- function E(t, i, o, s, u, g, l, n, r, h) {
10
- const e = u * (o - 1), a = u / 2, $ = i.length - 1, c = [];
11
- for (let f = 0; f < o; f++) {
12
- const d = f * u, m = (o - 1 - f) * u, v = s - d, B = s - m, j = e - m, k = r - h + a, x = d - r + k, y = h, w = t + y - m - a, L = x + (s - d), N = t + y - s - a, A = t + y - e - a, D = r + n, p = [
13
- `M ${w} ${D - e - u / 2 - l}`,
14
- `L ${w} ${D - e - l}`,
15
- `A ${j} ${j} 0 0 1 ${A} ${D - m - l}`,
16
- `L ${L} ${d + n - l}`,
17
- `A ${v} ${v} 0 0 0 ${x} ${s + n - l}`,
18
- `L ${x} ${s + n}`
9
+ function E(t) {
10
+ return Object.entries(t).map(([s, e]) => {
11
+ const n = s.replace(/([A-Z])/g, "-$1").toLowerCase(), i = typeof e == "number" && !Number.isNaN(e) ? `${e}px` : String(e);
12
+ return `${n}: ${i}`;
13
+ }).join("; ");
14
+ }
15
+ function H(t, s, e, n, i, g, u, o, c, f) {
16
+ const r = i * (e - 1), a = i / 2, p = s.length - 1, l = [];
17
+ for (let h = 0; h < e; h++) {
18
+ const d = h * i, x = (e - 1 - h) * i, b = n - d, B = n - x, M = r - x, N = c - f + a, m = d - c + N, L = f, y = t + L - x - a, j = m + (n - d), A = t + L - n - a, W = t + L - r - a, w = c + o, $ = [
19
+ `M ${y} ${w - r - i / 2 - u}`,
20
+ `L ${y} ${w - r - u}`,
21
+ `A ${M} ${M} 0 0 1 ${W} ${w - x - u}`,
22
+ `L ${j} ${d + o - u}`,
23
+ `A ${b} ${b} 0 0 0 ${m} ${n + o - u}`,
24
+ `L ${m} ${n + o}`
19
25
  ];
20
- for (let C = 0; C < $ - 1; C++) {
21
- const b = i[C + 1], R = i[C + 2], U = b + s - r + n;
22
- C % 2 === 0 ? (p.push(`L ${x} ${b - s + n}`), p.push(`A ${v} ${v} 0 0 0 ${L} ${b - d + n}`), p.push(`L ${N} ${b - d + n}`), p.push(`A ${B} ${B} 0 0 1 ${w} ${U}`), p.push(`L ${w} ${R - s + n}`)) : (p.push(`L ${w} ${b - s + n}`), p.push(`A ${B} ${B} 0 0 1 ${N} ${b - m + n}`), p.push(`L ${L} ${b - m + n}`), p.push(`A ${v} ${v} 0 0 0 ${x} ${U}`), p.push(`L ${x} ${R - s + n}`));
26
+ for (let C = 0; C < p - 1; C++) {
27
+ const v = s[C + 1], R = s[C + 2], S = v + n - c + o;
28
+ C % 2 === 0 ? ($.push(`L ${m} ${v - n + o}`), $.push(`A ${b} ${b} 0 0 0 ${j} ${v - d + o}`), $.push(`L ${A} ${v - d + o}`), $.push(`A ${B} ${B} 0 0 1 ${y} ${S}`), $.push(`L ${y} ${R - n + o}`)) : ($.push(`L ${y} ${v - n + o}`), $.push(`A ${B} ${B} 0 0 1 ${A} ${v - x + o}`), $.push(`L ${j} ${v - x + o}`), $.push(`A ${b} ${b} 0 0 0 ${m} ${S}`), $.push(`L ${m} ${R - n + o}`));
23
29
  }
24
- const z = i[$];
25
- ($ - 2) % 2 === 0 ? p.push(`L ${w} ${z}`) : p.push(`L ${x} ${z}`), c.push({
26
- d: p.join(" "),
27
- stroke: g[f % g.length],
28
- strokeWidth: u,
30
+ const z = s[p];
31
+ (p - 2) % 2 === 0 ? $.push(`L ${y} ${z}`) : $.push(`L ${m} ${z}`), l.push({
32
+ d: $.join(" "),
33
+ stroke: g[h % g.length],
34
+ "stroke-width": i,
29
35
  fill: "none"
30
36
  });
31
37
  }
32
- return c;
38
+ return l;
33
39
  }
34
- const V = "serpentine-border-svg", M = {
40
+ const G = "serpentine-border-svg", k = {
35
41
  strokeCount: 5,
36
42
  strokeWidth: 8,
37
43
  radius: 50,
38
44
  horizontalOverlap: 0,
39
- layoutMode: "content"
45
+ layoutMode: "border"
40
46
  };
41
- function H(t) {
42
- const i = t.strokeCount ?? M.strokeCount, o = t.strokeWidth ?? M.strokeWidth, s = t.radius ?? M.radius, u = t.horizontalOverlap ?? M.horizontalOverlap, g = t.colors ?? S, l = t.layoutMode ?? M.layoutMode, n = t.svgClassName ?? V;
43
- let r, h;
47
+ function O(t) {
48
+ const s = t.strokeCount ?? k.strokeCount, e = t.strokeWidth ?? k.strokeWidth, n = t.radius ?? k.radius, i = t.horizontalOverlap ?? k.horizontalOverlap, g = t.colors ?? Q, u = t.layoutMode ?? k.layoutMode, o = t.svgClassName ?? G;
49
+ let c, f;
44
50
  if (t.wrapperEl != null) {
45
- const L = t.wrapperEl;
46
- if (!(typeof document < "u" && typeof L.getBoundingClientRect == "function")) return null;
47
- const A = q(L, {
48
- layoutMode: l,
49
- horizontalOverlap: u,
50
- strokeCount: i,
51
- strokeWidth: o,
52
- excludeClassName: n
51
+ const A = t.wrapperEl;
52
+ if (!(typeof document < "u" && typeof A.getBoundingClientRect == "function")) return null;
53
+ const w = V(A, {
54
+ layoutMode: u,
55
+ horizontalOverlap: i,
56
+ strokeCount: s,
57
+ strokeWidth: e,
58
+ excludeClassName: o
53
59
  });
54
- if (!A) return null;
55
- r = A.width, h = A.sectionBottomYs;
60
+ if (!w) return null;
61
+ c = w.width, f = w.sectionBottomYs;
56
62
  } else {
57
63
  if (t.width == null || t.sectionBottomYs == null) return null;
58
- r = t.width, h = t.sectionBottomYs;
64
+ c = t.width, f = t.sectionBottomYs;
59
65
  }
60
- const e = G(u, i, o), a = (i - 1) * o, $ = i * o, c = 2 * o, f = a / 2, d = (i - 1) / 2 * o + f, W = l === "border" ? {
66
+ const r = X(i, s, e), a = (s - 1) * e, p = s * e, l = 2 * e, h = a / 2, d = (s - 1) / 2 * e + h, D = u === "border" ? {
61
67
  boxSizing: "border-box",
62
68
  position: "relative",
63
- marginTop: $ / 2,
64
- ...e > 0 && {
65
- paddingLeft: e,
66
- paddingRight: e
69
+ marginTop: `${p / 2}px`,
70
+ ...r > 0 && {
71
+ paddingLeft: `${r}px`,
72
+ paddingRight: `${r}px`
67
73
  }
68
74
  } : {
69
75
  position: "relative",
70
76
  boxSizing: "border-box"
71
- }, m = l === "border" ? {
77
+ }, x = u === "border" ? {
72
78
  position: "absolute",
73
79
  overflow: "hidden",
74
80
  width: "100%",
75
81
  left: 0,
76
- top: -(c + d),
77
- height: `calc(100% + ${c + d}px)`
82
+ top: -(l + d),
83
+ height: `calc(100% + ${l + d}px)`
78
84
  } : {
79
85
  position: "absolute",
80
86
  overflow: "hidden",
81
- width: `calc(100% + ${2 * e}px)`,
82
- left: -e,
83
- top: -(c + d),
84
- height: `calc(100% + ${c + d}px)`
85
- }, v = E(r, h, i, s, o, g, d, f, a, e), B = h[h.length - 1] ?? 0, j = Math.max(1, r + 2 * e), k = B + c + d, x = e > 0 ? -e : 0, y = -o * 2 - d, w = `${x} ${y} ${j} ${k}`;
87
+ width: `calc(100% + ${2 * r}px)`,
88
+ left: -r,
89
+ top: -(l + d),
90
+ height: `calc(100% + ${l + d}px)`
91
+ }, b = E(x), B = H(c, f, s, n, e, g, d, h, a, r), M = f[f.length - 1] ?? 0, N = Math.max(1, c + 2 * r), m = M + l + d, L = r > 0 ? -r : 0, y = -e * 2 - d, j = `${L} ${y} ${N} ${m}`;
86
92
  return {
87
- wrapperStyle: W,
93
+ wrapperStyle: D,
88
94
  svgAttributes: {
89
- class: n,
90
- viewBox: w,
91
- style: m
95
+ class: o,
96
+ viewBox: j,
97
+ style: b
92
98
  },
93
- paths: v
99
+ paths: B
94
100
  };
95
101
  }
96
- function q(t, i) {
97
- const { layoutMode: o, horizontalOverlap: s = 0, strokeCount: u, strokeWidth: g, excludeClassName: l = V } = i, n = G(s, u, g);
102
+ function V(t, s) {
103
+ const { layoutMode: e, horizontalOverlap: n = 0, strokeCount: i, strokeWidth: g, excludeClassName: u = G } = s, o = X(n, i, g);
98
104
  if (!t) return null;
99
- const r = l ? Array.from(t.children).filter((c) => !c.classList.contains(l)) : Array.from(t.children);
100
- if (r.length === 0) return null;
101
- const h = t.getBoundingClientRect(), e = h.width, a = o === "border" ? Math.max(1, e - 2 * n) : Math.max(1, e), $ = [0];
102
- for (let c = 0; c < r.length; c++) {
103
- const f = r[c].getBoundingClientRect();
104
- $.push(f.top - h.top + f.height);
105
+ const c = u ? Array.from(t.children).filter((l) => !l.classList.contains(u)) : Array.from(t.children);
106
+ if (c.length === 0) return null;
107
+ const f = t.getBoundingClientRect(), r = f.width, a = e === "border" ? Math.max(1, r - 2 * o) : Math.max(1, r), p = [0];
108
+ for (let l = 0; l < c.length; l++) {
109
+ const h = c[l].getBoundingClientRect();
110
+ p.push(h.top - f.top + h.height);
111
+ }
112
+ return { width: a, sectionBottomYs: p };
113
+ }
114
+ function K(t) {
115
+ const s = {};
116
+ if (!t || typeof t != "string") return s;
117
+ for (const e of t.split(";")) {
118
+ const n = e.indexOf(":");
119
+ if (n === -1) continue;
120
+ const i = e.slice(0, n).trim().replace(/-([a-z])/g, (u, o) => o.toUpperCase()), g = e.slice(n + 1).trim();
121
+ i && (s[i] = g);
105
122
  }
106
- return { width: a, sectionBottomYs: $ };
123
+ return s;
124
+ }
125
+ function Y(t) {
126
+ return Object.fromEntries(
127
+ Object.entries(t).map(([s, e]) => [s === "stroke-width" ? "strokeWidth" : s, e])
128
+ );
107
129
  }
108
- function I({
130
+ function _({
109
131
  children: t,
110
- strokeCount: i,
111
- strokeWidth: o,
112
- radius: s,
113
- horizontalOverlap: u,
132
+ strokeCount: s,
133
+ strokeWidth: e,
134
+ radius: n,
135
+ horizontalOverlap: i,
114
136
  colors: g,
115
- layoutMode: l
137
+ layoutMode: u
116
138
  }) {
117
- const n = P(null), [r, h] = Q(null);
118
- return Z(() => {
119
- const e = n.current;
120
- if (!e) return;
139
+ const o = q(null), [c, f] = J(null);
140
+ return P(() => {
141
+ const r = o.current;
142
+ if (!r) return;
121
143
  const a = () => {
122
- const c = q(e, {
123
- layoutMode: l,
124
- horizontalOverlap: u,
125
- strokeCount: i,
126
- strokeWidth: o
144
+ const l = V(r, {
145
+ layoutMode: u,
146
+ horizontalOverlap: i,
147
+ strokeCount: s,
148
+ strokeWidth: e
127
149
  });
128
- if (!c) return;
129
- const f = H({
130
- width: c.width,
131
- sectionBottomYs: c.sectionBottomYs,
132
- strokeCount: i,
133
- strokeWidth: o,
134
- radius: s,
135
- horizontalOverlap: u,
150
+ if (!l) return;
151
+ const h = O({
152
+ width: l.width,
153
+ sectionBottomYs: l.sectionBottomYs,
154
+ strokeCount: s,
155
+ strokeWidth: e,
156
+ radius: n,
157
+ horizontalOverlap: i,
136
158
  colors: g,
137
- layoutMode: l
159
+ layoutMode: u
138
160
  });
139
- h(f);
161
+ f(h);
140
162
  };
141
163
  a();
142
- const $ = new ResizeObserver(a);
143
- return $.observe(e), () => $.disconnect();
144
- }, [i, o, s, u, g, l]), /* @__PURE__ */ J(
164
+ const p = new ResizeObserver(a);
165
+ return p.observe(r), () => p.disconnect();
166
+ }, [s, e, n, i, g, u]), /* @__PURE__ */ Z(
145
167
  "div",
146
168
  {
147
- ref: n,
169
+ ref: o,
148
170
  className: "serpentine-wrapper",
149
- style: (r == null ? void 0 : r.wrapperStyle) ?? { position: "relative", boxSizing: "border-box" },
171
+ style: (c == null ? void 0 : c.wrapperStyle) ?? { position: "relative", boxSizing: "border-box" },
150
172
  "data-testid": "serpentine-wrapper",
151
173
  children: [
152
- r && (() => {
153
- const { class: e, ...a } = r.svgAttributes;
154
- return /* @__PURE__ */ X(
174
+ c && (() => {
175
+ const { class: r, style: a, ...p } = c.svgAttributes, l = typeof a == "string" ? K(a) : a;
176
+ return /* @__PURE__ */ U(
155
177
  "svg",
156
178
  {
157
179
  "data-testid": "serpentine-svg",
158
- className: e,
159
- ...a,
160
- children: r.paths.map(($, c) => /* @__PURE__ */ X("path", { ...$ }, c))
180
+ className: r,
181
+ style: l,
182
+ ...p,
183
+ children: c.paths.map((h, d) => /* @__PURE__ */ U("path", { ...Y(h) }, d))
161
184
  }
162
185
  );
163
186
  })(),
@@ -167,8 +190,7 @@ function I({
167
190
  );
168
191
  }
169
192
  export {
170
- I as SerpentineBorder,
171
- q as measureSections,
172
- H as serpentineBorder
193
+ _ as SerpentineBorder,
194
+ O as serpentineBorder
173
195
  };
174
196
  //# sourceMappingURL=serpentine-border.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"serpentine-border.js","sources":["../src/constants.js","../src/serpentineCore.js","../src/SerpentineBorder.jsx"],"sourcesContent":["export const DEFAULT_COLORS = ['#ffffff', '#000000']\n","/**\n * Vanilla JS core for serpentine border SVG generation.\n * Single export: call with measured dimensions and options to get everything needed to render.\n */\n\nimport { DEFAULT_COLORS } from './constants.js'\n\nfunction resolveOverlapToPixels(horizontalOverlap, N, STROKE_WIDTH) {\n if (typeof horizontalOverlap === 'number') return horizontalOverlap\n const totalBorderWidth = N * STROKE_WIDTH\n if (horizontalOverlap === 'borderWidth') return totalBorderWidth\n if (horizontalOverlap === 'halfBorderWidth') return totalBorderWidth / 2\n return 0\n}\n\nfunction buildPathD(W, Y, N, R, STROKE_WIDTH, COLORS, TOP_ARC_SHIFT, Y_OFFSET, O_TOTAL, BORDER_EXTRA) {\n const R1 = STROKE_WIDTH * (N - 1)\n const RIGHT_EXTEND = STROKE_WIDTH / 2\n const n = Y.length - 1\n const parts = []\n for (let i = 0; i < N; i++) {\n const o = i * STROKE_WIDTH\n const j = N - 1 - i\n const oj = j * STROKE_WIDTH\n const r = R - o\n const rj = R - oj\n const r1 = R1 - o\n const rj1 = R1 - oj\n\n const leftOffset = O_TOTAL - BORDER_EXTRA + RIGHT_EXTEND\n const xLeft = o - O_TOTAL + leftOffset\n const rightExt = BORDER_EXTRA\n const xRight = W + rightExt - oj - RIGHT_EXTEND\n const xLeftArc = xLeft + (R - o)\n const xRightArc = W + rightExt - R - RIGHT_EXTEND\n const xRightR1 = W + rightExt - R1 - RIGHT_EXTEND\n\n const yCurrTop = O_TOTAL + Y_OFFSET\n const segs = [\n `M ${xRight} ${yCurrTop - R1 - STROKE_WIDTH / 2 - TOP_ARC_SHIFT}`,\n `L ${xRight} ${yCurrTop - R1 - TOP_ARC_SHIFT}`,\n `A ${rj1} ${rj1} 0 0 1 ${xRightR1} ${yCurrTop - oj - TOP_ARC_SHIFT}`,\n `L ${xLeftArc} ${o + Y_OFFSET - TOP_ARC_SHIFT}`,\n `A ${r} ${r} 0 0 0 ${xLeft} ${R + Y_OFFSET - TOP_ARC_SHIFT}`,\n `L ${xLeft} ${R + Y_OFFSET}`,\n ]\n\n for (let t = 0; t < n - 1; t++) {\n const yCurr = Y[t + 1]\n const yNext = Y[t + 2]\n const yExit = yCurr + R - O_TOTAL + Y_OFFSET\n\n if (t % 2 === 0) {\n segs.push(`L ${xLeft} ${yCurr - R + Y_OFFSET}`)\n segs.push(`A ${r} ${r} 0 0 0 ${xLeftArc} ${yCurr - o + Y_OFFSET}`)\n segs.push(`L ${xRightArc} ${yCurr - o + Y_OFFSET}`)\n segs.push(`A ${rj} ${rj} 0 0 1 ${xRight} ${yExit}`)\n segs.push(`L ${xRight} ${yNext - R + Y_OFFSET}`)\n } else {\n segs.push(`L ${xRight} ${yCurr - R + Y_OFFSET}`)\n segs.push(`A ${rj} ${rj} 0 0 1 ${xRightArc} ${yCurr - oj + Y_OFFSET}`)\n segs.push(`L ${xLeftArc} ${yCurr - oj + Y_OFFSET}`)\n segs.push(`A ${r} ${r} 0 0 0 ${xLeft} ${yExit}`)\n segs.push(`L ${xLeft} ${yNext - R + Y_OFFSET}`)\n }\n }\n\n const lastY = Y[n]\n if ((n - 2) % 2 === 0) {\n segs.push(`L ${xRight} ${lastY}`)\n } else {\n segs.push(`L ${xLeft} ${lastY}`)\n }\n parts.push({\n d: segs.join(' '),\n stroke: COLORS[i % COLORS.length],\n strokeWidth: STROKE_WIDTH,\n fill: 'none',\n })\n }\n return parts\n}\n\nconst DEFAULT_SVG_CLASS = 'serpentine-border-svg'\n\nconst DEFAULTS = {\n strokeCount: 5,\n strokeWidth: 8,\n radius: 50,\n horizontalOverlap: 0,\n layoutMode: 'content',\n}\n\n/**\n * Compute everything needed to render the serpentine border.\n * Accepts either (width + sectionBottomYs) for pure/custom use, or wrapperEl to measure from the DOM.\n * When using wrapperEl, returns null in non-DOM environments (e.g. SSR) or when measurement fails.\n *\n * @param {{\n * width?: number\n * sectionBottomYs?: number[]\n * wrapperEl?: HTMLElement\n * strokeCount?: number\n * strokeWidth?: number\n * radius?: number\n * horizontalOverlap?: number | 'borderWidth' | 'halfBorderWidth'\n * colors?: string[]\n * layoutMode?: 'content' | 'border'\n * svgClassName?: string\n * }} options\n * @returns {{\n * wrapperStyle: Record<string, unknown>\n * svgAttributes: { class?: string, viewBox: string, style: Record<string, unknown> }\n * paths: Array<{ d: string, stroke: string, strokeWidth: number, fill: string }>\n * } | null}\n */\nexport function serpentineBorder(options) {\n const N = options.strokeCount ?? DEFAULTS.strokeCount\n const STROKE_WIDTH = options.strokeWidth ?? DEFAULTS.strokeWidth\n const R = options.radius ?? DEFAULTS.radius\n const horizontalOverlap = options.horizontalOverlap ?? DEFAULTS.horizontalOverlap\n const COLORS = options.colors ?? DEFAULT_COLORS\n const layoutMode = options.layoutMode ?? DEFAULTS.layoutMode\n const svgClassName = options.svgClassName ?? DEFAULT_SVG_CLASS\n\n let W, Y\n if (options.wrapperEl != null) {\n const wrapperEl = options.wrapperEl\n const hasDOM = typeof document !== 'undefined' && typeof wrapperEl.getBoundingClientRect === 'function'\n if (!hasDOM) return null\n const measured = measureSections(wrapperEl, {\n layoutMode,\n horizontalOverlap,\n strokeCount: N,\n strokeWidth: STROKE_WIDTH,\n excludeClassName: svgClassName,\n })\n if (!measured) return null\n W = measured.width\n Y = measured.sectionBottomYs\n } else {\n if (options.width == null || options.sectionBottomYs == null) return null\n W = options.width\n Y = options.sectionBottomYs\n }\n\n const BORDER_EXTRA = resolveOverlapToPixels(horizontalOverlap, N, STROKE_WIDTH)\n const O_TOTAL = (N - 1) * STROKE_WIDTH\n const TOTAL_BORDER_WIDTH = N * STROKE_WIDTH\n const TOP_OFFSET = 2 * STROKE_WIDTH\n const Y_OFFSET = O_TOTAL / 2\n const TOP_ARC_SHIFT = ((N - 1) / 2) * STROKE_WIDTH + Y_OFFSET\n\n const wrapperStyle =\n layoutMode === 'border'\n ? {\n boxSizing: 'border-box',\n position: 'relative',\n marginTop: TOTAL_BORDER_WIDTH / 2,\n ...(BORDER_EXTRA > 0 && {\n paddingLeft: BORDER_EXTRA,\n paddingRight: BORDER_EXTRA,\n }),\n }\n : {\n position: 'relative',\n boxSizing: 'border-box',\n }\n\n const svgStyle =\n layoutMode === 'border'\n ? {\n position: 'absolute',\n overflow: 'hidden',\n width: '100%',\n left: 0,\n top: -(TOP_OFFSET + TOP_ARC_SHIFT),\n height: `calc(100% + ${TOP_OFFSET + TOP_ARC_SHIFT}px)`,\n }\n : {\n position: 'absolute',\n overflow: 'hidden',\n width: `calc(100% + ${2 * BORDER_EXTRA}px)`,\n left: -BORDER_EXTRA,\n top: -(TOP_OFFSET + TOP_ARC_SHIFT),\n height: `calc(100% + ${TOP_OFFSET + TOP_ARC_SHIFT}px)`,\n }\n\n const paths = buildPathD(W, Y, N, R, STROKE_WIDTH, COLORS, TOP_ARC_SHIFT, Y_OFFSET, O_TOTAL, BORDER_EXTRA)\n\n const totalHeight = Y[Y.length - 1] ?? 0\n const totalWidth = Math.max(1, W + 2 * BORDER_EXTRA)\n const viewBoxHeight = totalHeight + TOP_OFFSET + TOP_ARC_SHIFT\n const viewBoxMinX = BORDER_EXTRA > 0 ? -BORDER_EXTRA : 0\n const viewBoxMinY = -STROKE_WIDTH * 2 - TOP_ARC_SHIFT\n const viewBoxStr = `${viewBoxMinX} ${viewBoxMinY} ${totalWidth} ${viewBoxHeight}`\n\n return {\n wrapperStyle,\n svgAttributes: {\n class: svgClassName,\n viewBox: viewBoxStr,\n style: svgStyle,\n },\n paths,\n }\n}\n\n/**\n * Measure wrapper and section elements to get width and section bottom Ys.\n * Children with the excludeClassName (default: same class used on the SVG by serpentineBorder) are excluded.\n * horizontalOverlap is resolved to pixels using strokeCount and strokeWidth.\n *\n * @param {HTMLElement} wrapperEl\n * @param {{\n * layoutMode: 'content' | 'border'\n * horizontalOverlap?: number | 'borderWidth' | 'halfBorderWidth'\n * strokeCount: number\n * strokeWidth: number\n * excludeClassName?: string\n * }} options\n * @returns {{ width: number, sectionBottomYs: number[] } | null}\n */\nexport function measureSections(wrapperEl, options) {\n const { layoutMode, horizontalOverlap = 0, strokeCount, strokeWidth, excludeClassName = DEFAULT_SVG_CLASS } = options\n const BORDER_EXTRA = resolveOverlapToPixels(horizontalOverlap, strokeCount, strokeWidth)\n if (!wrapperEl) return null\n\n const sectionEls = excludeClassName\n ? Array.from(wrapperEl.children).filter((el) => !el.classList.contains(excludeClassName))\n : Array.from(wrapperEl.children)\n\n if (sectionEls.length === 0) return null\n\n const rect = wrapperEl.getBoundingClientRect()\n const baseWidth = rect.width\n\n const W =\n layoutMode === 'border'\n ? Math.max(1, baseWidth - 2 * BORDER_EXTRA)\n : Math.max(1, baseWidth)\n\n const Y = [0]\n for (let i = 0; i < sectionEls.length; i++) {\n const r = sectionEls[i].getBoundingClientRect()\n Y.push(r.top - rect.top + r.height)\n }\n\n return { width: W, sectionBottomYs: Y }\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { measureSections, serpentineBorder } from './serpentineCore.js'\n\nfunction SerpentineBorder({\n children,\n strokeCount,\n strokeWidth,\n radius,\n horizontalOverlap,\n colors,\n layoutMode,\n}) {\n const wrapperRef = useRef(null)\n const [borderData, setBorderData] = useState(null)\n\n useEffect(() => {\n const wrapper = wrapperRef.current\n if (!wrapper) return\n\n const measure = () => {\n const measured = measureSections(wrapper, {\n layoutMode,\n horizontalOverlap,\n strokeCount,\n strokeWidth,\n })\n if (!measured) return\n\n const data = serpentineBorder({\n width: measured.width,\n sectionBottomYs: measured.sectionBottomYs,\n strokeCount,\n strokeWidth,\n radius,\n horizontalOverlap,\n colors,\n layoutMode,\n })\n setBorderData(data)\n }\n\n measure()\n const ro = new ResizeObserver(measure)\n ro.observe(wrapper)\n return () => ro.disconnect()\n }, [strokeCount, strokeWidth, radius, horizontalOverlap, colors, layoutMode])\n\n return (\n <div\n ref={wrapperRef}\n className=\"serpentine-wrapper\"\n style={borderData?.wrapperStyle ?? { position: 'relative', boxSizing: 'border-box' }}\n data-testid=\"serpentine-wrapper\"\n >\n {borderData && (() => {\n const { class: className, ...restSvgAttrs } = borderData.svgAttributes\n return (\n <svg\n data-testid=\"serpentine-svg\"\n className={className}\n {...restSvgAttrs}\n >\n {borderData.paths.map((pathAttributes, i) => (\n <path key={i} {...pathAttributes} />\n ))}\n </svg>\n )\n })()}\n {children}\n </div>\n )\n}\n\nexport default SerpentineBorder\n"],"names":["DEFAULT_COLORS","resolveOverlapToPixels","horizontalOverlap","N","STROKE_WIDTH","totalBorderWidth","buildPathD","W","Y","R","COLORS","TOP_ARC_SHIFT","Y_OFFSET","O_TOTAL","BORDER_EXTRA","R1","RIGHT_EXTEND","n","parts","i","o","oj","r","rj","rj1","leftOffset","xLeft","rightExt","xRight","xLeftArc","xRightArc","xRightR1","yCurrTop","segs","t","yCurr","yNext","yExit","lastY","DEFAULT_SVG_CLASS","DEFAULTS","serpentineBorder","options","layoutMode","svgClassName","wrapperEl","measured","measureSections","TOTAL_BORDER_WIDTH","TOP_OFFSET","wrapperStyle","svgStyle","paths","totalHeight","totalWidth","viewBoxHeight","viewBoxMinX","viewBoxMinY","viewBoxStr","strokeCount","strokeWidth","excludeClassName","sectionEls","el","rect","baseWidth","SerpentineBorder","children","radius","colors","wrapperRef","useRef","borderData","setBorderData","useState","useEffect","wrapper","measure","data","ro","jsxs","className","restSvgAttrs","jsx","pathAttributes"],"mappings":"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"}
1
+ {"version":3,"file":"serpentine-border.js","sources":["../src/constants.js","../src/serpentineCore.js","../src/SerpentineBorder.jsx"],"sourcesContent":["export const DEFAULT_COLORS = ['#ffffff', '#000000']\n","/**\n * Vanilla JS core for serpentine border SVG generation.\n * Single export: call with measured dimensions and options to get everything needed to render.\n */\n\nimport { DEFAULT_COLORS } from './constants.js'\n\nfunction resolveOverlapToPixels(horizontalOverlap, N, STROKE_WIDTH) {\n if (typeof horizontalOverlap === 'number') return horizontalOverlap\n const totalBorderWidth = N * STROKE_WIDTH\n if (horizontalOverlap === 'borderWidth') return totalBorderWidth\n if (horizontalOverlap === 'halfBorderWidth') return totalBorderWidth / 2\n return 0\n}\n\nfunction styleObjectToCss(obj) {\n return Object.entries(obj)\n .map(([k, v]) => {\n const key = k.replace(/([A-Z])/g, '-$1').toLowerCase()\n const val = typeof v === 'number' && !Number.isNaN(v) ? `${v}px` : String(v)\n return `${key}: ${val}`\n })\n .join('; ')\n}\n\nfunction buildPathD(W, Y, N, R, STROKE_WIDTH, COLORS, TOP_ARC_SHIFT, Y_OFFSET, O_TOTAL, BORDER_EXTRA) {\n const R1 = STROKE_WIDTH * (N - 1)\n const RIGHT_EXTEND = STROKE_WIDTH / 2\n const n = Y.length - 1\n const parts = []\n for (let i = 0; i < N; i++) {\n const o = i * STROKE_WIDTH\n const j = N - 1 - i\n const oj = j * STROKE_WIDTH\n const r = R - o\n const rj = R - oj\n const r1 = R1 - o\n const rj1 = R1 - oj\n\n const leftOffset = O_TOTAL - BORDER_EXTRA + RIGHT_EXTEND\n const xLeft = o - O_TOTAL + leftOffset\n const rightExt = BORDER_EXTRA\n const xRight = W + rightExt - oj - RIGHT_EXTEND\n const xLeftArc = xLeft + (R - o)\n const xRightArc = W + rightExt - R - RIGHT_EXTEND\n const xRightR1 = W + rightExt - R1 - RIGHT_EXTEND\n\n const yCurrTop = O_TOTAL + Y_OFFSET\n const segs = [\n `M ${xRight} ${yCurrTop - R1 - STROKE_WIDTH / 2 - TOP_ARC_SHIFT}`,\n `L ${xRight} ${yCurrTop - R1 - TOP_ARC_SHIFT}`,\n `A ${rj1} ${rj1} 0 0 1 ${xRightR1} ${yCurrTop - oj - TOP_ARC_SHIFT}`,\n `L ${xLeftArc} ${o + Y_OFFSET - TOP_ARC_SHIFT}`,\n `A ${r} ${r} 0 0 0 ${xLeft} ${R + Y_OFFSET - TOP_ARC_SHIFT}`,\n `L ${xLeft} ${R + Y_OFFSET}`,\n ]\n\n for (let t = 0; t < n - 1; t++) {\n const yCurr = Y[t + 1]\n const yNext = Y[t + 2]\n const yExit = yCurr + R - O_TOTAL + Y_OFFSET\n\n if (t % 2 === 0) {\n segs.push(`L ${xLeft} ${yCurr - R + Y_OFFSET}`)\n segs.push(`A ${r} ${r} 0 0 0 ${xLeftArc} ${yCurr - o + Y_OFFSET}`)\n segs.push(`L ${xRightArc} ${yCurr - o + Y_OFFSET}`)\n segs.push(`A ${rj} ${rj} 0 0 1 ${xRight} ${yExit}`)\n segs.push(`L ${xRight} ${yNext - R + Y_OFFSET}`)\n } else {\n segs.push(`L ${xRight} ${yCurr - R + Y_OFFSET}`)\n segs.push(`A ${rj} ${rj} 0 0 1 ${xRightArc} ${yCurr - oj + Y_OFFSET}`)\n segs.push(`L ${xLeftArc} ${yCurr - oj + Y_OFFSET}`)\n segs.push(`A ${r} ${r} 0 0 0 ${xLeft} ${yExit}`)\n segs.push(`L ${xLeft} ${yNext - R + Y_OFFSET}`)\n }\n }\n\n const lastY = Y[n]\n if ((n - 2) % 2 === 0) {\n segs.push(`L ${xRight} ${lastY}`)\n } else {\n segs.push(`L ${xLeft} ${lastY}`)\n }\n parts.push({\n d: segs.join(' '),\n stroke: COLORS[i % COLORS.length],\n 'stroke-width': STROKE_WIDTH,\n fill: 'none',\n })\n }\n return parts\n}\n\nconst DEFAULT_SVG_CLASS = 'serpentine-border-svg'\n\nconst DEFAULTS = {\n strokeCount: 5,\n strokeWidth: 8,\n radius: 50,\n horizontalOverlap: 0,\n layoutMode: 'border',\n}\n\n/**\n * Compute everything needed to render the serpentine border.\n * Accepts either (width + sectionBottomYs) for pure/custom use, or wrapperEl to measure from the DOM.\n * When using wrapperEl, returns null in non-DOM environments (e.g. SSR) or when measurement fails.\n *\n * @param {{\n * width?: number\n * sectionBottomYs?: number[]\n * wrapperEl?: HTMLElement\n * strokeCount?: number\n * strokeWidth?: number\n * radius?: number\n * horizontalOverlap?: number | 'borderWidth' | 'halfBorderWidth'\n * colors?: string[]\n * layoutMode?: 'content' | 'border'\n * svgClassName?: string\n * }} options\n * @returns {{\n * wrapperStyle: Record<string, unknown>\n * svgAttributes: { class?: string, viewBox: string, style: string }\n * paths: Array<{ d: string, stroke: string, 'stroke-width': number, fill: string }>\n * } | null}\n */\nexport function serpentineBorder(options) {\n const N = options.strokeCount ?? DEFAULTS.strokeCount\n const STROKE_WIDTH = options.strokeWidth ?? DEFAULTS.strokeWidth\n const R = options.radius ?? DEFAULTS.radius\n const horizontalOverlap = options.horizontalOverlap ?? DEFAULTS.horizontalOverlap\n const COLORS = options.colors ?? DEFAULT_COLORS\n const layoutMode = options.layoutMode ?? DEFAULTS.layoutMode\n const svgClassName = options.svgClassName ?? DEFAULT_SVG_CLASS\n\n let W, Y\n if (options.wrapperEl != null) {\n const wrapperEl = options.wrapperEl\n const hasDOM = typeof document !== 'undefined' && typeof wrapperEl.getBoundingClientRect === 'function'\n if (!hasDOM) return null\n const measured = measureSections(wrapperEl, {\n layoutMode,\n horizontalOverlap,\n strokeCount: N,\n strokeWidth: STROKE_WIDTH,\n excludeClassName: svgClassName,\n })\n if (!measured) return null\n W = measured.width\n Y = measured.sectionBottomYs\n } else {\n if (options.width == null || options.sectionBottomYs == null) return null\n W = options.width\n Y = options.sectionBottomYs\n }\n\n const BORDER_EXTRA = resolveOverlapToPixels(horizontalOverlap, N, STROKE_WIDTH)\n const O_TOTAL = (N - 1) * STROKE_WIDTH\n const TOTAL_BORDER_WIDTH = N * STROKE_WIDTH\n const TOP_OFFSET = 2 * STROKE_WIDTH\n const Y_OFFSET = O_TOTAL / 2\n const TOP_ARC_SHIFT = ((N - 1) / 2) * STROKE_WIDTH + Y_OFFSET\n\n const wrapperStyle =\n layoutMode === 'border'\n ? {\n boxSizing: 'border-box',\n position: 'relative',\n marginTop: `${TOTAL_BORDER_WIDTH / 2}px`,\n ...(BORDER_EXTRA > 0 && {\n paddingLeft: `${BORDER_EXTRA}px`,\n paddingRight: `${BORDER_EXTRA}px`,\n }),\n }\n : {\n position: 'relative',\n boxSizing: 'border-box',\n }\n\n const svgStyleObj =\n layoutMode === 'border'\n ? {\n position: 'absolute',\n overflow: 'hidden',\n width: '100%',\n left: 0,\n top: -(TOP_OFFSET + TOP_ARC_SHIFT),\n height: `calc(100% + ${TOP_OFFSET + TOP_ARC_SHIFT}px)`,\n }\n : {\n position: 'absolute',\n overflow: 'hidden',\n width: `calc(100% + ${2 * BORDER_EXTRA}px)`,\n left: -BORDER_EXTRA,\n top: -(TOP_OFFSET + TOP_ARC_SHIFT),\n height: `calc(100% + ${TOP_OFFSET + TOP_ARC_SHIFT}px)`,\n }\n const svgStyle = styleObjectToCss(svgStyleObj)\n\n const paths = buildPathD(W, Y, N, R, STROKE_WIDTH, COLORS, TOP_ARC_SHIFT, Y_OFFSET, O_TOTAL, BORDER_EXTRA)\n\n const totalHeight = Y[Y.length - 1] ?? 0\n const totalWidth = Math.max(1, W + 2 * BORDER_EXTRA)\n const viewBoxHeight = totalHeight + TOP_OFFSET + TOP_ARC_SHIFT\n const viewBoxMinX = BORDER_EXTRA > 0 ? -BORDER_EXTRA : 0\n const viewBoxMinY = -STROKE_WIDTH * 2 - TOP_ARC_SHIFT\n const viewBoxStr = `${viewBoxMinX} ${viewBoxMinY} ${totalWidth} ${viewBoxHeight}`\n\n return {\n wrapperStyle,\n svgAttributes: {\n class: svgClassName,\n viewBox: viewBoxStr,\n style: svgStyle,\n },\n paths,\n }\n}\n\n/**\n * Measure wrapper and section elements to get width and section bottom Ys.\n * Children with the excludeClassName (default: same class used on the SVG by serpentineBorder) are excluded.\n * horizontalOverlap is resolved to pixels using strokeCount and strokeWidth.\n *\n * @param {HTMLElement} wrapperEl\n * @param {{\n * layoutMode: 'content' | 'border'\n * horizontalOverlap?: number | 'borderWidth' | 'halfBorderWidth'\n * strokeCount: number\n * strokeWidth: number\n * excludeClassName?: string\n * }} options\n * @returns {{ width: number, sectionBottomYs: number[] } | null}\n */\nexport function measureSections(wrapperEl, options) {\n const { layoutMode, horizontalOverlap = 0, strokeCount, strokeWidth, excludeClassName = DEFAULT_SVG_CLASS } = options\n const BORDER_EXTRA = resolveOverlapToPixels(horizontalOverlap, strokeCount, strokeWidth)\n if (!wrapperEl) return null\n\n const sectionEls = excludeClassName\n ? Array.from(wrapperEl.children).filter((el) => !el.classList.contains(excludeClassName))\n : Array.from(wrapperEl.children)\n\n if (sectionEls.length === 0) return null\n\n const rect = wrapperEl.getBoundingClientRect()\n const baseWidth = rect.width\n\n const W =\n layoutMode === 'border'\n ? Math.max(1, baseWidth - 2 * BORDER_EXTRA)\n : Math.max(1, baseWidth)\n\n const Y = [0]\n for (let i = 0; i < sectionEls.length; i++) {\n const r = sectionEls[i].getBoundingClientRect()\n Y.push(r.top - rect.top + r.height)\n }\n\n return { width: W, sectionBottomYs: Y }\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { measureSections, serpentineBorder } from './serpentineCore.js'\n\nfunction cssStringToStyleObject(css) {\n const obj = {}\n if (!css || typeof css !== 'string') return obj\n for (const decl of css.split(';')) {\n const colon = decl.indexOf(':')\n if (colon === -1) continue\n const key = decl.slice(0, colon).trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase())\n const value = decl.slice(colon + 1).trim()\n if (key) obj[key] = value\n }\n return obj\n}\n\nfunction pathAttrsForReact(attrs) {\n return Object.fromEntries(\n Object.entries(attrs).map(([k, v]) => [k === 'stroke-width' ? 'strokeWidth' : k, v])\n )\n}\n\nfunction SerpentineBorder({\n children,\n strokeCount,\n strokeWidth,\n radius,\n horizontalOverlap,\n colors,\n layoutMode,\n}) {\n const wrapperRef = useRef(null)\n const [borderData, setBorderData] = useState(null)\n\n useEffect(() => {\n const wrapper = wrapperRef.current\n if (!wrapper) return\n\n const measure = () => {\n const measured = measureSections(wrapper, {\n layoutMode,\n horizontalOverlap,\n strokeCount,\n strokeWidth,\n })\n if (!measured) return\n\n const data = serpentineBorder({\n width: measured.width,\n sectionBottomYs: measured.sectionBottomYs,\n strokeCount,\n strokeWidth,\n radius,\n horizontalOverlap,\n colors,\n layoutMode,\n })\n setBorderData(data)\n }\n\n measure()\n const ro = new ResizeObserver(measure)\n ro.observe(wrapper)\n return () => ro.disconnect()\n }, [strokeCount, strokeWidth, radius, horizontalOverlap, colors, layoutMode])\n\n return (\n <div\n ref={wrapperRef}\n className=\"serpentine-wrapper\"\n style={borderData?.wrapperStyle ?? { position: 'relative', boxSizing: 'border-box' }}\n data-testid=\"serpentine-wrapper\"\n >\n {borderData && (() => {\n const { class: className, style: styleStr, ...restSvgAttrs } = borderData.svgAttributes\n const style = typeof styleStr === 'string' ? cssStringToStyleObject(styleStr) : styleStr\n return (\n <svg\n data-testid=\"serpentine-svg\"\n className={className}\n style={style}\n {...restSvgAttrs}\n >\n {borderData.paths.map((pathAttributes, i) => (\n <path key={i} {...pathAttrsForReact(pathAttributes)} />\n ))}\n </svg>\n )\n })()}\n {children}\n </div>\n )\n}\n\nexport default SerpentineBorder\n"],"names":["DEFAULT_COLORS","resolveOverlapToPixels","horizontalOverlap","N","STROKE_WIDTH","totalBorderWidth","styleObjectToCss","obj","k","v","key","val","buildPathD","W","Y","R","COLORS","TOP_ARC_SHIFT","Y_OFFSET","O_TOTAL","BORDER_EXTRA","R1","RIGHT_EXTEND","n","parts","i","o","oj","r","rj","rj1","leftOffset","xLeft","rightExt","xRight","xLeftArc","xRightArc","xRightR1","yCurrTop","segs","t","yCurr","yNext","yExit","lastY","DEFAULT_SVG_CLASS","DEFAULTS","serpentineBorder","options","layoutMode","svgClassName","wrapperEl","measured","measureSections","TOTAL_BORDER_WIDTH","TOP_OFFSET","wrapperStyle","svgStyleObj","svgStyle","paths","totalHeight","totalWidth","viewBoxHeight","viewBoxMinX","viewBoxMinY","viewBoxStr","strokeCount","strokeWidth","excludeClassName","sectionEls","el","rect","baseWidth","cssStringToStyleObject","css","decl","colon","_","c","value","pathAttrsForReact","attrs","SerpentineBorder","children","radius","colors","wrapperRef","useRef","borderData","setBorderData","useState","useEffect","wrapper","measure","data","ro","jsxs","className","styleStr","restSvgAttrs","style","jsx","pathAttributes"],"mappings":"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"}
package/index.d.ts CHANGED
@@ -31,19 +31,8 @@ export interface SerpentineBorderOptions {
31
31
 
32
32
  export interface SerpentineBorderResult {
33
33
  wrapperStyle: Record<string, unknown>
34
- svgAttributes: { class?: string; viewBox: string; style: Record<string, unknown> }
35
- paths: Array<{ d: string; stroke: string; strokeWidth: number; fill: string }>
34
+ svgAttributes: { class?: string; viewBox: string; style: string }
35
+ paths: Array<{ d: string; stroke: string; 'stroke-width': number; fill: string }>
36
36
  }
37
37
 
38
38
  export declare function serpentineBorder(options: SerpentineBorderOptions): SerpentineBorderResult | null
39
-
40
- export declare function measureSections(
41
- wrapperEl: HTMLElement,
42
- options: {
43
- layoutMode: 'content' | 'border'
44
- horizontalOverlap?: number | 'borderWidth' | 'halfBorderWidth'
45
- strokeCount: number
46
- strokeWidth: number
47
- excludeClassName?: string
48
- }
49
- ): { width: number; sectionBottomYs: number[] } | null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serpentine-border",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Multi-stroke serpentine (wavy) border SVG — vanilla JS and React",
5
5
  "type": "module",
6
6
  "main": "./dist/serpentine-border.cjs",