@sondor-technology/sondor-components 1.0.0-1165-initial-setup.11 → 1.0.1-develop.10
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/dist/components/checkbox/checkbox.d.ts +3 -0
- package/dist/components/checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/checkbox/checkbox.js +38 -20
- package/dist/components/divider/divider.js +1 -1
- package/dist/components/field/field.js +1 -1
- package/dist/components/form/form.d.ts.map +1 -1
- package/dist/components/form/form.js +33 -27
- package/dist/components/icon/icon-paths.d.ts +16 -0
- package/dist/components/icon/icon-paths.d.ts.map +1 -1
- package/dist/components/icon/icon-paths.js +16 -0
- package/dist/components/icon/icon.d.ts +2 -1
- package/dist/components/icon/icon.d.ts.map +1 -1
- package/dist/components/link/link.js +1 -1
- package/dist/components/modal/modal.d.ts +11 -0
- package/dist/components/modal/modal.d.ts.map +1 -1
- package/dist/components/modal/modal.js +103 -15
- package/dist/components/router/router.d.ts +27 -3
- package/dist/components/router/router.d.ts.map +1 -1
- package/dist/components/router/router.js +72 -12
- package/dist/components/select/select.js +1 -1
- package/dist/components/skeleton/skeleton.d.ts +2 -3
- package/dist/components/skeleton/skeleton.d.ts.map +1 -1
- package/dist/components/skeleton/skeleton.js +89 -18
- package/dist/components/vertical-nav/vertical-nav-group.d.ts +6 -0
- package/dist/components/vertical-nav/vertical-nav-group.d.ts.map +1 -1
- package/dist/components/vertical-nav/vertical-nav-group.js +37 -21
- package/dist/components/vertical-nav/vertical-nav-item.d.ts +6 -0
- package/dist/components/vertical-nav/vertical-nav-item.d.ts.map +1 -1
- package/dist/components/vertical-nav/vertical-nav-item.js +27 -7
- package/package.json +5 -1
- package/src/components/checkbox/checkbox.ts +54 -26
- package/src/components/divider/divider.ts +1 -1
- package/src/components/field/field.ts +1 -1
- package/src/components/form/form.ts +39 -30
- package/src/components/icon/icon-paths.ts +16 -0
- package/src/components/icon/icon.ts +3 -1
- package/src/components/link/link.ts +1 -1
- package/src/components/modal/modal.ts +103 -15
- package/src/components/router/router.ts +99 -12
- package/src/components/select/select.ts +1 -1
- package/src/components/skeleton/skeleton.ts +87 -20
- package/src/components/vertical-nav/vertical-nav-group.ts +40 -21
- package/src/components/vertical-nav/vertical-nav-item.ts +30 -7
|
@@ -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
|
-
|
|
33
|
-
|
|
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.
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
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:
|
|
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();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit';
|
|
2
2
|
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
-
import { resetStyles } from '../../styles/reset';
|
|
3
|
+
import { resetStyles } from '../../styles/reset.js';
|
|
4
4
|
|
|
5
5
|
@customElement('st-select')
|
|
6
6
|
export class StSelect extends LitElement {
|
|
@@ -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
|
-
|
|
8
|
-
@property({ type: String })
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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:
|
|
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
|
-
|
|
57
|
-
|
|
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:
|
|
16
|
+
margin-bottom: 2px;
|
|
17
|
+
overflow: hidden;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
button {
|
|
19
21
|
display: flex;
|
|
20
22
|
align-items: center;
|
|
21
|
-
|
|
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-
|
|
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-
|
|
32
|
+
font-weight: var(--st-font-weight-semibold);
|
|
29
33
|
cursor: pointer;
|
|
30
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
<
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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-
|
|
38
|
-
background-color:
|
|
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
|
}
|