@latty-ds/web 0.10.4 → 1.0.0
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/custom-elements.json +1969 -228
- package/dist/components/accordion/index.js +36 -2
- package/dist/components/alert/alert.d.ts +29 -6
- package/dist/components/alert/alert.types.d.ts +5 -0
- package/dist/components/alert/index.js +124 -17
- package/dist/components/avatar/avatar.d.ts +2 -0
- package/dist/components/avatar/index.js +10 -3
- package/dist/components/breadcrumb/breadcrumb.d.ts +4 -0
- package/dist/components/breadcrumb/breadcrumb.types.d.ts +5 -0
- package/dist/components/breadcrumb/index.js +20 -3
- package/dist/components/button/button.d.ts +33 -0
- package/dist/components/button/index.js +135 -12
- package/dist/components/calendar/calendar.d.ts +15 -7
- package/dist/components/calendar/calendar.types.d.ts +17 -1
- package/dist/components/calendar/index.js +78 -25
- package/dist/components/checkbox/checkbox.d.ts +14 -2
- package/dist/components/checkbox/checkbox.types.d.ts +5 -0
- package/dist/components/checkbox/index.js +82 -5
- package/dist/components/chip/chip.d.ts +4 -1
- package/dist/components/chip/chip.types.d.ts +5 -0
- package/dist/components/chip/index.js +29 -1
- package/dist/components/color-input/color-input.d.ts +11 -2
- package/dist/components/color-input/color-input.types.d.ts +5 -0
- package/dist/components/color-input/index.js +92 -8
- package/dist/components/combobox/combobox.d.ts +18 -2
- package/dist/components/combobox/combobox.types.d.ts +7 -0
- package/dist/components/combobox/index.js +164 -23
- package/dist/components/date-input/date-input.d.ts +22 -7
- package/dist/components/date-input/date-input.types.d.ts +7 -0
- package/dist/components/date-input/index.js +216 -53
- package/dist/components/datepicker/datepicker.d.ts +9 -1
- package/dist/components/datepicker/index.js +108 -15
- package/dist/components/dialog/dialog.d.ts +39 -19
- package/dist/components/dialog/dialog.types.d.ts +5 -0
- package/dist/components/dialog/index.js +237 -90
- package/dist/components/divider/index.js +2 -2
- package/dist/components/dropdown/dropdown.d.ts +4 -0
- package/dist/components/dropdown/index.js +43 -3
- package/dist/components/icon-button/icon-button.d.ts +4 -0
- package/dist/components/icon-button/index.js +36 -1
- package/dist/components/image/image.d.ts +7 -7
- package/dist/components/image/index.js +6 -11
- package/dist/components/list/index.js +14 -4
- package/dist/components/nav/index.js +37 -17
- package/dist/components/nav/nav-item.d.ts +8 -8
- package/dist/components/pagination/index.js +57 -16
- package/dist/components/pagination/pagination.d.ts +8 -1
- package/dist/components/pagination/pagination.types.d.ts +15 -0
- package/dist/components/progress/index.js +44 -6
- package/dist/components/progress/progress.d.ts +12 -0
- package/dist/components/radio/index.js +148 -9
- package/dist/components/radio/radio.d.ts +34 -2
- package/dist/components/radio/radio.types.d.ts +5 -0
- package/dist/components/radio-group/index.js +120 -29
- package/dist/components/radio-group/radio-group.d.ts +27 -2
- package/dist/components/select/index.js +197 -44
- package/dist/components/select/select.d.ts +38 -4
- package/dist/components/select/select.types.d.ts +5 -0
- package/dist/components/shared/aria-forwarding.d.ts +1 -1
- package/dist/components/shared/form-default.d.ts +30 -0
- package/dist/components/shared/form-disabled.d.ts +40 -0
- package/dist/components/shared/form-field.d.ts +7 -0
- package/dist/components/shared/form-validity.d.ts +17 -0
- package/dist/components/shared/roving.d.ts +11 -0
- package/dist/components/shared/scroll-lock.d.ts +2 -0
- package/dist/components/sidepanel/index.js +275 -72
- package/dist/components/sidepanel/sidepanel.d.ts +32 -10
- package/dist/components/sidepanel/sidepanel.types.d.ts +10 -1
- package/dist/components/skeleton/index.js +3 -8
- package/dist/components/slider/index.js +307 -75
- package/dist/components/slider/slider.d.ts +16 -3
- package/dist/components/snackbar/index.js +82 -9
- package/dist/components/snackbar/snackbar.d.ts +17 -1
- package/dist/components/snackbar/snackbar.types.d.ts +5 -0
- package/dist/components/surface/index.js +8 -0
- package/dist/components/switch/index.js +101 -5
- package/dist/components/switch/switch.d.ts +13 -2
- package/dist/components/switch/switch.types.d.ts +5 -0
- package/dist/components/tab/index.js +15 -14
- package/dist/components/tab/tab.d.ts +0 -1
- package/dist/components/tab-group/index.js +102 -30
- package/dist/components/tab-group/tab-group.d.ts +17 -2
- package/dist/components/table/index.js +105 -61
- package/dist/components/table/table.d.ts +8 -2
- package/dist/components/table/table.types.d.ts +4 -3
- package/dist/components/text/index.js +16 -1
- package/dist/components/text/text.d.ts +1 -1
- package/dist/components/textfield/index.js +243 -35
- package/dist/components/textfield/textfield.d.ts +24 -2
- package/dist/components/textfield/textfield.types.d.ts +11 -0
- package/dist/components/tooltip/index.js +176 -49
- package/dist/components/tooltip/tooltip.d.ts +17 -0
- package/dist/components/tooltip/tooltip.types.d.ts +5 -1
- package/dist/index.cjs +2458 -683
- package/dist/index.js +2469 -690
- package/dist/manifest.json +202 -32
- package/dist/utils/dev-warn.d.ts +5 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +3 -7
|
@@ -10,7 +10,7 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/components/sidepanel/sidepanel.ts
|
|
13
|
-
import { html as html3 } from "lit";
|
|
13
|
+
import { html as html3, nothing as nothing2 } from "lit";
|
|
14
14
|
|
|
15
15
|
// src/base/themeable-element.ts
|
|
16
16
|
import { LitElement } from "lit";
|
|
@@ -35,80 +35,145 @@ __decorateClass([
|
|
|
35
35
|
], ThemeableElement.prototype, "theme", 2);
|
|
36
36
|
|
|
37
37
|
// src/components/sidepanel/sidepanel.ts
|
|
38
|
-
import { customElement as customElement3, property as property4 } from "lit/decorators.js";
|
|
38
|
+
import { customElement as customElement3, property as property4, query } from "lit/decorators.js";
|
|
39
39
|
|
|
40
40
|
// src/components/sidepanel/sidepanel.styles.ts
|
|
41
41
|
import { css } from "lit";
|
|
42
42
|
var sidepanelStyles = css`
|
|
43
43
|
:host {
|
|
44
44
|
display: contents;
|
|
45
|
+
/* translateX/box-shadow offsets have no logical-property equivalent, unlike the inset-inline-*
|
|
46
|
+
positioning below (which resolves automatically) — these carry the direction-dependent sign instead,
|
|
47
|
+
flipped under :dir(rtl), so the off-screen slide direction and shadow side both stay correct without
|
|
48
|
+
any JS reading computed direction at all. */
|
|
49
|
+
--_start-x: -100%;
|
|
50
|
+
--_end-x: 100%;
|
|
51
|
+
--_dir-sign: 1;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
:host(:dir(rtl)) {
|
|
55
|
+
--_start-x: 100%;
|
|
56
|
+
--_end-x: -100%;
|
|
57
|
+
--_dir-sign: -1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ── Panel — the native <dialog> itself now provides the overlay too, via ::backdrop; a closed dialog is
|
|
61
|
+
display:none automatically, so unlike the old transform-only-hidden div, a closed panel is genuinely
|
|
62
|
+
non-interactive and out of the accessibility tree, with no extra JS needed (SPL-1). ────────────────────── */
|
|
48
63
|
|
|
49
|
-
|
|
64
|
+
dialog {
|
|
65
|
+
background: var(--lt-bg-default);
|
|
66
|
+
border: none;
|
|
67
|
+
color: var(--lt-text-default);
|
|
68
|
+
margin: 0;
|
|
69
|
+
max-height: none;
|
|
70
|
+
max-width: none;
|
|
71
|
+
padding: 0;
|
|
50
72
|
position: fixed;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
transition:
|
|
73
|
+
/* display is deliberately NOT set here — a closed dialog must stay display:none (the native default) for
|
|
74
|
+
SPL-1's fix to actually hold (genuinely non-interactive, out of the accessibility tree); it's set to flex
|
|
75
|
+
only for the open state, below. display/overlay need transition-behavior: allow-discrete so close()
|
|
76
|
+
doesn't remove the panel (and its ::backdrop) from rendering instantly, cutting the slide-out transform
|
|
77
|
+
transition off partway through — same reasoning as lt-dialog. */
|
|
78
|
+
transition:
|
|
79
|
+
transform 250ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
80
|
+
display 250ms allow-discrete,
|
|
81
|
+
overlay 250ms allow-discrete;
|
|
57
82
|
}
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
dialog::backdrop {
|
|
85
|
+
background: rgba(0, 0, 0, 0);
|
|
86
|
+
transition:
|
|
87
|
+
background-color 200ms ease,
|
|
88
|
+
display 250ms allow-discrete,
|
|
89
|
+
overlay 250ms allow-discrete;
|
|
90
|
+
}
|
|
60
91
|
|
|
61
|
-
[
|
|
62
|
-
|
|
63
|
-
background: var(--lt-bg-default);
|
|
64
|
-
display: flex;
|
|
65
|
-
flex-direction: column;
|
|
66
|
-
z-index: 401;
|
|
67
|
-
pointer-events: none;
|
|
68
|
-
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
92
|
+
dialog[open]::backdrop {
|
|
93
|
+
background: rgba(0, 0, 0, var(--lt-sidepanel-overlay-opacity, 0.45));
|
|
69
94
|
}
|
|
70
95
|
|
|
71
|
-
/* ── Anchors —
|
|
96
|
+
/* ── Anchors — off-screen positions; dialog[open] (below) is the only thing that moves it on-screen. Fully
|
|
97
|
+
explicit inset-inline-start/end/top/bottom (not inset-block/inline shorthand, and not omitting the
|
|
98
|
+
opposite side to fall back to "auto") and an explicit height/width — a native <dialog>, even
|
|
99
|
+
position:fixed, does not stretch to fill top:0/bottom:0 the way a plain block box would; confirmed
|
|
100
|
+
empirically, it needs an explicit dimension on the axis inset alone doesn't constrain. ────────────────── */
|
|
72
101
|
|
|
73
|
-
:host([anchor='
|
|
74
|
-
:host(:not([anchor]))
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
:host([anchor='start']) dialog,
|
|
103
|
+
:host(:not([anchor])) dialog {
|
|
104
|
+
top: 0;
|
|
105
|
+
inset-inline-end: auto;
|
|
106
|
+
bottom: auto;
|
|
107
|
+
inset-inline-start: 0;
|
|
108
|
+
height: 100vh;
|
|
109
|
+
transform: translateX(var(--_start-x));
|
|
110
|
+
box-shadow: var(--lt-sidepanel-shadow, calc(4px * var(--_dir-sign)) 0 24px rgba(0, 0, 0, 0.12));
|
|
79
111
|
}
|
|
80
112
|
|
|
81
|
-
:host([anchor='
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
:host([anchor='end']) dialog {
|
|
114
|
+
top: 0;
|
|
115
|
+
inset-inline-end: 0;
|
|
116
|
+
bottom: auto;
|
|
117
|
+
inset-inline-start: auto;
|
|
118
|
+
height: 100vh;
|
|
119
|
+
transform: translateX(var(--_end-x));
|
|
120
|
+
box-shadow: var(--lt-sidepanel-shadow, calc(-4px * var(--_dir-sign)) 0 24px rgba(0, 0, 0, 0.12));
|
|
86
121
|
}
|
|
87
122
|
|
|
88
|
-
:host([anchor='top'])
|
|
89
|
-
inset-inline: 0;
|
|
123
|
+
:host([anchor='top']) dialog {
|
|
90
124
|
top: 0;
|
|
125
|
+
inset-inline-end: auto;
|
|
126
|
+
bottom: auto;
|
|
127
|
+
inset-inline-start: 0;
|
|
128
|
+
width: 100vw;
|
|
91
129
|
transform: translateY(-100%);
|
|
92
130
|
box-shadow: var(--lt-sidepanel-shadow, 0 4px 24px rgba(0, 0, 0, 0.12));
|
|
93
131
|
}
|
|
94
132
|
|
|
95
|
-
:host([anchor='bottom'])
|
|
96
|
-
|
|
133
|
+
:host([anchor='bottom']) dialog {
|
|
134
|
+
top: auto;
|
|
135
|
+
inset-inline-end: auto;
|
|
97
136
|
bottom: 0;
|
|
137
|
+
inset-inline-start: 0;
|
|
138
|
+
width: 100vw;
|
|
98
139
|
transform: translateY(100%);
|
|
99
140
|
box-shadow: var(--lt-sidepanel-shadow, 0 -4px 24px rgba(0, 0, 0, 0.12));
|
|
100
141
|
}
|
|
101
142
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
opacity: 1;
|
|
106
|
-
pointer-events: auto;
|
|
143
|
+
dialog[open] {
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-direction: column;
|
|
107
146
|
}
|
|
108
147
|
|
|
109
|
-
|
|
148
|
+
/* The transform override has to be scoped per anchor, not a bare dialog[open] — :host([anchor='end'])
|
|
149
|
+
dialog (above) has HIGHER specificity than a plain dialog[open] (one more attribute selector, from the
|
|
150
|
+
:host() argument), so a bare "dialog[open] { transform: translate(0,0); }" would lose the cascade to the
|
|
151
|
+
anchor rule's own closed-position transform regardless of source order, leaving the panel permanently
|
|
152
|
+
stuck off-screen. Matching each anchor rule's own specificity here guarantees the open state wins. */
|
|
153
|
+
:host([anchor='start']) dialog[open],
|
|
154
|
+
:host(:not([anchor])) dialog[open],
|
|
155
|
+
:host([anchor='end']) dialog[open],
|
|
156
|
+
:host([anchor='top']) dialog[open],
|
|
157
|
+
:host([anchor='bottom']) dialog[open] {
|
|
110
158
|
transform: translate(0, 0);
|
|
111
|
-
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* A newly-shown dialog has no earlier rendered frame to transition from — @starting-style supplies one,
|
|
162
|
+
per anchor, so the slide-in actually animates instead of snapping straight to the open position. */
|
|
163
|
+
@starting-style {
|
|
164
|
+
:host([anchor='start']) dialog[open],
|
|
165
|
+
:host(:not([anchor])) dialog[open] {
|
|
166
|
+
transform: translateX(var(--_start-x));
|
|
167
|
+
}
|
|
168
|
+
:host([anchor='end']) dialog[open] {
|
|
169
|
+
transform: translateX(var(--_end-x));
|
|
170
|
+
}
|
|
171
|
+
:host([anchor='top']) dialog[open] {
|
|
172
|
+
transform: translateY(-100%);
|
|
173
|
+
}
|
|
174
|
+
:host([anchor='bottom']) dialog[open] {
|
|
175
|
+
transform: translateY(100%);
|
|
176
|
+
}
|
|
112
177
|
}
|
|
113
178
|
|
|
114
179
|
/* ── Header ─────────────────────────────────────────────────────────────── */
|
|
@@ -129,7 +194,7 @@ var sidepanelStyles = css`
|
|
|
129
194
|
|
|
130
195
|
.close-button {
|
|
131
196
|
flex-shrink: 0;
|
|
132
|
-
margin-
|
|
197
|
+
margin-inline-start: auto;
|
|
133
198
|
}
|
|
134
199
|
|
|
135
200
|
/* ── Body ───────────────────────────────────────────────────────────────── */
|
|
@@ -143,8 +208,8 @@ var sidepanelStyles = css`
|
|
|
143
208
|
/* ── Reduced motion ─────────────────────────────────────────────────────── */
|
|
144
209
|
|
|
145
210
|
@media (prefers-reduced-motion: reduce) {
|
|
146
|
-
|
|
147
|
-
|
|
211
|
+
dialog,
|
|
212
|
+
dialog::backdrop {
|
|
148
213
|
transition: none;
|
|
149
214
|
}
|
|
150
215
|
}
|
|
@@ -153,8 +218,7 @@ var sidepanelStyles = css`
|
|
|
153
218
|
// src/components/shared/backdrop.styles.ts
|
|
154
219
|
import { css as css2 } from "lit";
|
|
155
220
|
var backdropFeatureStyles = css2`
|
|
156
|
-
:host([backdrop-blur])
|
|
157
|
-
:host([backdrop-blur]) [part='overlay'] {
|
|
221
|
+
:host([backdrop-blur]) dialog::backdrop {
|
|
158
222
|
backdrop-filter: blur(6px);
|
|
159
223
|
-webkit-backdrop-filter: blur(6px);
|
|
160
224
|
}
|
|
@@ -163,6 +227,32 @@ var backdropFeatureStyles = css2`
|
|
|
163
227
|
// src/utils/dispatch.ts
|
|
164
228
|
var dispatch = (host, name, detail, init) => host.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true, ...init }));
|
|
165
229
|
|
|
230
|
+
// src/utils/dev-warn.ts
|
|
231
|
+
var devWarn = (tag, message) => {
|
|
232
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
233
|
+
console.warn(`[${tag}] ${message}`);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/components/shared/scroll-lock.ts
|
|
238
|
+
var lockCount = 0;
|
|
239
|
+
var previousOverflow = null;
|
|
240
|
+
function lockBodyScroll() {
|
|
241
|
+
if (lockCount === 0) {
|
|
242
|
+
previousOverflow = document.body.style.overflow;
|
|
243
|
+
document.body.style.overflow = "hidden";
|
|
244
|
+
}
|
|
245
|
+
lockCount++;
|
|
246
|
+
}
|
|
247
|
+
function unlockBodyScroll() {
|
|
248
|
+
if (lockCount === 0) return;
|
|
249
|
+
lockCount--;
|
|
250
|
+
if (lockCount === 0) {
|
|
251
|
+
document.body.style.overflow = previousOverflow ?? "";
|
|
252
|
+
previousOverflow = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
166
256
|
// src/components/icon-button/icon-button.ts
|
|
167
257
|
import { html, nothing } from "lit";
|
|
168
258
|
import { customElement, property as property2 } from "lit/decorators.js";
|
|
@@ -414,6 +504,14 @@ var iconButtonStyles = css3`
|
|
|
414
504
|
:host([round]) [part='base'] {
|
|
415
505
|
border-radius: 50%;
|
|
416
506
|
}
|
|
507
|
+
|
|
508
|
+
/* ── Reduced motion ─────────────────────────────────────────────────────── */
|
|
509
|
+
|
|
510
|
+
@media (prefers-reduced-motion: reduce) {
|
|
511
|
+
[part='base'] {
|
|
512
|
+
transition: none;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
417
515
|
`;
|
|
418
516
|
|
|
419
517
|
// src/components/shared/aria-forwarding.ts
|
|
@@ -422,7 +520,8 @@ var FORWARDED_ARIA_ATTRIBUTES = [
|
|
|
422
520
|
"aria-expanded",
|
|
423
521
|
"aria-haspopup",
|
|
424
522
|
"aria-controls",
|
|
425
|
-
"aria-current"
|
|
523
|
+
"aria-current",
|
|
524
|
+
"aria-description"
|
|
426
525
|
];
|
|
427
526
|
var AriaForwardingController = class {
|
|
428
527
|
constructor(host, getTarget) {
|
|
@@ -462,11 +561,30 @@ var IconButton = class extends ThemeableElement {
|
|
|
462
561
|
this.bordered = false;
|
|
463
562
|
this.size = "md";
|
|
464
563
|
this.disabled = false;
|
|
564
|
+
this._blockClickWhileDisabled = (e) => {
|
|
565
|
+
if (this.disabled) {
|
|
566
|
+
e.preventDefault();
|
|
567
|
+
e.stopImmediatePropagation();
|
|
568
|
+
}
|
|
569
|
+
};
|
|
465
570
|
this.href = "";
|
|
466
571
|
this.target = "";
|
|
467
572
|
this.rel = "";
|
|
468
573
|
this.round = false;
|
|
469
574
|
}
|
|
575
|
+
firstUpdated() {
|
|
576
|
+
if (!this.label && !this.textContent?.trim()) {
|
|
577
|
+
devWarn("lt-icon-button", "no label or visible text \u2014 set `label` so screen readers can name the button");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
connectedCallback() {
|
|
581
|
+
super.connectedCallback();
|
|
582
|
+
this.addEventListener("click", this._blockClickWhileDisabled, { capture: true });
|
|
583
|
+
}
|
|
584
|
+
disconnectedCallback() {
|
|
585
|
+
super.disconnectedCallback();
|
|
586
|
+
this.removeEventListener("click", this._blockClickWhileDisabled, { capture: true });
|
|
587
|
+
}
|
|
470
588
|
_inner() {
|
|
471
589
|
return this.icon ? html`<lt-icon name=${this.icon}></lt-icon>` : html`<slot></slot>`;
|
|
472
590
|
}
|
|
@@ -700,6 +818,20 @@ var textStyles = css4`
|
|
|
700
818
|
`;
|
|
701
819
|
|
|
702
820
|
// src/components/text/text.ts
|
|
821
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
822
|
+
"h1",
|
|
823
|
+
"h2",
|
|
824
|
+
"h3",
|
|
825
|
+
"h4",
|
|
826
|
+
"h5",
|
|
827
|
+
"h6",
|
|
828
|
+
"p",
|
|
829
|
+
"span",
|
|
830
|
+
"div",
|
|
831
|
+
"label",
|
|
832
|
+
"strong",
|
|
833
|
+
"em"
|
|
834
|
+
]);
|
|
703
835
|
var VARIANT_TAG = {
|
|
704
836
|
"display-2xl": "p",
|
|
705
837
|
"display-xl": "p",
|
|
@@ -724,7 +856,8 @@ var Text = class extends ThemeableElement {
|
|
|
724
856
|
this.as = "";
|
|
725
857
|
}
|
|
726
858
|
render() {
|
|
727
|
-
const
|
|
859
|
+
const requested = this.as && ALLOWED_TAGS.has(this.as) ? this.as : "";
|
|
860
|
+
const tag = unsafeStatic(requested || VARIANT_TAG[this.variant] || "p");
|
|
728
861
|
return html2`<${tag} part="base"><slot></slot></${tag}>`;
|
|
729
862
|
}
|
|
730
863
|
};
|
|
@@ -740,19 +873,45 @@ Text = __decorateClass([
|
|
|
740
873
|
], Text);
|
|
741
874
|
|
|
742
875
|
// src/components/sidepanel/sidepanel.ts
|
|
876
|
+
var DEFAULT_LABELS = {
|
|
877
|
+
close: "Close panel"
|
|
878
|
+
};
|
|
743
879
|
var SidePanel = class extends ThemeableElement {
|
|
744
880
|
constructor() {
|
|
745
881
|
super(...arguments);
|
|
746
882
|
this.open = false;
|
|
747
|
-
this.anchor = "
|
|
883
|
+
this.anchor = "start";
|
|
748
884
|
this.size = "320px";
|
|
749
885
|
this.label = "";
|
|
750
886
|
this.noCloseButton = false;
|
|
751
887
|
this.backdropBlur = false;
|
|
752
888
|
this.overlayOpacity = 0.45;
|
|
753
|
-
this.
|
|
754
|
-
|
|
889
|
+
this.labels = {};
|
|
890
|
+
this._scrollLocked = false;
|
|
891
|
+
// Same first-update guard, for the same reason, as lt-dialog (DLG-2), lt-dropdown (DDN-1) and lt-snackbar
|
|
892
|
+
// (SNK-1): `updated()` also runs after the very first render, where `changed.has('open')` is true even though
|
|
893
|
+
// nothing was ever shown.
|
|
894
|
+
this._skipNextOpenChange = true;
|
|
895
|
+
// Unlike lt-dialog, this has no close-on-escape toggle, so there's nothing to preventDefault() on 'cancel' —
|
|
896
|
+
// Escape is left entirely to the native element, now scoped to whichever panel actually has focus, not a
|
|
897
|
+
// window-level listener that closed every open panel on one keypress regardless of which was focused (SPL-3).
|
|
898
|
+
// The native element's own 'close' event fires whenever IT closes, for any reason — sync `open` back so the
|
|
899
|
+
// two never drift apart. If we're the ones who just called close() (inside _handleClose(), above), `open` is
|
|
900
|
+
// already false and this is a no-op.
|
|
901
|
+
this._handleNativeClose = () => {
|
|
902
|
+
this.open = false;
|
|
755
903
|
};
|
|
904
|
+
// The clicked-outside-the-content area of a shown <dialog> resolves e.target to the <dialog> element itself
|
|
905
|
+
// (::backdrop isn't a real node and can't be an event target) — clicking any actual descendant instead
|
|
906
|
+
// targets that descendant (same mechanism as lt-dialog's backdrop-click detection, confirmed in the spike).
|
|
907
|
+
this._handleDialogClick = (e) => {
|
|
908
|
+
if (e.target === this.dialogElement) {
|
|
909
|
+
this.hide();
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
get _labels() {
|
|
914
|
+
return { ...DEFAULT_LABELS, ...this.labels };
|
|
756
915
|
}
|
|
757
916
|
/** Open the panel. */
|
|
758
917
|
show() {
|
|
@@ -761,24 +920,30 @@ var SidePanel = class extends ThemeableElement {
|
|
|
761
920
|
/** Close the panel. */
|
|
762
921
|
hide() {
|
|
763
922
|
this.open = false;
|
|
764
|
-
dispatch(this, "close");
|
|
765
923
|
}
|
|
766
924
|
/** Toggle the panel open/closed. */
|
|
767
925
|
toggle() {
|
|
768
|
-
|
|
769
|
-
else this.show();
|
|
770
|
-
}
|
|
771
|
-
connectedCallback() {
|
|
772
|
-
super.connectedCallback();
|
|
773
|
-
window.addEventListener("keydown", this._handleKeyDown);
|
|
926
|
+
this.open = !this.open;
|
|
774
927
|
}
|
|
775
928
|
disconnectedCallback() {
|
|
776
929
|
super.disconnectedCallback();
|
|
777
|
-
|
|
930
|
+
if (this._scrollLocked) {
|
|
931
|
+
unlockBodyScroll();
|
|
932
|
+
this._scrollLocked = false;
|
|
933
|
+
}
|
|
778
934
|
}
|
|
779
935
|
updated(changed) {
|
|
936
|
+
if (changed.has("open")) {
|
|
937
|
+
if (this._skipNextOpenChange) {
|
|
938
|
+
this._skipNextOpenChange = false;
|
|
939
|
+
} else if (this.open) {
|
|
940
|
+
this._handleOpen();
|
|
941
|
+
} else {
|
|
942
|
+
this._handleClose();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
780
945
|
if (!changed.has("anchor")) return;
|
|
781
|
-
const panel = this.
|
|
946
|
+
const panel = this.dialogElement;
|
|
782
947
|
if (!panel) return;
|
|
783
948
|
panel.style.transition = "none";
|
|
784
949
|
if (this.open) {
|
|
@@ -790,33 +955,60 @@ var SidePanel = class extends ThemeableElement {
|
|
|
790
955
|
panel.style.transform = "";
|
|
791
956
|
});
|
|
792
957
|
}
|
|
958
|
+
_handleOpen() {
|
|
959
|
+
if (!this._scrollLocked) {
|
|
960
|
+
lockBodyScroll();
|
|
961
|
+
this._scrollLocked = true;
|
|
962
|
+
}
|
|
963
|
+
if (this.dialogElement && !this.dialogElement.open) {
|
|
964
|
+
this.dialogElement.showModal();
|
|
965
|
+
}
|
|
966
|
+
dispatch(this, "open");
|
|
967
|
+
}
|
|
968
|
+
_handleClose() {
|
|
969
|
+
if (this._scrollLocked) {
|
|
970
|
+
unlockBodyScroll();
|
|
971
|
+
this._scrollLocked = false;
|
|
972
|
+
}
|
|
973
|
+
if (this.dialogElement?.open) {
|
|
974
|
+
this.dialogElement.close();
|
|
975
|
+
}
|
|
976
|
+
dispatch(this, "close");
|
|
977
|
+
}
|
|
978
|
+
// translateX has no logical/direction-aware equivalent in CSS, unlike the positioning below (inset-inline-*
|
|
979
|
+
// resolves automatically) — var(--_start-x)/var(--_end-x) carry the direction-dependent sign instead (defined
|
|
980
|
+
// in the stylesheet, flipped under :host(:dir(rtl))), so this stays correct under RTL without reading
|
|
981
|
+
// getComputedStyle(this).direction here at all.
|
|
793
982
|
get _offScreenTransform() {
|
|
794
983
|
switch (this.anchor) {
|
|
795
|
-
case "
|
|
796
|
-
return "translateX(100%)";
|
|
984
|
+
case "end":
|
|
985
|
+
return "translateX(var(--_end-x, 100%))";
|
|
797
986
|
case "top":
|
|
798
987
|
return "translateY(-100%)";
|
|
799
988
|
case "bottom":
|
|
800
989
|
return "translateY(100%)";
|
|
801
990
|
default:
|
|
802
|
-
return "translateX(-100%)";
|
|
991
|
+
return "translateX(var(--_start-x, -100%))";
|
|
803
992
|
}
|
|
804
993
|
}
|
|
805
994
|
get _panelStyle() {
|
|
806
|
-
|
|
995
|
+
const sizeStyle = this.anchor === "start" || this.anchor === "end" ? `width: ${this.size}` : `height: ${this.size}`;
|
|
996
|
+
return `${sizeStyle}; --lt-sidepanel-overlay-opacity: ${this.overlayOpacity}`;
|
|
997
|
+
}
|
|
998
|
+
get _hasLabel() {
|
|
999
|
+
return this.label.trim().length > 0;
|
|
807
1000
|
}
|
|
808
1001
|
_renderHeader() {
|
|
809
|
-
|
|
810
|
-
if (!hasLabel && this.noCloseButton) return "";
|
|
1002
|
+
if (!this._hasLabel && this.noCloseButton) return "";
|
|
811
1003
|
return html3`
|
|
812
1004
|
<div part="header" class="header">
|
|
813
|
-
${
|
|
1005
|
+
${this._hasLabel ? html3`<lt-text variant="h6" class="panel-label" id="sidepanel-label">${this.label}</lt-text>` : ""}
|
|
814
1006
|
${!this.noCloseButton ? html3`
|
|
815
1007
|
<lt-icon-button
|
|
816
1008
|
part="close-button"
|
|
817
1009
|
class="close-button"
|
|
818
1010
|
icon="xmark"
|
|
819
|
-
label
|
|
1011
|
+
label=${this._labels.close}
|
|
820
1012
|
size="sm"
|
|
821
1013
|
@click=${this.hide}
|
|
822
1014
|
></lt-icon-button>
|
|
@@ -826,13 +1018,18 @@ var SidePanel = class extends ThemeableElement {
|
|
|
826
1018
|
}
|
|
827
1019
|
render() {
|
|
828
1020
|
return html3`
|
|
829
|
-
<
|
|
830
|
-
|
|
1021
|
+
<dialog
|
|
1022
|
+
part="panel"
|
|
1023
|
+
style=${this._panelStyle}
|
|
1024
|
+
aria-labelledby=${this._hasLabel ? "sidepanel-label" : nothing2}
|
|
1025
|
+
@close=${this._handleNativeClose}
|
|
1026
|
+
@click=${this._handleDialogClick}
|
|
1027
|
+
>
|
|
831
1028
|
${this._renderHeader()}
|
|
832
1029
|
<div part="body" class="body">
|
|
833
1030
|
<slot></slot>
|
|
834
1031
|
</div>
|
|
835
|
-
</
|
|
1032
|
+
</dialog>
|
|
836
1033
|
`;
|
|
837
1034
|
}
|
|
838
1035
|
};
|
|
@@ -858,6 +1055,12 @@ __decorateClass([
|
|
|
858
1055
|
__decorateClass([
|
|
859
1056
|
property4({ type: Number, attribute: "overlay-opacity" })
|
|
860
1057
|
], SidePanel.prototype, "overlayOpacity", 2);
|
|
1058
|
+
__decorateClass([
|
|
1059
|
+
property4({ type: Object })
|
|
1060
|
+
], SidePanel.prototype, "labels", 2);
|
|
1061
|
+
__decorateClass([
|
|
1062
|
+
query("dialog")
|
|
1063
|
+
], SidePanel.prototype, "dialogElement", 2);
|
|
861
1064
|
SidePanel = __decorateClass([
|
|
862
1065
|
customElement3("lt-sidepanel")
|
|
863
1066
|
], SidePanel);
|
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
import { type PropertyValues } from 'lit';
|
|
2
2
|
import { ThemeableElement } from '../../base';
|
|
3
|
-
import type { SidePanelAnchor } from './sidepanel.types';
|
|
3
|
+
import type { SidePanelAnchor, SidePanelLabels } from './sidepanel.types';
|
|
4
4
|
import '../icon-button/icon-button';
|
|
5
5
|
import '../text/text';
|
|
6
6
|
/**
|
|
7
|
-
* A slide-in panel anchored to any edge of the viewport,
|
|
7
|
+
* A slide-in panel anchored to any edge of the viewport, built on the native `<dialog>` element — the same
|
|
8
|
+
* migration `lt-dialog` went through (DLG-1..4), applied here to fix the equivalent gaps (SPL-1/2/3): a closed
|
|
9
|
+
* panel is no longer just transformed off-screen while staying focusable and exposed to assistive tech
|
|
10
|
+
* (`display: none` when closed is native, automatic, and absolute); focus trap and restore on close both come
|
|
11
|
+
* from `showModal()`/`close()` for free — no manual focus-tracking code needed here at all, unlike `lt-dialog`,
|
|
12
|
+
* which still carries its own from before that component's migration; Escape closes only the panel that was
|
|
13
|
+
* actually focused, not every open panel on the page (no more `window`-level keydown listener); and `close`
|
|
14
|
+
* fires consistently from `updated()` whenever `open` becomes `false`, however it happened — not only when
|
|
15
|
+
* `hide()` was called directly.
|
|
8
16
|
*
|
|
9
17
|
* @element lt-sidepanel
|
|
10
18
|
* @slot - Content rendered inside the panel body
|
|
11
|
-
* @fires
|
|
12
|
-
* @
|
|
13
|
-
*
|
|
19
|
+
* @fires open - Fired when the panel opens
|
|
20
|
+
* @fires close - Fired when the panel closes (however it closed — the close button, Escape, a backdrop click,
|
|
21
|
+
* or setting `open` to `false` directly)
|
|
22
|
+
* @part overlay - The backdrop scrim behind the panel (the native `<dialog>`'s own `::backdrop`, exposed as a
|
|
23
|
+
* part so consumers can still target it, even though it's not a real light-DOM-visible element)
|
|
24
|
+
* @part panel - The sliding panel container (the `<dialog>` element itself)
|
|
14
25
|
* @part header - The header bar (contains label and close button)
|
|
15
26
|
* @part close-button - The close button inside the header
|
|
16
27
|
* @part body - The scrollable content area wrapping the default slot
|
|
17
28
|
*
|
|
18
29
|
* @cssprop [--lt-sidepanel-shadow] - Overrides the panel's box-shadow, for all anchor positions.
|
|
30
|
+
* @cssprop [--lt-sidepanel-overlay-opacity] - Backdrop darkness. Set via the `overlay-opacity` property/attribute
|
|
31
|
+
* instead of this custom property directly, unless you're overriding it from outside the component.
|
|
19
32
|
*/
|
|
20
33
|
export declare class SidePanel extends ThemeableElement {
|
|
21
34
|
static styles: import("lit").CSSResult[];
|
|
22
35
|
/** Whether the panel is visible. */
|
|
23
36
|
open: boolean;
|
|
24
|
-
/** Edge from which the panel slides in. */
|
|
37
|
+
/** Edge from which the panel slides in. `'start'`/`'end'` flip with reading direction. */
|
|
25
38
|
anchor: SidePanelAnchor;
|
|
26
|
-
/** Panel width (
|
|
39
|
+
/** Panel width (start/end anchors) or height (top/bottom anchors). */
|
|
27
40
|
size: string;
|
|
28
41
|
/** Title shown in the panel header. When empty, the header is hidden. */
|
|
29
42
|
label: string;
|
|
@@ -33,18 +46,27 @@ export declare class SidePanel extends ThemeableElement {
|
|
|
33
46
|
backdropBlur: boolean;
|
|
34
47
|
/** Backdrop darkness as an alpha value from 0 (transparent) to 1 (opaque). */
|
|
35
48
|
overlayOpacity: number;
|
|
49
|
+
/** Overridable UI text (the close button's accessible name), for i18n. English defaults — override only the keys you need. */
|
|
50
|
+
labels: Partial<SidePanelLabels>;
|
|
51
|
+
private get _labels();
|
|
52
|
+
private dialogElement?;
|
|
53
|
+
private _scrollLocked;
|
|
54
|
+
private _skipNextOpenChange;
|
|
36
55
|
/** Open the panel. */
|
|
37
56
|
show(): void;
|
|
38
57
|
/** Close the panel. */
|
|
39
58
|
hide(): void;
|
|
40
59
|
/** Toggle the panel open/closed. */
|
|
41
60
|
toggle(): void;
|
|
42
|
-
connectedCallback(): void;
|
|
43
61
|
disconnectedCallback(): void;
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
updated(changed: PropertyValues<this>): void;
|
|
63
|
+
private _handleOpen;
|
|
64
|
+
private _handleClose;
|
|
65
|
+
private _handleNativeClose;
|
|
66
|
+
private _handleDialogClick;
|
|
46
67
|
private get _offScreenTransform();
|
|
47
68
|
private get _panelStyle();
|
|
69
|
+
private get _hasLabel();
|
|
48
70
|
private _renderHeader;
|
|
49
71
|
render(): import("lit").TemplateResult<1>;
|
|
50
72
|
}
|
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Edge the panel slides in from. `'start'`/`'end'` are logical — they resolve to the left/right edge based on
|
|
3
|
+
* the page's reading direction (left/right in LTR, right/left in RTL), not a fixed physical side.
|
|
4
|
+
*/
|
|
5
|
+
export type SidePanelAnchor = 'start' | 'end' | 'top' | 'bottom';
|
|
6
|
+
/** Overridable UI text, for i18n — English defaults; override only the keys you need. */
|
|
7
|
+
export interface SidePanelLabels {
|
|
8
|
+
/** Accessible label for the close button. */
|
|
9
|
+
close: string;
|
|
10
|
+
}
|
|
@@ -46,7 +46,7 @@ var skeletonStyles = css`
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
[part='base'] {
|
|
49
|
-
background: var(--lt-
|
|
49
|
+
background: var(--lt-bg-overlay);
|
|
50
50
|
border-radius: var(--lt-border-radius);
|
|
51
51
|
height: 1.25rem;
|
|
52
52
|
overflow: hidden;
|
|
@@ -66,12 +66,7 @@ var skeletonStyles = css`
|
|
|
66
66
|
|
|
67
67
|
:host([animated]) [part='base'] {
|
|
68
68
|
animation: shimmer 1.5s ease-in-out infinite;
|
|
69
|
-
background: linear-gradient(
|
|
70
|
-
90deg,
|
|
71
|
-
var(--lt-color-neutral-100) 25%,
|
|
72
|
-
var(--lt-color-neutral-200) 50%,
|
|
73
|
-
var(--lt-color-neutral-100) 75%
|
|
74
|
-
);
|
|
69
|
+
background: linear-gradient(90deg, var(--lt-bg-surface) 25%, var(--lt-bg-overlay) 50%, var(--lt-bg-surface) 75%);
|
|
75
70
|
background-size: 200% 100%;
|
|
76
71
|
}
|
|
77
72
|
|
|
@@ -87,7 +82,7 @@ var skeletonStyles = css`
|
|
|
87
82
|
@media (prefers-reduced-motion: reduce) {
|
|
88
83
|
:host([animated]) [part='base'] {
|
|
89
84
|
animation: none;
|
|
90
|
-
background: var(--lt-
|
|
85
|
+
background: var(--lt-bg-overlay);
|
|
91
86
|
}
|
|
92
87
|
}
|
|
93
88
|
`;
|