@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.
- 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/form/form-step.d.ts +1 -0
- package/dist/components/form/form-step.d.ts.map +1 -1
- package/dist/components/form/form-step.js +3 -0
- package/dist/components/form/form.d.ts +7 -2
- package/dist/components/form/form.d.ts.map +1 -1
- package/dist/components/form/form.js +157 -59
- 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/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/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/form/form-step.ts +5 -1
- package/src/components/form/form.ts +182 -66
- package/src/components/icon/icon-paths.ts +16 -0
- package/src/components/icon/icon.ts +3 -1
- package/src/components/modal/modal.ts +103 -15
- package/src/components/router/router.ts +99 -12
- 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,5 +1,5 @@
|
|
|
1
|
-
import {LitElement, html, css} from "lit";
|
|
2
|
-
import {customElement, property, state,
|
|
1
|
+
import {LitElement, html, css, PropertyValues} from "lit";
|
|
2
|
+
import {customElement, property, state, query} from "lit/decorators.js";
|
|
3
3
|
import {resetStyles} from "../../styles/reset.js";
|
|
4
4
|
import type {StFormStep} from "./form-step.js";
|
|
5
5
|
|
|
@@ -13,11 +13,14 @@ export class StForm extends LitElement {
|
|
|
13
13
|
|
|
14
14
|
@state() private _currentStepIndex = 0;
|
|
15
15
|
|
|
16
|
-
@queryAssignedElements({selector: "st-form-step"})
|
|
17
|
-
private _stepElements!: StFormStep[];
|
|
18
|
-
|
|
19
16
|
@query("form") private _formEl!: HTMLFormElement;
|
|
20
17
|
|
|
18
|
+
private _modalFooterEl: HTMLElement | null = null;
|
|
19
|
+
|
|
20
|
+
private get _steps(): StFormStep[] {
|
|
21
|
+
return Array.from(this.querySelectorAll("st-form-step"));
|
|
22
|
+
}
|
|
23
|
+
|
|
21
24
|
static styles = [
|
|
22
25
|
resetStyles,
|
|
23
26
|
css`
|
|
@@ -31,9 +34,6 @@ export class StForm extends LitElement {
|
|
|
31
34
|
gap: var(--st-spacing-md);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
/* ==========================================================
|
|
35
|
-
Layouts
|
|
36
|
-
========================================================== */
|
|
37
37
|
:host([layout="vertical"]) form {
|
|
38
38
|
flex-direction: column;
|
|
39
39
|
}
|
|
@@ -55,9 +55,6 @@ export class StForm extends LitElement {
|
|
|
55
55
|
flex-direction: column;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/* ==========================================================
|
|
59
|
-
Progress Navigation
|
|
60
|
-
========================================================== */
|
|
61
58
|
.progress-nav {
|
|
62
59
|
display: flex;
|
|
63
60
|
align-items: center;
|
|
@@ -121,9 +118,6 @@ export class StForm extends LitElement {
|
|
|
121
118
|
background-color: var(--st-color-success);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
/* ==========================================================
|
|
125
|
-
Actions & States
|
|
126
|
-
========================================================== */
|
|
127
121
|
.form-actions {
|
|
128
122
|
display: flex;
|
|
129
123
|
gap: var(--st-spacing-md);
|
|
@@ -162,42 +156,165 @@ export class StForm extends LitElement {
|
|
|
162
156
|
`,
|
|
163
157
|
];
|
|
164
158
|
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
override connectedCallback() {
|
|
160
|
+
super.connectedCallback();
|
|
161
|
+
this.updateComplete.then(() => {
|
|
167
162
|
this._updateStepVisibility();
|
|
163
|
+
this._updateModalFooter();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override disconnectedCallback() {
|
|
168
|
+
super.disconnectedCallback();
|
|
169
|
+
if (this._modalFooterEl) {
|
|
170
|
+
this._modalFooterEl.remove();
|
|
171
|
+
this._modalFooterEl = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
override updated(changedProperties: PropertyValues<this>) {
|
|
176
|
+
super.updated(changedProperties);
|
|
177
|
+
this._updateModalFooter();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private _updateModalFooter() {
|
|
181
|
+
const modal = this.closest("st-modal");
|
|
182
|
+
if (modal) {
|
|
183
|
+
if (!this._modalFooterEl) {
|
|
184
|
+
this._modalFooterEl = document.createElement("div");
|
|
185
|
+
this._modalFooterEl.setAttribute("slot", "footer");
|
|
186
|
+
this._modalFooterEl.style.display = "flex";
|
|
187
|
+
this._modalFooterEl.style.gap = "var(--st-spacing-md)";
|
|
188
|
+
this._modalFooterEl.style.width = "100%";
|
|
189
|
+
this._modalFooterEl.style.justifyContent = "flex-end";
|
|
190
|
+
this._modalFooterEl.style.alignItems = "center";
|
|
191
|
+
modal.appendChild(this._modalFooterEl);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const steps = this._steps;
|
|
195
|
+
const hasSteps = steps.length > 0;
|
|
196
|
+
const isLastStep = hasSteps ? this._currentStepIndex === steps.length - 1 : true;
|
|
197
|
+
|
|
198
|
+
// Imperative DOM management prevents event listener loss and re-rendering conflicts
|
|
199
|
+
this._modalFooterEl.innerHTML = "";
|
|
200
|
+
|
|
201
|
+
if (hasSteps && this._currentStepIndex > 0) {
|
|
202
|
+
const backBtn = document.createElement("st-button");
|
|
203
|
+
backBtn.setAttribute("variant", "secondary");
|
|
204
|
+
backBtn.setAttribute("type", "button");
|
|
205
|
+
if (this.loading) backBtn.setAttribute("disabled", "");
|
|
206
|
+
backBtn.textContent = "Back";
|
|
207
|
+
backBtn.addEventListener("click", (e) => {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
this._prevStep();
|
|
210
|
+
});
|
|
211
|
+
this._modalFooterEl.appendChild(backBtn);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const submitBtn = document.createElement("st-button");
|
|
215
|
+
submitBtn.setAttribute("color", "primary");
|
|
216
|
+
submitBtn.setAttribute("type", "button");
|
|
217
|
+
if (this.loading) submitBtn.setAttribute("disabled", "");
|
|
218
|
+
submitBtn.textContent = this.loading ? "Submitting..." : (isLastStep ? "Submit" : "Continue");
|
|
219
|
+
submitBtn.addEventListener("click", (e) => {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
this._triggerSubmission();
|
|
222
|
+
});
|
|
223
|
+
this._modalFooterEl.appendChild(submitBtn);
|
|
224
|
+
|
|
225
|
+
} else if (this._modalFooterEl) {
|
|
226
|
+
this._modalFooterEl.remove();
|
|
227
|
+
this._modalFooterEl = null;
|
|
168
228
|
}
|
|
169
229
|
}
|
|
170
230
|
|
|
231
|
+
private _handleSlotChange() {
|
|
232
|
+
this._updateStepVisibility();
|
|
233
|
+
this._updateModalFooter();
|
|
234
|
+
}
|
|
235
|
+
|
|
171
236
|
private _updateStepVisibility() {
|
|
172
|
-
|
|
237
|
+
const steps = this._steps;
|
|
238
|
+
steps.forEach((step, index) => {
|
|
173
239
|
step.active = index === this._currentStepIndex;
|
|
174
240
|
});
|
|
175
241
|
this.requestUpdate();
|
|
176
242
|
}
|
|
177
243
|
|
|
178
|
-
private _validateCurrentContext(): boolean {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
244
|
+
private async _validateCurrentContext(): Promise<boolean> {
|
|
245
|
+
const steps = this._steps;
|
|
246
|
+
const scope: ParentNode = steps.length > 0
|
|
247
|
+
? steps[this._currentStepIndex]
|
|
248
|
+
: this;
|
|
182
249
|
|
|
183
|
-
const currentStep = this._stepElements[this._currentStepIndex];
|
|
184
250
|
let isValid = true;
|
|
185
251
|
|
|
186
|
-
// Find all custom and native form inputs assigned to the current step
|
|
187
252
|
const inputs = Array.from(
|
|
188
|
-
|
|
189
|
-
HTMLElement & {
|
|
190
|
-
|
|
253
|
+
scope.querySelectorAll<
|
|
254
|
+
HTMLElement & {
|
|
255
|
+
checkValidity?: () => boolean;
|
|
256
|
+
reportValidity?: () => boolean;
|
|
257
|
+
required?: boolean;
|
|
258
|
+
value?: any;
|
|
259
|
+
checked?: boolean;
|
|
260
|
+
invalid?: boolean;
|
|
261
|
+
}
|
|
262
|
+
>("st-input, st-select, st-textarea, st-checkbox, st-radio, input, select, textarea"),
|
|
191
263
|
);
|
|
192
|
-
|
|
264
|
+
|
|
193
265
|
for (const input of inputs) {
|
|
266
|
+
if (input.hasAttribute("disabled") || (input as any).disabled) continue;
|
|
267
|
+
|
|
268
|
+
let fieldValid = true;
|
|
269
|
+
|
|
194
270
|
if (typeof input.checkValidity === "function") {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
271
|
+
fieldValid = input.checkValidity();
|
|
272
|
+
} else {
|
|
273
|
+
const isRequired = input.hasAttribute("required") || input.required;
|
|
274
|
+
if (isRequired) {
|
|
275
|
+
const isToggle = input.tagName === "ST-CHECKBOX" || input.tagName === "ST-RADIO";
|
|
276
|
+
const val = isToggle ? input.checked : input.value;
|
|
277
|
+
if (val === undefined || val === null || val === "" || val === false) {
|
|
278
|
+
fieldValid = false;
|
|
199
279
|
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!fieldValid) {
|
|
284
|
+
if (typeof input.reportValidity === "function") {
|
|
285
|
+
input.reportValidity();
|
|
286
|
+
} else if ("invalid" in input) {
|
|
287
|
+
input.invalid = true;
|
|
288
|
+
}
|
|
289
|
+
if (isValid && typeof (input as any).focus === "function") {
|
|
290
|
+
(input as any).focus();
|
|
291
|
+
}
|
|
292
|
+
isValid = false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (steps.length === 0 && this._formEl) {
|
|
297
|
+
if (!this._formEl.checkValidity()) {
|
|
298
|
+
isValid = false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (isValid && steps.length > 0) {
|
|
303
|
+
const currentStep = steps[this._currentStepIndex] as StFormStep & {
|
|
304
|
+
validate?: () => Promise<boolean> | boolean;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (typeof currentStep.validate === "function") {
|
|
308
|
+
try {
|
|
309
|
+
this.loading = true;
|
|
310
|
+
const stepPassed = await currentStep.validate();
|
|
311
|
+
if (!stepPassed) {
|
|
312
|
+
isValid = false;
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
200
315
|
isValid = false;
|
|
316
|
+
} finally {
|
|
317
|
+
this.loading = false;
|
|
201
318
|
}
|
|
202
319
|
}
|
|
203
320
|
}
|
|
@@ -206,7 +323,6 @@ export class StForm extends LitElement {
|
|
|
206
323
|
}
|
|
207
324
|
|
|
208
325
|
private _triggerSubmission() {
|
|
209
|
-
// HTML5 method that triggers form validation and fires the native submit event
|
|
210
326
|
this._formEl.requestSubmit();
|
|
211
327
|
}
|
|
212
328
|
|
|
@@ -220,13 +336,11 @@ export class StForm extends LitElement {
|
|
|
220
336
|
private _extractFormData(): FormData {
|
|
221
337
|
const formData = new FormData();
|
|
222
338
|
|
|
223
|
-
// Query all light-DOM children that have a 'name' attribute
|
|
224
339
|
const fields = this.querySelectorAll<
|
|
225
340
|
HTMLElement & {name: string; value: any; checked?: boolean; files?: FileList}
|
|
226
341
|
>("[name]");
|
|
227
342
|
|
|
228
343
|
fields.forEach((field) => {
|
|
229
|
-
// Standard HTML form behavior: ignore disabled fields
|
|
230
344
|
if (field.hasAttribute("disabled")) return;
|
|
231
345
|
|
|
232
346
|
const isToggle =
|
|
@@ -236,7 +350,6 @@ export class StForm extends LitElement {
|
|
|
236
350
|
field.getAttribute("type") === "checkbox";
|
|
237
351
|
|
|
238
352
|
if (isToggle) {
|
|
239
|
-
// Only append radios/checkboxes if they are actively checked
|
|
240
353
|
if (field.checked) {
|
|
241
354
|
formData.append(field.name, field.value || "on");
|
|
242
355
|
}
|
|
@@ -245,10 +358,8 @@ export class StForm extends LitElement {
|
|
|
245
358
|
field.getAttribute("type") === "file" &&
|
|
246
359
|
field.files
|
|
247
360
|
) {
|
|
248
|
-
// Handle native file uploads
|
|
249
361
|
Array.from(field.files).forEach((file) => formData.append(field.name, file));
|
|
250
362
|
} else if (field.value !== undefined) {
|
|
251
|
-
// Handle st-input, st-select, st-textarea, and standard inputs
|
|
252
363
|
formData.append(field.name, field.value);
|
|
253
364
|
}
|
|
254
365
|
});
|
|
@@ -256,14 +367,16 @@ export class StForm extends LitElement {
|
|
|
256
367
|
return formData;
|
|
257
368
|
}
|
|
258
369
|
|
|
259
|
-
private _handleSubmit(e: Event) {
|
|
370
|
+
private async _handleSubmit(e: Event) {
|
|
260
371
|
e.preventDefault();
|
|
261
372
|
e.stopPropagation();
|
|
262
373
|
|
|
263
|
-
|
|
374
|
+
const isValid = await this._validateCurrentContext();
|
|
375
|
+
if (!isValid) return;
|
|
264
376
|
|
|
265
|
-
const
|
|
266
|
-
const
|
|
377
|
+
const steps = this._steps;
|
|
378
|
+
const hasSteps = steps.length > 0;
|
|
379
|
+
const isLastStep = hasSteps ? this._currentStepIndex === steps.length - 1 : true;
|
|
267
380
|
|
|
268
381
|
if (hasSteps && !isLastStep) {
|
|
269
382
|
this._currentStepIndex++;
|
|
@@ -271,7 +384,6 @@ export class StForm extends LitElement {
|
|
|
271
384
|
return;
|
|
272
385
|
}
|
|
273
386
|
|
|
274
|
-
// Use our custom extractor to pierce the Shadow DOM boundary
|
|
275
387
|
const formData = this._extractFormData();
|
|
276
388
|
|
|
277
389
|
this.dispatchEvent(
|
|
@@ -285,8 +397,9 @@ export class StForm extends LitElement {
|
|
|
285
397
|
}
|
|
286
398
|
|
|
287
399
|
render() {
|
|
288
|
-
const
|
|
289
|
-
const
|
|
400
|
+
const steps = this._steps;
|
|
401
|
+
const hasSteps = steps.length > 0;
|
|
402
|
+
const isInModal = !!this.closest("st-modal");
|
|
290
403
|
|
|
291
404
|
return html`
|
|
292
405
|
<div class="${hasSteps ? "has-steps" : ""}" aria-busy=${this.loading}>
|
|
@@ -300,7 +413,7 @@ export class StForm extends LitElement {
|
|
|
300
413
|
${hasSteps
|
|
301
414
|
? html`
|
|
302
415
|
<div class="progress-nav" role="tablist">
|
|
303
|
-
${
|
|
416
|
+
${steps.map((step, index) => {
|
|
304
417
|
const isActive = index === this._currentStepIndex;
|
|
305
418
|
const isCompleted = index < this._currentStepIndex;
|
|
306
419
|
|
|
@@ -319,7 +432,7 @@ export class StForm extends LitElement {
|
|
|
319
432
|
</div>
|
|
320
433
|
<span>${step.title || `Step ${index + 1}`}</span>
|
|
321
434
|
</div>
|
|
322
|
-
${index <
|
|
435
|
+
${index < steps.length - 1
|
|
323
436
|
? html` <div class="step-connector ${isCompleted ? "completed" : ""}"></div> `
|
|
324
437
|
: ""}
|
|
325
438
|
`;
|
|
@@ -339,30 +452,33 @@ export class StForm extends LitElement {
|
|
|
339
452
|
<form @submit=${this._handleSubmit}>
|
|
340
453
|
<slot @slotchange=${this._handleSlotChange}></slot>
|
|
341
454
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
455
|
+
${!isInModal
|
|
456
|
+
? html`
|
|
457
|
+
<div class="form-actions">
|
|
458
|
+
${hasSteps && this._currentStepIndex > 0
|
|
459
|
+
? html`
|
|
460
|
+
<st-button
|
|
461
|
+
variant="secondary"
|
|
462
|
+
type="button"
|
|
463
|
+
@click=${this._prevStep}
|
|
464
|
+
?disabled=${this.loading}
|
|
465
|
+
>
|
|
466
|
+
Back
|
|
467
|
+
</st-button>
|
|
468
|
+
`
|
|
469
|
+
: ""}
|
|
470
|
+
|
|
345
471
|
<st-button
|
|
346
|
-
|
|
472
|
+
color="primary"
|
|
347
473
|
type="button"
|
|
348
|
-
@click=${this.
|
|
474
|
+
@click=${this._triggerSubmission}
|
|
349
475
|
?disabled=${this.loading}
|
|
350
476
|
>
|
|
351
|
-
|
|
477
|
+
${this.loading ? "Submitting..." : hasSteps && this._currentStepIndex === steps.length - 1 ? "Submit" : hasSteps ? "Continue" : "Submit"}
|
|
352
478
|
</st-button>
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<!-- Type is "button", but click triggers requestSubmit() on the internal form -->
|
|
357
|
-
<st-button
|
|
358
|
-
color="primary"
|
|
359
|
-
type="button"
|
|
360
|
-
@click=${this._triggerSubmission}
|
|
361
|
-
?disabled=${this.loading}
|
|
362
|
-
>
|
|
363
|
-
${this.loading ? "Submitting..." : isLastStep ? "Submit" : "Continue"}
|
|
364
|
-
</st-button>
|
|
365
|
-
</div>
|
|
479
|
+
</div>
|
|
480
|
+
`
|
|
481
|
+
: ""}
|
|
366
482
|
</form>
|
|
367
483
|
</div>
|
|
368
484
|
`;
|
|
@@ -373,4 +489,4 @@ declare global {
|
|
|
373
489
|
interface HTMLElementTagNameMap {
|
|
374
490
|
"st-form": StForm;
|
|
375
491
|
}
|
|
376
|
-
}
|
|
492
|
+
}
|
|
@@ -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;
|
|
@@ -3,10 +3,12 @@ import { customElement, property } from 'lit/decorators.js';
|
|
|
3
3
|
import { iconPaths, IconName } from './icon-paths.js';
|
|
4
4
|
import { resetStyles } from '../../styles/reset.js';
|
|
5
5
|
|
|
6
|
+
export type IconVariant = 'outline' | 'solid';
|
|
7
|
+
|
|
6
8
|
@customElement('st-icon')
|
|
7
9
|
export class StIcon extends LitElement {
|
|
8
10
|
@property({ type: String }) name: IconName | string = '';
|
|
9
|
-
@property({ type: String }) variant:
|
|
11
|
+
@property({ type: String }) variant: IconVariant = 'outline';
|
|
10
12
|
|
|
11
13
|
// Semantic Colors
|
|
12
14
|
@property({ type: String, reflect: true }) color: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' = 'default';
|
|
@@ -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>
|