@sondor-technology/sondor-components 1.0.2 → 1.0.3

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.
@@ -12,25 +12,22 @@ let StNavGroup = class StNavGroup extends LitElement {
12
12
  super(...arguments);
13
13
  this.label = '';
14
14
  this.open = false;
15
+ this.depth = 0;
15
16
  }
16
17
  _toggle() {
17
18
  this.open = !this.open;
18
19
  }
19
20
  render() {
20
21
  return html `
21
- <button @click=${this._toggle} aria-expanded=${this.open}>
22
- ${this.label}
23
- <svg viewBox="0 0 24 24">
24
- <path d="M6 9l6 6 6-6"></path>
25
- </svg>
22
+ <button @click=${this._toggle} aria-expanded=${this.open} style="--nav-depth: ${this.depth}">
23
+ <slot name="icon"></slot>
24
+ <span class="label">${this.label}</span>
25
+ <svg class="chevron" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"></path></svg>
26
26
  </button>
27
27
 
28
28
  <div class="drawer">
29
29
  <div class="drawer-content">
30
- <!-- We add slight padding top/bottom so the inner links breathe -->
31
- <div style="padding: 4px 0;">
32
- <slot></slot>
33
- </div>
30
+ <slot></slot>
34
31
  </div>
35
32
  </div>
36
33
  `;
@@ -41,32 +38,51 @@ StNavGroup.styles = [
41
38
  css `
42
39
  :host {
43
40
  display: block;
44
- margin-bottom: var(--st-spacing-sm);
41
+ margin-bottom: 2px;
42
+ overflow: hidden;
45
43
  }
46
44
 
47
45
  button {
48
46
  display: flex;
49
47
  align-items: center;
50
- justify-content: space-between;
48
+ gap: var(--st-spacing-sm, 8px);
51
49
  width: 100%;
50
+ box-sizing: border-box;
52
51
  background: transparent;
53
52
  border: none;
54
- padding: var(--st-spacing-xs) var(--st-spacing-lg);
53
+ padding: var(--st-spacing-sm) var(--st-spacing-xl);
54
+ padding-left: calc(var(--st-spacing-xl, 24px) + (var(--nav-depth, 0) * 16px));
55
55
  color: var(--st-color-text);
56
56
  font-size: var(--st-font-size-sm);
57
- font-weight: var(--st-font-weight-bold);
57
+ font-weight: var(--st-font-weight-semibold);
58
58
  cursor: pointer;
59
- text-align: left;
60
- transition: color 0.2s ease;
59
+ transition: background-color 0.2s ease, color 0.2s ease;
61
60
  }
62
61
 
63
62
  button:hover {
64
- color: var(--st-color-primary);
63
+ background-color: var(--st-color-surface-hover, rgba(0,0,0,0.03));
65
64
  }
66
65
 
67
- svg {
66
+ /* Styles for slotted icons */
67
+ ::slotted([slot="icon"]) {
68
68
  width: 1rem;
69
69
  height: 1rem;
70
+ flex-shrink: 0;
71
+ color: var(--st-color-text-muted);
72
+ }
73
+
74
+ .label {
75
+ flex: 1;
76
+ white-space: nowrap;
77
+ overflow: hidden;
78
+ text-overflow: ellipsis;
79
+ text-align: left;
80
+ }
81
+
82
+ svg.chevron {
83
+ width: 1rem;
84
+ height: 1rem;
85
+ flex-shrink: 0;
70
86
  fill: none;
71
87
  stroke: currentColor;
72
88
  stroke-width: 2;
@@ -75,12 +91,10 @@ StNavGroup.styles = [
75
91
  transition: transform 0.2s ease;
76
92
  }
77
93
 
78
- /* Rotate Chevron when open */
79
- :host([open]) svg {
94
+ :host([open]) svg.chevron {
80
95
  transform: rotate(180deg);
81
96
  }
82
97
 
83
- /* Modern CSS Grid Animation trick for accordion height */
84
98
  .drawer {
85
99
  display: grid;
86
100
  grid-template-rows: 0fr;
@@ -95,7 +109,6 @@ StNavGroup.styles = [
95
109
  overflow: hidden;
96
110
  display: flex;
97
111
  flex-direction: column;
98
- gap: 2px;
99
112
  }
100
113
  `
101
114
  ];
@@ -105,6 +118,9 @@ __decorate([
105
118
  __decorate([
106
119
  property({ type: Boolean, reflect: true })
107
120
  ], StNavGroup.prototype, "open", void 0);
121
+ __decorate([
122
+ property({ type: Number })
123
+ ], StNavGroup.prototype, "depth", void 0);
108
124
  StNavGroup = __decorate([
109
125
  customElement('st-vertical-nav-group')
110
126
  ], StNavGroup);
@@ -2,7 +2,13 @@ import { LitElement } from 'lit';
2
2
  export declare class StNavItem extends LitElement {
3
3
  href: string;
4
4
  active: boolean;
5
+ depth: number;
5
6
  static styles: import("lit").CSSResult[];
6
7
  render(): import("lit-html").TemplateResult<1>;
7
8
  }
9
+ declare global {
10
+ interface HTMLElementTagNameMap {
11
+ 'st-vertical-nav-item': StNavItem;
12
+ }
13
+ }
8
14
  //# sourceMappingURL=vertical-nav-item.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vertical-nav-item.d.ts","sourceRoot":"","sources":["../../../src/components/vertical-nav/vertical-nav-item.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAI5C,qBACa,SAAU,SAAQ,UAAU;IACX,IAAI,SAAO;IACK,MAAM,UAAS;IAE3D,MAAM,CAAC,MAAM,4BA+BX;IAEF,MAAM,yCAML;CACF"}
1
+ {"version":3,"file":"vertical-nav-item.d.ts","sourceRoot":"","sources":["../../../src/components/vertical-nav/vertical-nav-item.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAI5C,qBACa,SAAU,SAAQ,UAAU;IACX,IAAI,SAAO;IACK,MAAM,UAAS;IAC/B,KAAK,SAAK;IAEtC,MAAM,CAAC,MAAM,4BA8CX;IAEF,MAAM,yCAOL;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,sBAAsB,EAAE,SAAS,CAAC;KACnC;CACF"}
@@ -12,10 +12,12 @@ let StNavItem = class StNavItem extends LitElement {
12
12
  super(...arguments);
13
13
  this.href = '#';
14
14
  this.active = false;
15
+ this.depth = 0;
15
16
  }
16
17
  render() {
17
18
  return html `
18
- <a href=${this.href} part="link">
19
+ <a href=${this.href} part="link" style="--nav-depth: ${this.depth}">
20
+ <slot name="icon"></slot>
19
21
  <slot></slot>
20
22
  </a>
21
23
  `;
@@ -26,30 +28,45 @@ StNavItem.styles = [
26
28
  css `
27
29
  :host {
28
30
  display: block;
31
+ overflow: hidden;
29
32
  }
30
33
 
31
34
  a {
32
35
  display: flex;
33
36
  align-items: center;
34
- padding: 6px var(--st-spacing-lg);
37
+ gap: var(--st-spacing-sm, 8px);
38
+ width: 100%;
39
+ box-sizing: border-box;
40
+ padding: 8px var(--st-spacing-xl);
41
+ padding-left: calc(var(--st-spacing-xl, 24px) + (var(--nav-depth, 0) * 16px));
35
42
  color: var(--st-color-text-muted);
36
43
  font-size: var(--st-font-size-sm);
37
44
  text-decoration: none;
38
- border-left: 2px solid transparent;
45
+ border-left: 3px solid transparent;
39
46
  transition: all 0.15s ease;
47
+
48
+ white-space: nowrap;
49
+ overflow: hidden;
50
+ text-overflow: ellipsis;
40
51
  }
41
52
 
42
53
  a:hover {
43
54
  color: var(--st-color-text);
44
- background-color: var(--st-color-surface);
55
+ background-color: var(--st-color-surface-hover, rgba(0,0,0,0.03));
56
+ }
57
+
58
+ ::slotted([slot="icon"]) {
59
+ width: 1rem;
60
+ height: 1rem;
61
+ flex-shrink: 0;
62
+ color: inherit;
45
63
  }
46
64
 
47
- /* Active state: Purple text with a left border indicator */
48
65
  :host([active]) a {
49
66
  color: var(--st-color-primary);
50
67
  border-left-color: var(--st-color-primary);
51
- font-weight: var(--st-font-weight-medium);
52
- background-color: transparent;
68
+ font-weight: var(--st-font-weight-bold);
69
+ background-color: var(--st-color-primary-light, rgba(98, 0, 238, 0.05));
53
70
  }
54
71
  `
55
72
  ];
@@ -59,6 +76,9 @@ __decorate([
59
76
  __decorate([
60
77
  property({ type: Boolean, reflect: true })
61
78
  ], StNavItem.prototype, "active", void 0);
79
+ __decorate([
80
+ property({ type: Number })
81
+ ], StNavItem.prototype, "depth", void 0);
62
82
  StNavItem = __decorate([
63
83
  customElement('st-vertical-nav-item')
64
84
  ], StNavItem);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sondor-technology/sondor-components",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -101,6 +101,10 @@
101
101
  "types": "./dist/components/icon/icon.d.ts",
102
102
  "default": "./dist/components/icon/icon.js"
103
103
  },
104
+ "./icon-paths": {
105
+ "types": "./dist/components/icon/icon-paths.d.ts",
106
+ "default": "./dist/components/icon/icon-paths.js"
107
+ },
104
108
  "./footer": {
105
109
  "types": "./dist/components/footer/footer.d.ts",
106
110
  "default": "./dist/components/footer/footer.js"
@@ -119,6 +119,22 @@ export const iconPaths = {
119
119
  outline: svg`<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"></rect><line x1="3" y1="8" x2="21" y2="8" stroke="currentColor" stroke-width="2"></line><line x1="7" y1="8" x2="7" y2="21" stroke="currentColor" stroke-width="2"></line><line x1="17" y1="8" x2="17" y2="21" stroke="currentColor" stroke-width="2"></line><line x1="10" y1="14" x2="14" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></line>`,
120
120
  solid: svg`<path fill="currentColor" d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5zm2 3h14V5H5v3zM7 9H3v12h4V9zm14 0h-4v12h4V9z"></path>`,
121
121
  },
122
+ calender: {
123
+ outline: svg`<rect x="3" y="4" width="18" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></rect><line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line><line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line><line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"></line>`,
124
+ solid: svg`<path fill="currentColor" d="M19 4h-1V2h-2v2H8V2H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 16H5V9h14v11z"></path>`,
125
+ },
126
+ user: {
127
+ outline: svg`<circle cx="12" cy="8" r="4" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M4 20c0-4 4-6 8-6s8 2 8 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>`,
128
+ solid: svg`<path fill="currentColor" d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5zm0 2c-3.33 0-10 1.67-10 5v3h20v-3c0-3.33-6.67-5-10-5z"></path>`,
129
+ },
130
+ home: {
131
+ outline: svg`<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><polyline points="9 22 9 12 15 12 15 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></polyline>`,
132
+ solid: svg`<path fill="currentColor" d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"></path>`,
133
+ },
134
+ "circle-cross": {
135
+ outline: svg`<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></circle><line x1="15" y1="9" x2="9" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></line><line x1="9" y1="9" x2="15" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></line>`,
136
+ solid: svg`<path fill="currentColor" fill-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm3.36 6.64a1 1 0 0 1 0 1.41L13.41 12l1.95 1.95a1 1 0 0 1-1.41 1.41L12 13.41l-1.95 1.95a1 1 0 0 1-1.41-1.41L10.59 12 8.64 10.05a1 1 0 0 1 1.41-1.41L12 10.59l1.95-1.95a1 1 0 0 1 1.41 0z" clip-rule="evenodd"></path>`,
137
+ },
122
138
  };
123
139
 
124
140
  export type IconName = keyof typeof iconPaths;
@@ -1,4 +1,4 @@
1
- import { LitElement, html, css, PropertyValues } from 'lit';
1
+ import { LitElement, html, css, PropertyValues, nothing } from 'lit';
2
2
  import { customElement, property, query } from 'lit/decorators.js';
3
3
  import { resetStyles } from '../../styles/reset.js';
4
4
 
@@ -14,8 +14,20 @@ export class StModal extends LitElement {
14
14
  */
15
15
  @property({ type: String }) heading = '';
16
16
 
17
+ /**
18
+ * Size of the modal (sm, md, lg, xl, xxl).
19
+ */
20
+ @property({ type: String }) size: 'sm' | 'md' | 'lg' | 'xl' | 'xxl' = 'md';
21
+
22
+ /**
23
+ * If true, the modal cannot be closed by clicking the backdrop or pressing Esc.
24
+ */
25
+ @property({ type: Boolean }) static = false;
26
+
17
27
  @query('dialog') private dialogEl!: HTMLDialogElement;
18
28
 
29
+ private isClosing = false;
30
+
19
31
  static styles = [
20
32
  resetStyles,
21
33
  css`
@@ -29,17 +41,59 @@ export class StModal extends LitElement {
29
41
  border-radius: var(--st-radius-lg, 0.5rem);
30
42
  background-color: var(--st-color-surface);
31
43
  color: var(--st-color-text);
32
- width: 100%;
33
- max-width: 500px;
44
+
45
+ /* Responsive width calculation */
46
+ width: calc(100% - 2rem);
47
+ margin: auto;
48
+
34
49
  box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
35
50
  font-family: var(--st-font-family, inherit);
36
51
  }
37
52
 
53
+ /* Sizes */
54
+ dialog.size-sm { max-width: 400px; }
55
+ dialog.size-md { max-width: 500px; }
56
+ dialog.size-lg { max-width: 800px; }
57
+ dialog.size-xl { max-width: 1024px; }
58
+ dialog.size-xxl { max-width: 1280px; }
59
+
38
60
  dialog::backdrop {
39
61
  background-color: rgba(0, 0, 0, 0.5);
40
62
  backdrop-filter: blur(2px);
41
63
  }
42
64
 
65
+ /* Animations */
66
+ @keyframes fade-in {
67
+ from { opacity: 0; }
68
+ to { opacity: 1; }
69
+ }
70
+ @keyframes fade-out {
71
+ from { opacity: 1; }
72
+ to { opacity: 0; }
73
+ }
74
+ @keyframes slide-up {
75
+ from { opacity: 0; transform: translateY(20px) scale(0.95); }
76
+ to { opacity: 1; transform: translateY(0) scale(1); }
77
+ }
78
+ @keyframes slide-down {
79
+ from { opacity: 1; transform: translateY(0) scale(1); }
80
+ to { opacity: 0; transform: translateY(20px) scale(0.95); }
81
+ }
82
+
83
+ dialog[open] {
84
+ animation: slide-up 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
85
+ }
86
+ dialog[open]::backdrop {
87
+ animation: fade-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
88
+ }
89
+
90
+ dialog.closing {
91
+ animation: slide-down 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
92
+ }
93
+ dialog.closing::backdrop {
94
+ animation: fade-out 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
95
+ }
96
+
43
97
  .modal-header {
44
98
  display: flex;
45
99
  align-items: center;
@@ -92,21 +146,47 @@ export class StModal extends LitElement {
92
146
 
93
147
  updated(changedProperties: PropertyValues) {
94
148
  if (changedProperties.has('open')) {
95
- if (this.dialogEl) {
96
- if (this.open && !this.dialogEl.open) {
149
+ if (this.open) {
150
+ this.dialogEl.classList.remove('closing');
151
+ this.isClosing = false;
152
+ if (!this.dialogEl.open) {
97
153
  this.dialogEl.showModal();
98
- } else if (!this.open && this.dialogEl.open) {
99
- this.dialogEl.close();
100
154
  }
155
+ } else if (this.dialogEl.open && !this.isClosing) {
156
+ this.closeWithAnimation();
101
157
  }
102
158
  }
103
159
  }
104
160
 
161
+ private closeWithAnimation() {
162
+ if (this.isClosing) return;
163
+ this.isClosing = true;
164
+ this.dialogEl.classList.add('closing');
165
+
166
+ const onAnimationEnd = () => {
167
+ this.dialogEl.removeEventListener('animationend', onAnimationEnd);
168
+ this.dialogEl.classList.remove('closing');
169
+ this.isClosing = false;
170
+ if (!this.open) {
171
+ this.dialogEl.close();
172
+ }
173
+ };
174
+
175
+ this.dialogEl.addEventListener('animationend', onAnimationEnd);
176
+ }
177
+
105
178
  private handleClose() {
106
179
  this.open = false;
107
180
  this.dispatchEvent(new CustomEvent('st-close', { bubbles: true, composed: true }));
108
181
  }
109
182
 
183
+ private handleCancel(e: Event) {
184
+ e.preventDefault();
185
+ if (!this.static) {
186
+ this.handleClose();
187
+ }
188
+ }
189
+
110
190
  private onDialogClose() {
111
191
  if (this.open) {
112
192
  this.open = false;
@@ -115,13 +195,16 @@ export class StModal extends LitElement {
115
195
  }
116
196
 
117
197
  private handleBackdropClick(e: MouseEvent) {
198
+ if (this.static) return;
199
+
118
200
  const rect = this.dialogEl.getBoundingClientRect();
119
- if (
201
+ const clickedOutside =
120
202
  e.clientX < rect.left ||
121
203
  e.clientX > rect.right ||
122
204
  e.clientY < rect.top ||
123
- e.clientY > rect.bottom
124
- ) {
205
+ e.clientY > rect.bottom;
206
+
207
+ if (clickedOutside) {
125
208
  this.handleClose();
126
209
  }
127
210
  }
@@ -129,17 +212,22 @@ export class StModal extends LitElement {
129
212
  render() {
130
213
  return html`
131
214
  <dialog
132
- @cancel=${this.handleClose}
215
+ class="size-${this.size}"
216
+ aria-labelledby=${this.heading ? 'modal-heading' : nothing}
217
+ @cancel=${this.handleCancel}
133
218
  @click=${this.handleBackdropClick}
134
219
  @close=${this.onDialogClose}
135
220
  >
136
221
  <div class="modal-header" @click=${(e: MouseEvent) => e.stopPropagation()}>
137
222
  <slot name="header">
138
- <h3 class="modal-title">${this.heading}</h3>
223
+ <h3 class="modal-title" id="modal-heading">${this.heading}</h3>
139
224
  </slot>
140
- <button class="close-btn" @click=${this.handleClose} aria-label="Close modal">
141
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
142
- </button>
225
+
226
+ ${!this.static ? html`
227
+ <button class="close-btn" @click=${this.handleClose} aria-label="Close modal">
228
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
229
+ </button>
230
+ ` : nothing}
143
231
  </div>
144
232
  <div class="modal-body" @click=${(e: MouseEvent) => e.stopPropagation()}>
145
233
  <slot></slot>
@@ -1,14 +1,40 @@
1
- class AppRouter {
2
- constructor() {
1
+ export interface RouteDefinition {
2
+ component: string;
3
+ [key: string]: unknown; // Allows consuming apps to attach custom metadata (e.g., titles, auth roles)
4
+ }
5
+
6
+ export type RouteConfig = Record<string, RouteDefinition>;
7
+
8
+ export interface RouteMatch {
9
+ path: string;
10
+ component: string;
11
+ params: Record<string, string>;
12
+ definition: RouteDefinition;
13
+ }
14
+
15
+ function getRouteWeight(pattern: string): number {
16
+ const segments = pattern.split('/').filter(Boolean);
17
+ return segments.reduce((weight, seg) => {
18
+ return weight + (seg.startsWith(':') ? 1 : 10);
19
+ }, 0);
20
+ }
21
+
22
+ export class AppRouter {
23
+ private _routes: RouteConfig = {};
24
+ private _initialized = false;
25
+
26
+ constructor(routes?: RouteConfig) {
27
+ if (routes) {
28
+ this.setRoutes(routes);
29
+ }
30
+
3
31
  window.addEventListener('popstate', () => this._notify());
4
32
 
5
33
  document.addEventListener('click', (e) => {
6
- // 1. Ignore modified clicks (Ctrl+Click, etc)
7
34
  if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
8
35
  return;
9
36
  }
10
37
 
11
- // 2. Safely traverse the path
12
38
  const path = e.composedPath();
13
39
  const link = path.find((el: any) =>
14
40
  el && typeof el.hasAttribute === 'function' && el.hasAttribute('href')
@@ -19,29 +45,90 @@ class AppRouter {
19
45
  const href = link.getAttribute('href');
20
46
  const target = link.getAttribute('target');
21
47
 
22
- // 3. Ignore external links
23
48
  if (!href || href.startsWith('http') || href.startsWith('mailto:') || target === '_blank') {
24
49
  return;
25
50
  }
26
51
 
27
- // 4. Intercept internal links
28
52
  if (href.startsWith('/')) {
29
53
  e.preventDefault();
30
- window.history.pushState(null, '', href);
31
- this._notify();
54
+ this.navigate(href);
32
55
  }
33
56
  });
57
+ }
58
+
59
+ /**
60
+ * Register or update the route configuration for this application.
61
+ */
62
+ public setRoutes(routes: RouteConfig) {
63
+ this._routes = routes;
34
64
 
35
- // Broadcast initial route on load
36
- // Use setTimeout to ensure all Lit components have registered first
37
- setTimeout(() => this._notify(), 0);
65
+ // Trigger initial route resolution once routes are registered
66
+ if (!this._initialized) {
67
+ this._initialized = true;
68
+ setTimeout(() => this._notify(), 0);
69
+ } else {
70
+ this._notify();
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Programmatically navigate to an internal path.
76
+ */
77
+ public navigate(path: string) {
78
+ window.history.pushState(null, '', path);
79
+ this._notify();
80
+ }
81
+
82
+ /**
83
+ * Matches a URL path against the registered routes.
84
+ */
85
+ public matchRoute(currentPath: string): RouteMatch | null {
86
+ const sortedRoutes = Object.entries(this._routes).sort(
87
+ ([patternA], [patternB]) => getRouteWeight(patternB) - getRouteWeight(patternA)
88
+ );
89
+
90
+ for (const [pattern, routeDef] of sortedRoutes) {
91
+ const paramNames: string[] = [];
92
+ const regexPath = pattern.replace(/:([a-zA-Z0-9_]+)/g, (_, paramName) => {
93
+ paramNames.push(paramName);
94
+ return '([^/]+)';
95
+ });
96
+
97
+ const regex = new RegExp(`^${regexPath}$`);
98
+ const match = currentPath.match(regex);
99
+
100
+ if (match) {
101
+ const params: Record<string, string> = {};
102
+ paramNames.forEach((name, index) => {
103
+ params[name] = decodeURIComponent(match[index + 1]);
104
+ });
105
+
106
+ return {
107
+ path: currentPath,
108
+ component: routeDef.component,
109
+ params,
110
+ definition: routeDef
111
+ };
112
+ }
113
+ }
114
+
115
+ return null;
38
116
  }
39
117
 
40
118
  private _notify() {
119
+ const currentPath = window.location.pathname;
120
+ const match = this.matchRoute(currentPath);
121
+
41
122
  window.dispatchEvent(new CustomEvent('route-changed', {
42
- detail: window.location.pathname
123
+ detail: {
124
+ path: currentPath,
125
+ component: match ? match.component : 'not-found-page',
126
+ params: match ? match.params : {},
127
+ definition: match ? match.definition : null
128
+ }
43
129
  }));
44
130
  }
45
131
  }
46
132
 
133
+ // Export a singleton instance for standard app-wide use
47
134
  export const router = new AppRouter();