@sondor-technology/sondor-components 1.0.1-develop.14 → 1.0.1-develop.16
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/calendar/calendar.d.ts +36 -0
- package/dist/components/calendar/calendar.d.ts.map +1 -0
- package/dist/components/calendar/calendar.js +449 -0
- package/dist/components/calendar/calendar.js.map +1 -0
- package/dist/components/cron-builder/cron-builder.d.ts +37 -0
- package/dist/components/cron-builder/cron-builder.d.ts.map +1 -0
- package/dist/components/cron-builder/cron-builder.js +522 -0
- package/dist/components/cron-builder/cron-builder.js.map +1 -0
- package/dist/components/input/input.d.ts +1 -0
- package/dist/components/input/input.d.ts.map +1 -1
- package/dist/components/input/input.js +35 -12
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/pin-input/pin-input.d.ts +33 -0
- package/dist/components/pin-input/pin-input.d.ts.map +1 -0
- package/dist/components/pin-input/pin-input.js +253 -0
- package/dist/components/pin-input/pin-input.js.map +1 -0
- package/dist/components/popover/popover.d.ts +26 -0
- package/dist/components/popover/popover.d.ts.map +1 -0
- package/dist/components/popover/popover.js +196 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/select/select.d.ts +4 -1
- package/dist/components/select/select.d.ts.map +1 -1
- package/dist/components/select/select.js +26 -4
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/time-picker/time-picker.d.ts +38 -0
- package/dist/components/time-picker/time-picker.d.ts.map +1 -0
- package/dist/components/time-picker/time-picker.js +290 -0
- package/dist/components/time-picker/time-picker.js.map +1 -0
- package/dist/components/toggle-group/toggle-button.d.ts +23 -0
- package/dist/components/toggle-group/toggle-button.d.ts.map +1 -0
- package/dist/components/toggle-group/toggle-button.js +114 -0
- package/dist/components/toggle-group/toggle-button.js.map +1 -0
- package/dist/components/toggle-group/toggle-group.d.ts +38 -0
- package/dist/components/toggle-group/toggle-group.d.ts.map +1 -0
- package/dist/components/toggle-group/toggle-group.js +270 -0
- package/dist/components/toggle-group/toggle-group.js.map +1 -0
- package/dist/components/tooltip/tooltip.d.ts +33 -0
- package/dist/components/tooltip/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip/tooltip.js +293 -0
- package/dist/components/tooltip/tooltip.js.map +1 -0
- package/dist/styles/index.css +2 -0
- package/package.json +33 -1
- package/src/components/calendar/calendar.ts +458 -0
- package/src/components/cron-builder/cron-builder.ts +529 -0
- package/src/components/input/input.ts +38 -14
- package/src/components/pin-input/pin-input.ts +249 -0
- package/src/components/popover/popover.ts +192 -0
- package/src/components/select/select.ts +25 -63
- package/src/components/time-picker/time-picker.ts +286 -0
- package/src/components/toggle-group/toggle-button.ts +103 -0
- package/src/components/toggle-group/toggle-group.ts +264 -0
- package/src/components/tooltip/tooltip.ts +286 -0
- package/src/styles/theme.css +3 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import {LitElement, html, css, PropertyValues} from "lit";
|
|
2
|
+
import {customElement, property, query, state} from "lit/decorators.js";
|
|
3
|
+
import {classMap} from "lit/directives/class-map.js";
|
|
4
|
+
import {ifDefined} from "lit/directives/if-defined.js";
|
|
5
|
+
import {resetStyles} from "../../styles/reset.js";
|
|
6
|
+
|
|
7
|
+
@customElement("st-time-picker")
|
|
8
|
+
export class StTimePicker extends LitElement {
|
|
9
|
+
static formAssociated = true;
|
|
10
|
+
private _internals: ElementInternals;
|
|
11
|
+
|
|
12
|
+
@property({type: String, reflect: true}) name = "";
|
|
13
|
+
@property({type: String}) label = "";
|
|
14
|
+
|
|
15
|
+
// Visually hide the label for screen-reader only accessibility
|
|
16
|
+
@property({type: Boolean, attribute: "hide-label"}) hideLabel = false;
|
|
17
|
+
|
|
18
|
+
// Time string in HH:mm format (e.g., "14:30")
|
|
19
|
+
@property({type: String}) value = "";
|
|
20
|
+
|
|
21
|
+
@property({type: String}) min = "";
|
|
22
|
+
@property({type: String}) max = "";
|
|
23
|
+
@property({type: Number}) step?: number;
|
|
24
|
+
|
|
25
|
+
@property({type: Boolean, reflect: true}) disabled = false;
|
|
26
|
+
@property({type: Boolean, reflect: true}) required = false;
|
|
27
|
+
@property({type: Boolean, reflect: true}) invalid = false;
|
|
28
|
+
|
|
29
|
+
@state() private _touched = false;
|
|
30
|
+
|
|
31
|
+
@query(".native-input") private _inputEl!: HTMLInputElement;
|
|
32
|
+
|
|
33
|
+
private _labelId = `time-label-${Math.random().toString(36).substring(2, 9)}`;
|
|
34
|
+
private _defaultValue = "";
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
super();
|
|
38
|
+
this._internals = this.attachInternals();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static styles = [
|
|
42
|
+
resetStyles,
|
|
43
|
+
css`
|
|
44
|
+
:host {
|
|
45
|
+
display: block;
|
|
46
|
+
font-family: inherit;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
label {
|
|
50
|
+
display: block;
|
|
51
|
+
font-size: var(--st-font-size-sm);
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
color: var(--st-color-text-muted);
|
|
54
|
+
margin-bottom: var(--st-spacing-xs);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.sr-only {
|
|
58
|
+
position: absolute;
|
|
59
|
+
width: 1px;
|
|
60
|
+
height: 1px;
|
|
61
|
+
padding: 0;
|
|
62
|
+
margin: -1px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
clip: rect(0, 0, 0, 0);
|
|
65
|
+
border: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.input-wrapper {
|
|
69
|
+
position: relative;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
width: 100%;
|
|
73
|
+
background: var(--st-color-surface);
|
|
74
|
+
border: 1px solid var(--st-color-border);
|
|
75
|
+
border-radius: var(--st-radius-md);
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
transition:
|
|
78
|
+
border-color 0.2s,
|
|
79
|
+
box-shadow 0.2s,
|
|
80
|
+
background-color 0.2s;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Focus within ring */
|
|
84
|
+
.input-wrapper:focus-within {
|
|
85
|
+
border-color: var(--st-color-primary);
|
|
86
|
+
box-shadow: 0 0 0 2px var(--st-color-primary-muted, rgba(59, 130, 246, 0.25));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.input-wrapper.is-invalid {
|
|
90
|
+
border-color: var(--st-color-danger, #dc2626);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.input-wrapper.is-disabled {
|
|
94
|
+
background: var(--st-color-surface-muted, #f1f5f9);
|
|
95
|
+
cursor: not-allowed;
|
|
96
|
+
opacity: 0.7;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.native-input {
|
|
100
|
+
width: 100%;
|
|
101
|
+
min-height: 42px;
|
|
102
|
+
padding: var(--st-spacing-xs) var(--st-spacing-md);
|
|
103
|
+
background: transparent;
|
|
104
|
+
border: none;
|
|
105
|
+
color: var(--st-color-text);
|
|
106
|
+
font-family: inherit;
|
|
107
|
+
font-size: var(--st-font-size-base);
|
|
108
|
+
outline: none;
|
|
109
|
+
cursor: inherit;
|
|
110
|
+
appearance: none;
|
|
111
|
+
-webkit-appearance: none;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Style the native clock icon generated by WebKit/Blink */
|
|
115
|
+
.native-input::-webkit-calendar-picker-indicator {
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
opacity: 0.6;
|
|
118
|
+
transition: opacity 0.2s;
|
|
119
|
+
filter: var(--st-icon-filter, none); /* Allows inverting for dark mode */
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.native-input::-webkit-calendar-picker-indicator:hover {
|
|
123
|
+
opacity: 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.native-input:disabled {
|
|
127
|
+
color: var(--st-color-text-muted);
|
|
128
|
+
}
|
|
129
|
+
`,
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
override firstUpdated(changedProperties: PropertyValues) {
|
|
133
|
+
super.firstUpdated(changedProperties);
|
|
134
|
+
this._defaultValue = this.value;
|
|
135
|
+
|
|
136
|
+
// Sync initial validity state based on native input
|
|
137
|
+
this._syncValidity();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
override updated(changedProperties: PropertyValues) {
|
|
141
|
+
super.updated(changedProperties);
|
|
142
|
+
if (
|
|
143
|
+
changedProperties.has("value") ||
|
|
144
|
+
changedProperties.has("required") ||
|
|
145
|
+
changedProperties.has("min") ||
|
|
146
|
+
changedProperties.has("max")
|
|
147
|
+
) {
|
|
148
|
+
this._internals.setFormValue(this.value || null);
|
|
149
|
+
this._syncValidity();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private _syncValidity() {
|
|
154
|
+
if (!this._inputEl) return;
|
|
155
|
+
|
|
156
|
+
this.invalid = false;
|
|
157
|
+
|
|
158
|
+
// 🚀 FIX: Safari/WebKit poorly supports min/max on time inputs.
|
|
159
|
+
// We enforce these constraints manually using strict lexicographical comparison.
|
|
160
|
+
if (this.required && !this.value) {
|
|
161
|
+
this._internals.setValidity({valueMissing: true}, "Please select a time.");
|
|
162
|
+
if (this._touched) this.invalid = true;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.value && this.min && this.value < this.min) {
|
|
167
|
+
this._internals.setValidity({rangeUnderflow: true}, `Time must be ${this.min} or later.`);
|
|
168
|
+
if (this._touched) this.invalid = true;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (this.value && this.max && this.value > this.max) {
|
|
173
|
+
this._internals.setValidity({rangeOverflow: true}, `Time must be ${this.max} or earlier.`);
|
|
174
|
+
if (this._touched) this.invalid = true;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Fallback to native validation for things like 'step' bounds
|
|
179
|
+
const isValid = this._inputEl.checkValidity();
|
|
180
|
+
|
|
181
|
+
if (isValid) {
|
|
182
|
+
this._internals.setValidity({});
|
|
183
|
+
this.invalid = false;
|
|
184
|
+
} else {
|
|
185
|
+
this._internals.setValidity(
|
|
186
|
+
this._inputEl.validity,
|
|
187
|
+
this._inputEl.validationMessage,
|
|
188
|
+
this._inputEl,
|
|
189
|
+
);
|
|
190
|
+
if (this._touched) {
|
|
191
|
+
this.invalid = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public checkValidity() {
|
|
197
|
+
return this._internals.checkValidity();
|
|
198
|
+
}
|
|
199
|
+
public reportValidity() {
|
|
200
|
+
this._touched = true;
|
|
201
|
+
this._syncValidity();
|
|
202
|
+
return this._internals.reportValidity();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
formResetCallback() {
|
|
206
|
+
this.value = this._defaultValue;
|
|
207
|
+
this._touched = false;
|
|
208
|
+
this.invalid = false;
|
|
209
|
+
this._syncValidity();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public override focus(options?: FocusOptions) {
|
|
213
|
+
this._inputEl?.focus(options);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private _handleInput(e: Event) {
|
|
217
|
+
this.value = (e.target as HTMLInputElement).value;
|
|
218
|
+
this.dispatchEvent(new Event("input", {bubbles: true, composed: true}));
|
|
219
|
+
this.dispatchEvent(
|
|
220
|
+
new CustomEvent("st-input", {
|
|
221
|
+
bubbles: true,
|
|
222
|
+
composed: true,
|
|
223
|
+
detail: {name: this.name, value: this.value},
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private _handleChange(e: Event) {
|
|
229
|
+
this._touched = true;
|
|
230
|
+
this.value = (e.target as HTMLInputElement).value;
|
|
231
|
+
this._syncValidity();
|
|
232
|
+
this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
|
|
233
|
+
this.dispatchEvent(
|
|
234
|
+
new CustomEvent("st-change", {
|
|
235
|
+
bubbles: true,
|
|
236
|
+
composed: true,
|
|
237
|
+
detail: {name: this.name, value: this.value},
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private _handleBlur() {
|
|
243
|
+
this._touched = true;
|
|
244
|
+
this._syncValidity();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
render() {
|
|
248
|
+
return html`
|
|
249
|
+
${this.label
|
|
250
|
+
? html`<label id=${this._labelId} class=${classMap({"sr-only": this.hideLabel})}
|
|
251
|
+
>${this.label}</label
|
|
252
|
+
>`
|
|
253
|
+
: ""}
|
|
254
|
+
|
|
255
|
+
<div
|
|
256
|
+
class=${classMap({
|
|
257
|
+
"input-wrapper": true,
|
|
258
|
+
"is-disabled": this.disabled,
|
|
259
|
+
"is-invalid": this.invalid,
|
|
260
|
+
})}
|
|
261
|
+
>
|
|
262
|
+
<input
|
|
263
|
+
class="native-input"
|
|
264
|
+
type="time"
|
|
265
|
+
name=${ifDefined(this.name || undefined)}
|
|
266
|
+
.value=${this.value}
|
|
267
|
+
min=${ifDefined(this.min || undefined)}
|
|
268
|
+
max=${ifDefined(this.max || undefined)}
|
|
269
|
+
step=${ifDefined(this.step)}
|
|
270
|
+
?required=${this.required}
|
|
271
|
+
?disabled=${this.disabled}
|
|
272
|
+
aria-labelledby=${ifDefined(this.label ? this._labelId : undefined)}
|
|
273
|
+
@input=${this._handleInput}
|
|
274
|
+
@change=${this._handleChange}
|
|
275
|
+
@blur=${this._handleBlur}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
declare global {
|
|
283
|
+
interface HTMLElementTagNameMap {
|
|
284
|
+
"st-time-picker": StTimePicker;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import { resetStyles } from "../../styles/reset.js";
|
|
4
|
+
|
|
5
|
+
@customElement("st-toggle-button")
|
|
6
|
+
export class StToggleButton extends LitElement {
|
|
7
|
+
static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true };
|
|
8
|
+
|
|
9
|
+
@property({ type: String }) value = "";
|
|
10
|
+
@property({ type: Boolean, reflect: true }) selected = false;
|
|
11
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
12
|
+
|
|
13
|
+
static styles = [
|
|
14
|
+
resetStyles,
|
|
15
|
+
css`
|
|
16
|
+
:host {
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
flex: 1; /* Ensures all buttons in a group stretch evenly */
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
button {
|
|
22
|
+
display: inline-flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
gap: var(--st-spacing-xs);
|
|
26
|
+
width: 100%;
|
|
27
|
+
padding: var(--st-spacing-sm) var(--st-spacing-md);
|
|
28
|
+
background-color: var(--st-color-surface);
|
|
29
|
+
color: var(--st-color-text);
|
|
30
|
+
border: none;
|
|
31
|
+
font-size: var(--st-font-size-sm);
|
|
32
|
+
font-weight: var(--st-font-weight-medium);
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
transition: all 0.2s ease;
|
|
35
|
+
white-space: nowrap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
button:hover:not(:disabled) {
|
|
39
|
+
background-color: var(--st-color-surface-hover, #f8fafc);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
button:focus-visible {
|
|
43
|
+
outline: 2px solid var(--st-color-primary);
|
|
44
|
+
outline-offset: -2px;
|
|
45
|
+
z-index: 1; /* Brings the focus ring above siblings */
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Active/Selected State */
|
|
49
|
+
:host([selected]) button {
|
|
50
|
+
background-color: var(--st-color-primary-light, #eff6ff);
|
|
51
|
+
color: var(--st-color-primary-dark, #1d4ed8);
|
|
52
|
+
font-weight: var(--st-font-weight-semibold);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Disabled State */
|
|
56
|
+
:host([disabled]) {
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:host([disabled]) button {
|
|
61
|
+
opacity: 0.5;
|
|
62
|
+
background-color: var(--st-color-surface-muted, #f1f5f9);
|
|
63
|
+
color: var(--st-color-text-muted);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
::slotted(svg) {
|
|
67
|
+
width: 1.125rem;
|
|
68
|
+
height: 1.125rem;
|
|
69
|
+
fill: currentColor;
|
|
70
|
+
}
|
|
71
|
+
`
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
private _handleClick() {
|
|
75
|
+
if (this.disabled) return;
|
|
76
|
+
|
|
77
|
+
// Dispatch an internal event for the group orchestrator to catch
|
|
78
|
+
this.dispatchEvent(new CustomEvent("st-toggle-action", {
|
|
79
|
+
detail: { value: this.value },
|
|
80
|
+
bubbles: true,
|
|
81
|
+
composed: true
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
render() {
|
|
86
|
+
return html`
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
aria-pressed=${this.selected ? "true" : "false"}
|
|
90
|
+
?disabled=${this.disabled}
|
|
91
|
+
@click=${this._handleClick}
|
|
92
|
+
>
|
|
93
|
+
<slot></slot>
|
|
94
|
+
</button>
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
declare global {
|
|
100
|
+
interface HTMLElementTagNameMap {
|
|
101
|
+
"st-toggle-button": StToggleButton;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {LitElement, html, css, PropertyValues} from "lit";
|
|
2
|
+
import {customElement, property, queryAssignedElements, state, query} from "lit/decorators.js";
|
|
3
|
+
import {ifDefined} from "lit/directives/if-defined.js";
|
|
4
|
+
import {classMap} from "lit/directives/class-map.js";
|
|
5
|
+
import {resetStyles} from "../../styles/reset.js";
|
|
6
|
+
import type {StToggleButton} from "./toggle-button.js";
|
|
7
|
+
import "./toggle-button.js";
|
|
8
|
+
|
|
9
|
+
@customElement("st-toggle-group")
|
|
10
|
+
export class StToggleGroup extends LitElement {
|
|
11
|
+
static formAssociated = true;
|
|
12
|
+
private _internals: ElementInternals;
|
|
13
|
+
private _resizeObserver!: ResizeObserver;
|
|
14
|
+
|
|
15
|
+
@query(".group-container") private _container!: HTMLElement;
|
|
16
|
+
|
|
17
|
+
@queryAssignedElements({selector: "st-toggle-button"})
|
|
18
|
+
private _buttons!: StToggleButton[];
|
|
19
|
+
|
|
20
|
+
@property({type: String, reflect: true}) name = "";
|
|
21
|
+
@property({type: String}) value = "";
|
|
22
|
+
@property({type: Boolean}) multiple = false;
|
|
23
|
+
@property({type: Boolean, reflect: true}) disabled = false;
|
|
24
|
+
@property({type: Boolean, reflect: true}) required = false;
|
|
25
|
+
@property({attribute: "aria-label"}) ariaLabel = "";
|
|
26
|
+
|
|
27
|
+
// 🚀 NEW: State properties for dynamic shadows
|
|
28
|
+
@state() private _canScrollLeft = false;
|
|
29
|
+
@state() private _canScrollRight = false;
|
|
30
|
+
|
|
31
|
+
private _defaultValue = "";
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
super();
|
|
35
|
+
this._internals = this.attachInternals();
|
|
36
|
+
this.addEventListener("st-toggle-action", this._handleToggle as EventListener);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static styles = [
|
|
40
|
+
resetStyles,
|
|
41
|
+
css`
|
|
42
|
+
:host {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
max-width: 100%;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.scroll-wrapper {
|
|
49
|
+
position: relative;
|
|
50
|
+
display: inline-flex;
|
|
51
|
+
max-width: 100%;
|
|
52
|
+
min-width: 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* 🚀 FIX: Widened to 40px, boosted z-index to 10, and darkened the opacity */
|
|
56
|
+
.scroll-wrapper::before,
|
|
57
|
+
.scroll-wrapper::after {
|
|
58
|
+
content: "";
|
|
59
|
+
position: absolute;
|
|
60
|
+
top: 0;
|
|
61
|
+
bottom: 0;
|
|
62
|
+
width: 40px;
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
z-index: 10;
|
|
65
|
+
opacity: 0;
|
|
66
|
+
transition: opacity 0.3s ease;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.scroll-wrapper::before {
|
|
70
|
+
left: 0;
|
|
71
|
+
background: linear-gradient(
|
|
72
|
+
to right,
|
|
73
|
+
var(--st-color-scroll-shadow, rgba(0, 0, 0, 0.15)) 0%,
|
|
74
|
+
transparent 100%
|
|
75
|
+
);
|
|
76
|
+
border-top-left-radius: var(--st-radius-md);
|
|
77
|
+
border-bottom-left-radius: var(--st-radius-md);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.scroll-wrapper::after {
|
|
81
|
+
right: 0;
|
|
82
|
+
background: linear-gradient(
|
|
83
|
+
to left,
|
|
84
|
+
var(--st-color-scroll-shadow, rgba(0, 0, 0, 0.15)) 0%,
|
|
85
|
+
transparent 100%
|
|
86
|
+
);
|
|
87
|
+
border-top-right-radius: var(--st-radius-md);
|
|
88
|
+
border-bottom-right-radius: var(--st-radius-md);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.scroll-wrapper.can-scroll-left::before {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.scroll-wrapper.can-scroll-right::after {
|
|
96
|
+
opacity: 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* 🚀 FIX: Dialed up to 0.5 for dark mode */
|
|
100
|
+
:host([data-theme="dark"]) .scroll-wrapper::before {
|
|
101
|
+
background: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, transparent 100%);
|
|
102
|
+
}
|
|
103
|
+
:host([data-theme="dark"]) .scroll-wrapper::after {
|
|
104
|
+
background: linear-gradient(to left, rgba(0, 0, 0, 0.5) 0%, transparent 100%);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.group-container {
|
|
108
|
+
display: inline-flex;
|
|
109
|
+
max-width: 100%;
|
|
110
|
+
min-width: 0;
|
|
111
|
+
background-color: var(--st-color-border);
|
|
112
|
+
gap: 1px;
|
|
113
|
+
border: 1px solid var(--st-color-border);
|
|
114
|
+
border-radius: var(--st-radius-md);
|
|
115
|
+
|
|
116
|
+
overflow-x: auto;
|
|
117
|
+
overflow-y: hidden;
|
|
118
|
+
-webkit-overflow-scrolling: touch;
|
|
119
|
+
scrollbar-width: none;
|
|
120
|
+
-ms-overflow-style: none;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.group-container::-webkit-scrollbar {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
::slotted(*) {
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
:host([disabled]) .group-container {
|
|
132
|
+
opacity: 0.5;
|
|
133
|
+
pointer-events: none;
|
|
134
|
+
}
|
|
135
|
+
`,
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
override firstUpdated(changedProperties: PropertyValues) {
|
|
139
|
+
super.firstUpdated(changedProperties);
|
|
140
|
+
this._defaultValue = this.value;
|
|
141
|
+
|
|
142
|
+
// 🚀 NEW: Observe the container. If the user rotates their phone,
|
|
143
|
+
// we need to recalculate if shadows are still needed.
|
|
144
|
+
this._resizeObserver = new ResizeObserver(() => this._checkScroll());
|
|
145
|
+
if (this._container) {
|
|
146
|
+
this._resizeObserver.observe(this._container);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override disconnectedCallback() {
|
|
151
|
+
super.disconnectedCallback();
|
|
152
|
+
if (this._resizeObserver) {
|
|
153
|
+
this._resizeObserver.disconnect();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override updated(changedProperties: PropertyValues) {
|
|
158
|
+
super.updated(changedProperties);
|
|
159
|
+
|
|
160
|
+
if (changedProperties.has("value")) {
|
|
161
|
+
this._internals.setFormValue(this.value || null);
|
|
162
|
+
this._syncButtons();
|
|
163
|
+
this._syncValidity();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (changedProperties.has("disabled")) {
|
|
167
|
+
this._buttons.forEach((btn) => (btn.disabled = this.disabled));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 🚀 NEW: Mathematically determine if we are at the edges of the scroll box
|
|
172
|
+
private _checkScroll = () => {
|
|
173
|
+
if (!this._container) return;
|
|
174
|
+
const {scrollLeft, scrollWidth, clientWidth} = this._container;
|
|
175
|
+
|
|
176
|
+
// Use a 1px buffer to prevent fractional pixel rounding errors on high-DPI screens
|
|
177
|
+
this._canScrollLeft = scrollLeft > 1;
|
|
178
|
+
this._canScrollRight = Math.ceil(scrollLeft + clientWidth) < scrollWidth - 1;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
private _syncButtons() {
|
|
182
|
+
const values = this.multiple && this.value ? this.value.split(",") : [this.value];
|
|
183
|
+
this._buttons.forEach((btn) => {
|
|
184
|
+
btn.selected = values.includes(btn.value);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private _handleSlotChange() {
|
|
189
|
+
this._syncButtons();
|
|
190
|
+
if (this.disabled) {
|
|
191
|
+
this._buttons.forEach((btn) => (btn.disabled = true));
|
|
192
|
+
}
|
|
193
|
+
// Calculate shadows after slots render
|
|
194
|
+
setTimeout(() => this._checkScroll(), 50);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private _handleToggle(e: CustomEvent<{value: string}>) {
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
const toggledValue = e.detail.value;
|
|
200
|
+
|
|
201
|
+
if (this.multiple) {
|
|
202
|
+
let values = this.value ? this.value.split(",") : [];
|
|
203
|
+
if (values.includes(toggledValue)) {
|
|
204
|
+
values = values.filter((v) => v !== toggledValue);
|
|
205
|
+
} else {
|
|
206
|
+
values.push(toggledValue);
|
|
207
|
+
}
|
|
208
|
+
this.value = values.join(",");
|
|
209
|
+
} else {
|
|
210
|
+
if (this.value !== toggledValue) {
|
|
211
|
+
this.value = toggledValue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
formResetCallback() {
|
|
219
|
+
this.value = this._defaultValue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private _syncValidity() {
|
|
223
|
+
if (this.required && !this.value) {
|
|
224
|
+
this._internals.setValidity({valueMissing: true}, "Please select an option.");
|
|
225
|
+
} else {
|
|
226
|
+
this._internals.setValidity({});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public checkValidity() {
|
|
231
|
+
return this._internals.checkValidity();
|
|
232
|
+
}
|
|
233
|
+
public reportValidity() {
|
|
234
|
+
return this._internals.reportValidity();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
render() {
|
|
238
|
+
const wrapperClasses = classMap({
|
|
239
|
+
"scroll-wrapper": true,
|
|
240
|
+
"can-scroll-left": this._canScrollLeft,
|
|
241
|
+
"can-scroll-right": this._canScrollRight,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return html`
|
|
245
|
+
<div class=${wrapperClasses}>
|
|
246
|
+
<div
|
|
247
|
+
class="group-container"
|
|
248
|
+
role="group"
|
|
249
|
+
aria-label=${ifDefined(this.ariaLabel || undefined)}
|
|
250
|
+
aria-required=${this.required ? "true" : "false"}
|
|
251
|
+
@scroll=${this._checkScroll}
|
|
252
|
+
>
|
|
253
|
+
<slot @slotchange=${this._handleSlotChange}></slot>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
declare global {
|
|
261
|
+
interface HTMLElementTagNameMap {
|
|
262
|
+
"st-toggle-group": StToggleGroup;
|
|
263
|
+
}
|
|
264
|
+
}
|