@wsxjs/wsx-router 0.0.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 WSX Framework Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # @wsxjs/wsx-router
2
+
3
+ WSX Router - Native History API-based routing for WSX Framework
4
+
5
+ ## Features
6
+
7
+ - 🚀 **Zero dependencies** - Built on native History API
8
+ - 📦 **Lightweight** - Minimal bundle size
9
+ - 🎯 **TypeScript first** - Full type safety
10
+ - 🔄 **Declarative routing** - HTML-based route configuration
11
+ - 📱 **SPA-ready** - Single Page Application support
12
+ - 🎨 **Customizable** - CSS custom properties for styling
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add @wsxjs-router
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### 1. Basic Setup
23
+
24
+ ```html
25
+ <wsx-router>
26
+ <wsx-view route="/" component="home-page"></wsx-view>
27
+ <wsx-view route="/about" component="about-page"></wsx-view>
28
+ <wsx-view route="/users/:id" component="user-detail"></wsx-view>
29
+ <wsx-view route="*" component="not-found"></wsx-view>
30
+ </wsx-router>
31
+ ```
32
+
33
+ ### 2. Navigation Links
34
+
35
+ ```html
36
+ <wsx-link to="/">Home</wsx-link>
37
+ <wsx-link to="/about">About</wsx-link>
38
+ <wsx-link to="/users/123">User 123</wsx-link>
39
+ ```
40
+
41
+ ### 3. Programmatic Navigation
42
+
43
+ ```typescript
44
+ import { RouterUtils } from '@wsxjs-router';
45
+
46
+ // Navigate to a route
47
+ RouterUtils.navigate('/users/456');
48
+
49
+ // Replace current route
50
+ RouterUtils.replace('/login');
51
+
52
+ // Go back
53
+ RouterUtils.goBack();
54
+ ```
55
+
56
+ ## Components
57
+
58
+ ### WsxRouter
59
+
60
+ Main router container that manages route matching and rendering.
61
+
62
+ **Attributes:**
63
+ - None (router configuration is defined by child `wsx-view` elements)
64
+
65
+ **Events:**
66
+ - `route-changed`: Fired when route changes with `{ path, view }` detail
67
+
68
+ ### WsxView
69
+
70
+ Route view container that conditionally renders components.
71
+
72
+ **Attributes:**
73
+ - `route`: Route pattern (supports parameters like `/users/:id`)
74
+ - `component`: Component name to render
75
+ - `params`: Route parameters (automatically set by router)
76
+
77
+ ### WsxLink
78
+
79
+ Navigation link component with active state management.
80
+
81
+ **Attributes:**
82
+ - `to`: Target route path
83
+ - `replace`: Replace history instead of push (default: false)
84
+ - `active-class`: CSS class for active state (default: 'active')
85
+ - `exact`: Exact path matching (default: false)
86
+
87
+ **CSS Custom Properties:**
88
+ - `--link-color`: Link text color
89
+ - `--link-hover-color`: Link hover color
90
+ - `--link-active-color`: Active link color
91
+
92
+ ## API Reference
93
+
94
+ ### RouterUtils
95
+
96
+ Static utility class for programmatic navigation and route management.
97
+
98
+ #### Navigation Methods
99
+
100
+ ```typescript
101
+ // Navigate to a route
102
+ RouterUtils.navigate(path: string, replace?: boolean): void
103
+
104
+ // Get current route information
105
+ RouterUtils.getCurrentRoute(): RouteInfo
106
+
107
+ // Check if route is active
108
+ RouterUtils.isRouteActive(route: string, exact?: boolean): boolean
109
+
110
+ // Build path with parameters
111
+ RouterUtils.buildPath(route: string, params?: Record<string, string>): string
112
+ ```
113
+
114
+ #### Query Parameters
115
+
116
+ ```typescript
117
+ // Get query parameter
118
+ RouterUtils.getQueryParam(key: string): string | null
119
+
120
+ // Set query parameter
121
+ RouterUtils.setQueryParam(key: string, value: string, replace?: boolean): void
122
+
123
+ // Remove query parameter
124
+ RouterUtils.removeQueryParam(key: string, replace?: boolean): void
125
+ ```
126
+
127
+ #### History Management
128
+
129
+ ```typescript
130
+ // Go back/forward
131
+ RouterUtils.goBack(): void
132
+ RouterUtils.goForward(): void
133
+
134
+ // Replace current route
135
+ RouterUtils.replace(path: string): void
136
+ ```
137
+
138
+ ### Types
139
+
140
+ ```typescript
141
+ interface RouteInfo {
142
+ path: string;
143
+ params: Record<string, string>;
144
+ query: Record<string, string>;
145
+ hash: string;
146
+ meta?: Record<string, string | number | boolean>;
147
+ }
148
+
149
+ interface RouteMatch {
150
+ route: string;
151
+ params: Record<string, string>;
152
+ exact: boolean;
153
+ }
154
+ ```
155
+
156
+ ## Examples
157
+
158
+ ### Parameter Routes
159
+
160
+ ```html
161
+ <wsx-router>
162
+ <wsx-view route="/users/:userId/posts/:postId" component="post-detail"></wsx-view>
163
+ </wsx-router>
164
+ ```
165
+
166
+ The component will receive parameters as attributes:
167
+ ```html
168
+ <!-- For route: /users/123/posts/456 -->
169
+ <post-detail userid="123" postid="456"></post-detail>
170
+ ```
171
+
172
+ ### Nested Routing
173
+
174
+ ```html
175
+ <!-- Parent route with wildcard -->
176
+ <wsx-view route="/admin/*" component="admin-layout"></wsx-view>
177
+
178
+ <!-- Inside admin-layout component -->
179
+ <wsx-router base="/admin">
180
+ <wsx-view route="/dashboard" component="admin-dashboard"></wsx-view>
181
+ <wsx-view route="/users" component="admin-users"></wsx-view>
182
+ </wsx-router>
183
+ ```
184
+
185
+ ### Link Variations
186
+
187
+ ```html
188
+ <!-- Basic link -->
189
+ <wsx-link to="/about">About</wsx-link>
190
+
191
+ <!-- Button style -->
192
+ <wsx-link to="/login" variant="button">Login</wsx-link>
193
+
194
+ <!-- Tab style -->
195
+ <wsx-link to="/dashboard" variant="tab">Dashboard</wsx-link>
196
+
197
+ <!-- External link -->
198
+ <wsx-link to="https://example.com" external>External Site</wsx-link>
199
+ ```
200
+
201
+ ## Styling
202
+
203
+ ### CSS Custom Properties
204
+
205
+ ```css
206
+ wsx-link {
207
+ --link-color: #007bff;
208
+ --link-hover-color: #0056b3;
209
+ --link-active-color: #6c757d;
210
+ --link-focus-color: #007bff;
211
+ }
212
+
213
+ wsx-view {
214
+ --transition-duration: 300ms;
215
+ }
216
+ ```
217
+
218
+ ### CSS Parts
219
+
220
+ ```css
221
+ /* Style the link element */
222
+ wsx-link::part(link) {
223
+ font-weight: bold;
224
+ }
225
+
226
+ /* Style the view container */
227
+ wsx-view::part(view) {
228
+ padding: 1rem;
229
+ }
230
+ ```
231
+
232
+ ## Browser Support
233
+
234
+ - Modern browsers with Web Components support
235
+ - Shadow DOM v1
236
+ - Custom Elements v1
237
+ - History API
238
+
239
+ ## Contributing
240
+
241
+ See the main [WSX Framework Contributing Guide](../../CONTRIBUTING.md).
242
+
243
+ ## License
244
+
245
+ MIT License - see [LICENSE](../../LICENSE) for details.
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l=require("@wsxjs/wsx-core"),G=":host{display:block;width:100%;height:100%}.router-outlet{width:100%;height:100%}";var K=Object.create,$=Object.defineProperty,X=Object.getOwnPropertyDescriptor,L=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),P=e=>{throw TypeError(e)},I=(e,t,r)=>t in e?$(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,Y=(e,t)=>$(e,"name",{value:t,configurable:!0}),Z=e=>[,,,K((e==null?void 0:e[L("metadata")])??null)],tt=["class","method","getter","setter","accessor","field","value","get","set"],O=e=>e!==void 0&&typeof e!="function"?P("Function expected"):e,et=(e,t,r,i,o)=>({kind:tt[e],name:t,metadata:i,addInitializer:n=>r._?P("Already initialized"):o.push(O(n||null))}),rt=(e,t)=>I(t,L("metadata"),e[3]),ot=(e,t,r,i)=>{for(var o=0,n=e[t>>1],s=n&&n.length;o<s;o++)n[o].call(r);return i},it=(e,t,r,i,o,n)=>{var s,h,c,a=t&7,d=!1,m=0,f=e[m]||(e[m]=[]),u=a&&(o=o.prototype,a<5&&(a>3||!d)&&X(o,r));Y(o,r);for(var p=i.length-1;p>=0;p--)c=et(a,r,h={},e[3],f),s=(0,i[p])(o,c),h._=1,O(s)&&(o=s);return rt(e,o),u&&$(o,r,u),d?a^4?n:u:o},b=(e,t,r)=>I(e,typeof t!="symbol"?t+"":t,r),j,E,N;const v=l.createLogger("WsxRouter");j=[l.autoRegister({tagName:"wsx-router"})];class y extends(N=l.WebComponent){constructor(){super({styles:G}),b(this,"views",new Map),b(this,"currentView",null),b(this,"handleRouteChange",()=>{const t=window.location.pathname;v.debug(`Route changed to: ${t}`),this.currentView&&(this.currentView.style.display="none",v.debug("Hiding previous view"));const r=this.matchRoute(t);if(r){r.style.display="block",this.currentView=r,v.debug(`Showing view for route: ${r.getAttribute("route")}`);const i=this.extractParams(r.getAttribute("route")||"/",t);i&&r.setAttribute("params",JSON.stringify(i))}else v.warn(`No view found for path: ${t}`);this.dispatchEvent(new CustomEvent("route-changed",{detail:{path:t,view:r},bubbles:!0,composed:!0}))}),b(this,"interceptLinks",t=>{const r=t.target.closest("a");if(!r)return;const i=r.getAttribute("href");!i||i.startsWith("http")||i.startsWith("#")||(t.preventDefault(),this.navigate(i))})}render(){return l.jsx("div",{class:"router-outlet"},l.jsx("slot",null))}onConnected(){v.debug("WsxRouter connected to DOM"),this.collectViews(),v.debug("WsxRouter collected views:",this.views.size),window.addEventListener("popstate",this.handleRouteChange),this.addEventListener("click",this.interceptLinks),this.handleRouteChange()}onDisconnected(){window.removeEventListener("popstate",this.handleRouteChange)}collectViews(){var o;const t=(o=this.shadowRoot)==null?void 0:o.querySelector("slot");if(!t){v.error("WsxRouter: No slot found");return}const i=t.assignedElements().filter(n=>n.tagName.toLowerCase()==="wsx-view");v.debug("WsxRouter found views:",i.length),i.forEach(n=>{const s=n.getAttribute("route")||"/";this.views.set(s,n),n.style.display="none",v.debug(`WsxRouter hiding view for route: ${s}`)})}matchRoute(t){if(this.views.has(t))return this.views.get(t);for(const[r,i]of this.views)if(r.includes(":")){const o=r.replace(/:[^/]+/g,"([^/]+)");if(new RegExp(`^${o}$`).test(t))return i}return this.views.get("*")||null}extractParams(t,r){var c;if(!t.includes(":"))return null;const i=((c=t.match(/:([^/]+)/g))==null?void 0:c.map(a=>a.slice(1)))||[],o=t.replace(/:[^/]+/g,"([^/]+)"),n=new RegExp(`^${o}$`),s=r.match(n);if(!s||!i.length)return null;const h={};return i.forEach((a,d)=>{h[a]=s[d+1]}),h}navigate(t){window.history.pushState(null,"",t),this.handleRouteChange()}}E=Z(N);y=it(E,0,"WsxRouter",j,y);ot(E,1,y);const nt=":host{display:block;width:100%;height:100%}.route-view{width:100%;height:100%;position:relative}:host([loading]) .route-view{opacity:.5;transition:opacity .3s ease}:host([error]) .route-view{border:1px solid var(--error-color, #ff0000);padding:1rem}.route-view.entering{animation:fadeIn .3s ease-out}.route-view.leaving{animation:fadeOut .3s ease-in}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}";var st=Object.create,R=Object.defineProperty,at=Object.getOwnPropertyDescriptor,z=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),D=e=>{throw TypeError(e)},F=(e,t,r)=>t in e?R(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,ct=(e,t)=>R(e,"name",{value:t,configurable:!0}),lt=e=>[,,,st((e==null?void 0:e[z("metadata")])??null)],ht=["class","method","getter","setter","accessor","field","value","get","set"],U=e=>e!==void 0&&typeof e!="function"?D("Function expected"):e,dt=(e,t,r,i,o)=>({kind:ht[e],name:t,metadata:i,addInitializer:n=>r._?D("Already initialized"):o.push(U(n||null))}),ut=(e,t)=>F(t,z("metadata"),e[3]),pt=(e,t,r,i)=>{for(var o=0,n=e[t>>1],s=n&&n.length;o<s;o++)n[o].call(r);return i},vt=(e,t,r,i,o,n)=>{var s,h,c,a=t&7,d=!1,m=0,f=e[m]||(e[m]=[]),u=a&&(o=o.prototype,a<5&&(a>3||!d)&&at(o,r));ct(o,r);for(var p=i.length-1;p>=0;p--)c=dt(a,r,h={},e[3],f),s=(0,i[p])(o,c),h._=1,U(s)&&(o=s);return ut(e,o),u&&R(o,r,u),d?a^4?n:u:o},x=(e,t,r)=>F(e,typeof t!="symbol"?t+"":t,r),M,S,q;const k=l.createLogger("WsxView");M=[l.autoRegister({tagName:"wsx-view"})];class g extends(q=l.WebComponent){constructor(){super({styles:nt,styleName:"wsx-view"}),x(this,"component",null),x(this,"params",{}),x(this,"componentInstance",null)}render(){return l.jsx("div",{class:"route-view",part:"view"})}onConnected(){const t=this.getAttribute("component");t&&!this.componentInstance&&this.loadComponent(t)}onAttributeChanged(t,r,i){if(t==="component"&&i&&!this.componentInstance)this.loadComponent(i);else if(t==="params"&&this.componentInstance)try{this.params=JSON.parse(i),Object.entries(this.params).forEach(([o,n])=>{this.componentInstance.setAttribute(o,n)})}catch(o){k.error("Failed to parse params:",o)}}async loadComponent(t){var o;if(this.componentInstance&&(this.componentInstance.remove(),this.componentInstance=null),!customElements.get(t)){k.warn(`Component ${t} not found in customElements registry`);return}this.componentInstance=document.createElement(t),Object.keys(this.params).length>0&&Object.entries(this.params).forEach(([n,s])=>{this.componentInstance.setAttribute(n,s)});const i=(o=this.shadowRoot)==null?void 0:o.querySelector(".route-view");i?i.appendChild(this.componentInstance):k.error("Route view container not found")}onDisconnected(){this.componentInstance&&(this.componentInstance.remove(),this.componentInstance=null)}}S=lt(q);g=vt(S,0,"WsxView",M,g);x(g,"observedAttributes",["route","component","params"]);pt(S,1,g);const mt=':host{display:inline-block;min-width:fit-content;min-height:fit-content;width:auto;height:auto}.wsx-link{color:var(--link-color, #007bff);text-decoration:var(--link-decoration, underline);cursor:pointer;transition:color .2s ease;display:inline-block;min-height:1.2em;line-height:1.2}.wsx-link:hover{color:var(--link-hover-color, #0056b3);text-decoration:var(--link-hover-decoration, underline)}.wsx-link:focus{outline:2px solid var(--link-focus-color, #007bff);outline-offset:2px}.wsx-link.active{color:var(--link-active-color, #6c757d);font-weight:var(--link-active-weight, bold)}:host([disabled]) .wsx-link{color:var(--link-disabled-color, #6c757d);cursor:not-allowed;pointer-events:none}:host([external]) .wsx-link:after{content:"↗";font-size:.8em;margin-left:.2em;opacity:.7}:host([variant="button"]) .wsx-link{background-color:var(--button-bg, #007bff);color:var(--button-color, white);padding:.5rem 1rem;border-radius:.25rem;text-decoration:none;display:inline-block}:host([variant="button"]) .wsx-link:hover{background-color:var(--button-hover-bg, #0056b3);color:var(--button-hover-color, white)}:host([variant="tab"]) .wsx-link{padding:.5rem 1rem;border-bottom:2px solid transparent;text-decoration:none}:host([variant="tab"]) .wsx-link.active{border-bottom-color:var(--tab-active-border, #007bff)}';var wt=Object.create,C=Object.defineProperty,ft=Object.getOwnPropertyDescriptor,V=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),T=e=>{throw TypeError(e)},Q=(e,t,r)=>t in e?C(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,gt=(e,t)=>C(e,"name",{value:t,configurable:!0}),_t=e=>[,,,wt((e==null?void 0:e[V("metadata")])??null)],bt=["class","method","getter","setter","accessor","field","value","get","set"],H=e=>e!==void 0&&typeof e!="function"?T("Function expected"):e,xt=(e,t,r,i,o)=>({kind:bt[e],name:t,metadata:i,addInitializer:n=>r._?T("Already initialized"):o.push(H(n||null))}),yt=(e,t)=>Q(t,V("metadata"),e[3]),kt=(e,t,r,i)=>{for(var o=0,n=e[t>>1],s=n&&n.length;o<s;o++)n[o].call(r);return i},$t=(e,t,r,i,o,n)=>{var s,h,c,a=t&7,d=!1,m=0,f=e[m]||(e[m]=[]),u=a&&(o=o.prototype,a<5&&(a>3||!d)&&ft(o,r));gt(o,r);for(var p=i.length-1;p>=0;p--)c=xt(a,r,h={},e[3],f),s=(0,i[p])(o,c),h._=1,H(s)&&(o=s);return yt(e,o),u&&C(o,r,u),d?a^4?n:u:o},w=(e,t,r)=>Q(e,typeof t!="symbol"?t+"":t,r),J,W,B;const A=l.createLogger("WsxLink");J=[l.autoRegister({tagName:"wsx-link"})];class _ extends(B=l.WebComponent){constructor(){super({styles:mt,styleName:"wsx-link"}),w(this,"to",""),w(this,"replace",!1),w(this,"activeClass","active"),w(this,"exact",!1),w(this,"handleClick",t=>{if(t.preventDefault(),!this.to){A.warn("No 'to' attribute specified");return}if(this.isExternalLink(this.to)){window.open(this.to,"_blank");return}this.replace?window.history.replaceState(null,"",this.to):window.history.pushState(null,"",this.to),window.dispatchEvent(new PopStateEvent("popstate")),A.debug(`Navigated to: ${this.to}`)}),w(this,"updateActiveState",()=>{var o;const t=window.location.pathname,r=this.exact?t===this.to:t.startsWith(this.to)&&this.to!=="/",i=(o=this.shadowRoot)==null?void 0:o.querySelector("a");i&&(r?(i.classList.add(this.activeClass),this.setAttribute("active","")):(i.classList.remove(this.activeClass),this.removeAttribute("active")))})}render(){return l.jsx("a",{href:this.to,class:"wsx-link",onClick:this.handleClick,part:"link"},l.jsx("slot",null))}onConnected(){this.to=this.getAttribute("to")||"",this.replace=this.hasAttribute("replace"),this.activeClass=this.getAttribute("active-class")||"active",this.exact=this.hasAttribute("exact");const t=this.shadowRoot.querySelector(".wsx-link");t&&(t.href=this.to),window.addEventListener("popstate",this.updateActiveState),document.addEventListener("route-changed",this.updateActiveState),this.updateActiveState()}onDisconnected(){window.removeEventListener("popstate",this.updateActiveState),document.removeEventListener("route-changed",this.updateActiveState)}onAttributeChanged(t,r,i){switch(t){case"to":this.to=i||"",this.rerender(),this.updateActiveState();break;case"replace":this.replace=i!==null&&i!=="false";break;case"active-class":this.activeClass=i||"active",this.updateActiveState();break;case"exact":this.exact=i!==null&&i!=="false",this.updateActiveState();break}}isExternalLink(t){return t.startsWith("http://")||t.startsWith("https://")||t.startsWith("mailto:")||t.startsWith("tel:")}navigate(){this.to&&this.handleClick(new MouseEvent("click",{bubbles:!0,cancelable:!0}))}}W=_t(B);_=$t(W,0,"WsxLink",J,_);w(_,"observedAttributes",["to","replace","active-class","exact"]);kt(W,1,_);const Et=l.createLogger("RouterUtils");class Rt{static navigate(t,r=!1){r?window.history.replaceState(null,"",t):window.history.pushState(null,"",t),window.dispatchEvent(new PopStateEvent("popstate")),Et.debug(`Navigated to: ${t} (replace: ${r})`)}static getCurrentRoute(){const t=new URL(window.location.href);return{path:t.pathname,params:{},query:Object.fromEntries(t.searchParams.entries()),hash:t.hash.slice(1)}}static parseRoute(t,r){var i;if(t===r)return{route:t,params:{},exact:!0};if(t==="*")return{route:t,params:{},exact:!1};if(t.includes(":")){const o=((i=t.match(/:([^/]+)/g))==null?void 0:i.map(c=>c.slice(1)))||[],n=t.replace(/:[^/]+/g,"([^/]+)"),s=new RegExp(`^${n}$`),h=r.match(s);if(h&&o.length>0){const c={};return o.forEach((a,d)=>{c[a]=h[d+1]}),{route:t,params:c,exact:!0}}}if(t.endsWith("/*")){const o=t.slice(0,-2);if(r.startsWith(o))return{route:t,params:{},exact:!1}}return null}static buildPath(t,r={}){let i=t;return Object.entries(r).forEach(([o,n])=>{i=i.replace(`:${o}`,encodeURIComponent(n))}),i}static isRouteActive(t,r=!1){const i=window.location.pathname;return r?i===t:t==="/"?i==="/":i.startsWith(t)}static getRouteDepth(t){return t.split("/").filter(r=>r.length>0).length}static getParentRoute(t){const r=t.split("/").filter(i=>i.length>0);return r.length<=1?"/":(r.pop(),"/"+r.join("/"))}static joinPaths(...t){return t.map(r=>r.replace(/^\/+|\/+$/g,"")).filter(r=>r.length>0).join("/").replace(/^/,"/")}static isExternalUrl(t){return/^https?:\/\//.test(t)||/^mailto:/.test(t)||/^tel:/.test(t)}static getQueryParam(t){return new URL(window.location.href).searchParams.get(t)}static setQueryParam(t,r,i=!1){const o=new URL(window.location.href);o.searchParams.set(t,r);const n=o.pathname+o.search+o.hash;this.navigate(n,i)}static removeQueryParam(t,r=!1){const i=new URL(window.location.href);i.searchParams.delete(t);const o=i.pathname+i.search+i.hash;this.navigate(o,r)}static goBack(){window.history.back()}static goForward(){window.history.forward()}static replace(t){this.navigate(t,!0)}static getHistoryLength(){return window.history.length}static onRouteChange(t){const r=()=>{const i=this.getCurrentRoute();t(i)};return window.addEventListener("popstate",r),document.addEventListener("route-changed",r),()=>{window.removeEventListener("popstate",r),document.removeEventListener("route-changed",r)}}}exports.RouterUtils=Rt;exports.WsxLink=_;exports.WsxRouter=y;exports.WsxView=g;