@sondor-technology/sondor-components 1.0.2 → 1.0.4

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.
Files changed (40) hide show
  1. package/dist/components/checkbox/checkbox.d.ts +3 -0
  2. package/dist/components/checkbox/checkbox.d.ts.map +1 -1
  3. package/dist/components/checkbox/checkbox.js +38 -20
  4. package/dist/components/form/form-step.d.ts +1 -0
  5. package/dist/components/form/form-step.d.ts.map +1 -1
  6. package/dist/components/form/form-step.js +3 -0
  7. package/dist/components/form/form.d.ts +7 -2
  8. package/dist/components/form/form.d.ts.map +1 -1
  9. package/dist/components/form/form.js +157 -59
  10. package/dist/components/icon/icon-paths.d.ts +16 -0
  11. package/dist/components/icon/icon-paths.d.ts.map +1 -1
  12. package/dist/components/icon/icon-paths.js +16 -0
  13. package/dist/components/icon/icon.d.ts +2 -1
  14. package/dist/components/icon/icon.d.ts.map +1 -1
  15. package/dist/components/modal/modal.d.ts +11 -0
  16. package/dist/components/modal/modal.d.ts.map +1 -1
  17. package/dist/components/modal/modal.js +103 -15
  18. package/dist/components/router/router.d.ts +27 -3
  19. package/dist/components/router/router.d.ts.map +1 -1
  20. package/dist/components/router/router.js +72 -12
  21. package/dist/components/skeleton/skeleton.d.ts +2 -3
  22. package/dist/components/skeleton/skeleton.d.ts.map +1 -1
  23. package/dist/components/skeleton/skeleton.js +89 -18
  24. package/dist/components/vertical-nav/vertical-nav-group.d.ts +6 -0
  25. package/dist/components/vertical-nav/vertical-nav-group.d.ts.map +1 -1
  26. package/dist/components/vertical-nav/vertical-nav-group.js +37 -21
  27. package/dist/components/vertical-nav/vertical-nav-item.d.ts +6 -0
  28. package/dist/components/vertical-nav/vertical-nav-item.d.ts.map +1 -1
  29. package/dist/components/vertical-nav/vertical-nav-item.js +27 -7
  30. package/package.json +5 -1
  31. package/src/components/checkbox/checkbox.ts +54 -26
  32. package/src/components/form/form-step.ts +5 -1
  33. package/src/components/form/form.ts +182 -66
  34. package/src/components/icon/icon-paths.ts +16 -0
  35. package/src/components/icon/icon.ts +3 -1
  36. package/src/components/modal/modal.ts +103 -15
  37. package/src/components/router/router.ts +99 -12
  38. package/src/components/skeleton/skeleton.ts +87 -20
  39. package/src/components/vertical-nav/vertical-nav-group.ts +40 -21
  40. package/src/components/vertical-nav/vertical-nav-item.ts +30 -7
@@ -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();
@@ -4,26 +4,69 @@ import { resetStyles } from '../../styles/reset.js';
4
4
 
5
5
  @customElement('st-skeleton')
6
6
  export class StSkeleton extends LitElement {
7
- /** Variant shape: 'text' | 'circular' | 'rectangular' */
8
- @property({ type: String }) variant: 'text' | 'circular' | 'rectangular' = 'text';
9
-
10
- /** Custom width override (e.g., '200px', '50%') */
7
+ @property({ type: String, reflect: true }) variant: 'text' | 'circular' | 'rectangular' = 'text';
8
+ @property({ type: String, reflect: true }) animation: 'shimmer' | 'pulse' | 'none' = 'shimmer';
11
9
  @property({ type: String }) width = '';
12
-
13
- /** Custom height override (e.g., '40px', '1rem') */
14
10
  @property({ type: String }) height = '';
15
11
 
12
+ // Apply properties directly to the :host as CSS variables when they change
13
+ updated(changedProperties: Map<string | number | symbol, unknown>) {
14
+ super.updated(changedProperties);
15
+
16
+ if (changedProperties.has('width')) {
17
+ if (this.width) {
18
+ this.style.setProperty('--st-skeleton-width', this.width);
19
+ } else {
20
+ this.style.removeProperty('--st-skeleton-width');
21
+ }
22
+ }
23
+
24
+ if (changedProperties.has('height')) {
25
+ if (this.height) {
26
+ this.style.setProperty('--st-skeleton-height', this.height);
27
+ } else {
28
+ this.style.removeProperty('--st-skeleton-height');
29
+ }
30
+ }
31
+ }
32
+
16
33
  static styles = [
17
34
  resetStyles,
18
35
  css`
19
36
  :host {
20
37
  display: block;
21
- width: 100%;
38
+
39
+ /* Define base sizing variables that can be overridden by props */
40
+ --_width: var(--st-skeleton-width, 100%);
41
+ --_height: var(--st-skeleton-height, 1em);
42
+
43
+ /*
44
+ CRITICAL FIX: Apply sizing directly to the host.
45
+ This allows external classes and styles (like margin: auto) to work perfectly.
46
+ */
47
+ width: var(--_width);
48
+ height: var(--_height);
49
+
50
+ /* Expose radius as a token for external theme overrides */
51
+ --st-skeleton-radius: var(--st-radius-sm, 0.25rem);
52
+ }
53
+
54
+ /* Shape Variants adjust the host's sizing variables */
55
+ :host([variant="circular"]) {
56
+ --_width: var(--st-skeleton-width, 2.5rem);
57
+ --_height: var(--st-skeleton-height, 2.5rem);
58
+ }
59
+
60
+ :host([variant="rectangular"]) {
61
+ --_height: var(--st-skeleton-height, 8rem);
22
62
  }
23
63
 
24
64
  .skeleton {
25
- width: var(--st-skeleton-width, 100%);
26
- height: var(--st-skeleton-height, 1em);
65
+ /* The inner element simply fills the host */
66
+ display: block;
67
+ width: 100%;
68
+ height: 100%;
69
+
27
70
  background: linear-gradient(
28
71
  90deg,
29
72
  var(--st-color-surface-muted, #f1f5f9) 25%,
@@ -31,33 +74,57 @@ export class StSkeleton extends LitElement {
31
74
  var(--st-color-surface-muted, #f1f5f9) 63%
32
75
  );
33
76
  background-size: 400% 100%;
34
- animation: st-shimmer 1.4s ease infinite;
35
- border-radius: var(--st-radius-sm, 0.25rem);
77
+ border-radius: var(--st-skeleton-radius);
78
+
79
+ /* Prevent accidental selection */
80
+ user-select: none;
81
+ pointer-events: none;
36
82
  }
37
83
 
38
84
  :host([variant="circular"]) .skeleton {
39
85
  border-radius: 50%;
40
- height: var(--st-skeleton-height, 2.5rem);
41
- width: var(--st-skeleton-width, 2.5rem);
42
86
  }
43
87
 
44
- :host([variant="rectangular"]) .skeleton {
45
- height: var(--st-skeleton-height, 8rem);
88
+ /* Animations */
89
+ :host([animation="shimmer"]) .skeleton {
90
+ animation: st-shimmer 1.4s ease infinite;
91
+ }
92
+
93
+ :host([animation="pulse"]) .skeleton {
94
+ background: var(--st-color-surface-muted, #f1f5f9);
95
+ animation: st-pulse 1.5s ease-in-out infinite;
96
+ }
97
+
98
+ :host([animation="none"]) .skeleton {
99
+ background: var(--st-color-surface-muted, #f1f5f9);
100
+ animation: none;
101
+ }
102
+
103
+ /* Accessibility: Respect OS reduced motion settings */
104
+ @media (prefers-reduced-motion: reduce) {
105
+ .skeleton {
106
+ animation: none !important;
107
+ background: var(--st-color-surface-muted, #f1f5f9) !important;
108
+ }
46
109
  }
47
110
 
48
111
  @keyframes st-shimmer {
49
112
  0% { background-position: 100% 0; }
50
- 100% { background-position: 0 0; }
113
+ 100% { background-position: -100% 0; }
114
+ }
115
+
116
+ @keyframes st-pulse {
117
+ 0% { opacity: 1; }
118
+ 50% { opacity: 0.4; }
119
+ 100% { opacity: 1; }
51
120
  }
52
121
  `
53
122
  ];
54
123
 
55
124
  render() {
56
- const style = `
57
- ${this.width ? `--st-skeleton-width: ${this.width};` : ''}
58
- ${this.height ? `--st-skeleton-height: ${this.height};` : ''}
125
+ return html`
126
+ <div class="skeleton" aria-hidden="true" part="base"></div>
59
127
  `;
60
- return html`<div class="skeleton" style=${style}></div>`;
61
128
  }
62
129
  }
63
130
 
@@ -6,38 +6,58 @@ import { resetStyles } from '../../styles/reset.js';
6
6
  export class StNavGroup extends LitElement {
7
7
  @property({ type: String }) label = '';
8
8
  @property({ type: Boolean, reflect: true }) open = false;
9
+ @property({ type: Number }) depth = 0;
9
10
 
10
11
  static styles = [
11
12
  resetStyles,
12
13
  css`
13
14
  :host {
14
15
  display: block;
15
- margin-bottom: var(--st-spacing-sm);
16
+ margin-bottom: 2px;
17
+ overflow: hidden;
16
18
  }
17
19
 
18
20
  button {
19
21
  display: flex;
20
22
  align-items: center;
21
- justify-content: space-between;
23
+ gap: var(--st-spacing-sm, 8px);
22
24
  width: 100%;
25
+ box-sizing: border-box;
23
26
  background: transparent;
24
27
  border: none;
25
- padding: var(--st-spacing-xs) var(--st-spacing-lg);
28
+ padding: var(--st-spacing-sm) var(--st-spacing-xl);
29
+ padding-left: calc(var(--st-spacing-xl, 24px) + (var(--nav-depth, 0) * 16px));
26
30
  color: var(--st-color-text);
27
31
  font-size: var(--st-font-size-sm);
28
- font-weight: var(--st-font-weight-bold);
32
+ font-weight: var(--st-font-weight-semibold);
29
33
  cursor: pointer;
30
- text-align: left;
31
- transition: color 0.2s ease;
34
+ transition: background-color 0.2s ease, color 0.2s ease;
32
35
  }
33
36
 
34
37
  button:hover {
35
- color: var(--st-color-primary);
38
+ background-color: var(--st-color-surface-hover, rgba(0,0,0,0.03));
39
+ }
40
+
41
+ /* Styles for slotted icons */
42
+ ::slotted([slot="icon"]) {
43
+ width: 1rem;
44
+ height: 1rem;
45
+ flex-shrink: 0;
46
+ color: var(--st-color-text-muted);
36
47
  }
37
48
 
38
- svg {
49
+ .label {
50
+ flex: 1;
51
+ white-space: nowrap;
52
+ overflow: hidden;
53
+ text-overflow: ellipsis;
54
+ text-align: left;
55
+ }
56
+
57
+ svg.chevron {
39
58
  width: 1rem;
40
59
  height: 1rem;
60
+ flex-shrink: 0;
41
61
  fill: none;
42
62
  stroke: currentColor;
43
63
  stroke-width: 2;
@@ -46,12 +66,10 @@ export class StNavGroup extends LitElement {
46
66
  transition: transform 0.2s ease;
47
67
  }
48
68
 
49
- /* Rotate Chevron when open */
50
- :host([open]) svg {
69
+ :host([open]) svg.chevron {
51
70
  transform: rotate(180deg);
52
71
  }
53
72
 
54
- /* Modern CSS Grid Animation trick for accordion height */
55
73
  .drawer {
56
74
  display: grid;
57
75
  grid-template-rows: 0fr;
@@ -66,7 +84,6 @@ export class StNavGroup extends LitElement {
66
84
  overflow: hidden;
67
85
  display: flex;
68
86
  flex-direction: column;
69
- gap: 2px;
70
87
  }
71
88
  `
72
89
  ];
@@ -77,21 +94,23 @@ export class StNavGroup extends LitElement {
77
94
 
78
95
  render() {
79
96
  return html`
80
- <button @click=${this._toggle} aria-expanded=${this.open}>
81
- ${this.label}
82
- <svg viewBox="0 0 24 24">
83
- <path d="M6 9l6 6 6-6"></path>
84
- </svg>
97
+ <button @click=${this._toggle} aria-expanded=${this.open} style="--nav-depth: ${this.depth}">
98
+ <slot name="icon"></slot>
99
+ <span class="label">${this.label}</span>
100
+ <svg class="chevron" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"></path></svg>
85
101
  </button>
86
102
 
87
103
  <div class="drawer">
88
104
  <div class="drawer-content">
89
- <!-- We add slight padding top/bottom so the inner links breathe -->
90
- <div style="padding: 4px 0;">
91
- <slot></slot>
92
- </div>
105
+ <slot></slot>
93
106
  </div>
94
107
  </div>
95
108
  `;
96
109
  }
110
+ }
111
+
112
+ declare global {
113
+ interface HTMLElementTagNameMap {
114
+ 'st-vertical-nav-group': StNavGroup;
115
+ }
97
116
  }
@@ -6,45 +6,68 @@ import { resetStyles } from '../../styles/reset.js';
6
6
  export class StNavItem extends LitElement {
7
7
  @property({ type: String }) href = '#';
8
8
  @property({ type: Boolean, reflect: true }) active = false;
9
+ @property({ type: Number }) depth = 0;
9
10
 
10
11
  static styles = [
11
12
  resetStyles,
12
13
  css`
13
14
  :host {
14
15
  display: block;
16
+ overflow: hidden;
15
17
  }
16
18
 
17
19
  a {
18
20
  display: flex;
19
21
  align-items: center;
20
- padding: 6px var(--st-spacing-lg);
22
+ gap: var(--st-spacing-sm, 8px);
23
+ width: 100%;
24
+ box-sizing: border-box;
25
+ padding: 8px var(--st-spacing-xl);
26
+ padding-left: calc(var(--st-spacing-xl, 24px) + (var(--nav-depth, 0) * 16px));
21
27
  color: var(--st-color-text-muted);
22
28
  font-size: var(--st-font-size-sm);
23
29
  text-decoration: none;
24
- border-left: 2px solid transparent;
30
+ border-left: 3px solid transparent;
25
31
  transition: all 0.15s ease;
32
+
33
+ white-space: nowrap;
34
+ overflow: hidden;
35
+ text-overflow: ellipsis;
26
36
  }
27
37
 
28
38
  a:hover {
29
39
  color: var(--st-color-text);
30
- background-color: var(--st-color-surface);
40
+ background-color: var(--st-color-surface-hover, rgba(0,0,0,0.03));
41
+ }
42
+
43
+ ::slotted([slot="icon"]) {
44
+ width: 1rem;
45
+ height: 1rem;
46
+ flex-shrink: 0;
47
+ color: inherit;
31
48
  }
32
49
 
33
- /* Active state: Purple text with a left border indicator */
34
50
  :host([active]) a {
35
51
  color: var(--st-color-primary);
36
52
  border-left-color: var(--st-color-primary);
37
- font-weight: var(--st-font-weight-medium);
38
- background-color: transparent;
53
+ font-weight: var(--st-font-weight-bold);
54
+ background-color: var(--st-color-primary-light, rgba(98, 0, 238, 0.05));
39
55
  }
40
56
  `
41
57
  ];
42
58
 
43
59
  render() {
44
60
  return html`
45
- <a href=${this.href} part="link">
61
+ <a href=${this.href} part="link" style="--nav-depth: ${this.depth}">
62
+ <slot name="icon"></slot>
46
63
  <slot></slot>
47
64
  </a>
48
65
  `;
49
66
  }
67
+ }
68
+
69
+ declare global {
70
+ interface HTMLElementTagNameMap {
71
+ 'st-vertical-nav-item': StNavItem;
72
+ }
50
73
  }