@sondor-technology/sondor-components 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/components/calendar/calendar.d.ts +36 -0
  2. package/dist/components/calendar/calendar.d.ts.map +1 -0
  3. package/dist/components/calendar/calendar.js +439 -0
  4. package/dist/components/calendar/calendar.js.map +1 -0
  5. package/dist/components/cron-builder/cron-builder.d.ts +34 -0
  6. package/dist/components/cron-builder/cron-builder.d.ts.map +1 -0
  7. package/dist/components/cron-builder/cron-builder.js +456 -0
  8. package/dist/components/cron-builder/cron-builder.js.map +1 -0
  9. package/dist/components/input/input.d.ts +1 -0
  10. package/dist/components/input/input.d.ts.map +1 -1
  11. package/dist/components/input/input.js +35 -12
  12. package/dist/components/input/input.js.map +1 -1
  13. package/dist/components/pin-input/pin-input.d.ts +33 -0
  14. package/dist/components/pin-input/pin-input.d.ts.map +1 -0
  15. package/dist/components/pin-input/pin-input.js +253 -0
  16. package/dist/components/pin-input/pin-input.js.map +1 -0
  17. package/dist/components/popover/popover.d.ts +26 -0
  18. package/dist/components/popover/popover.d.ts.map +1 -0
  19. package/dist/components/popover/popover.js +196 -0
  20. package/dist/components/popover/popover.js.map +1 -0
  21. package/dist/components/select/select.d.ts +4 -1
  22. package/dist/components/select/select.d.ts.map +1 -1
  23. package/dist/components/select/select.js +26 -4
  24. package/dist/components/select/select.js.map +1 -1
  25. package/dist/components/time-picker/time-picker.d.ts +38 -0
  26. package/dist/components/time-picker/time-picker.d.ts.map +1 -0
  27. package/dist/components/time-picker/time-picker.js +280 -0
  28. package/dist/components/time-picker/time-picker.js.map +1 -0
  29. package/dist/components/toggle-group/toggle-button.d.ts +23 -0
  30. package/dist/components/toggle-group/toggle-button.d.ts.map +1 -0
  31. package/dist/components/toggle-group/toggle-button.js +115 -0
  32. package/dist/components/toggle-group/toggle-button.js.map +1 -0
  33. package/dist/components/toggle-group/toggle-group.d.ts +32 -0
  34. package/dist/components/toggle-group/toggle-group.d.ts.map +1 -0
  35. package/dist/components/toggle-group/toggle-group.js +150 -0
  36. package/dist/components/toggle-group/toggle-group.js.map +1 -0
  37. package/dist/components/tooltip/tooltip.d.ts +33 -0
  38. package/dist/components/tooltip/tooltip.d.ts.map +1 -0
  39. package/dist/components/tooltip/tooltip.js +293 -0
  40. package/dist/components/tooltip/tooltip.js.map +1 -0
  41. package/dist/styles/index.css +1 -0
  42. package/package.json +29 -1
  43. package/src/components/calendar/calendar.ts +444 -0
  44. package/src/components/cron-builder/cron-builder.ts +452 -0
  45. package/src/components/input/input.ts +38 -14
  46. package/src/components/pin-input/pin-input.ts +249 -0
  47. package/src/components/popover/popover.ts +192 -0
  48. package/src/components/select/select.ts +25 -63
  49. package/src/components/time-picker/time-picker.ts +272 -0
  50. package/src/components/toggle-group/toggle-button.ts +104 -0
  51. package/src/components/toggle-group/toggle-group.ts +148 -0
  52. package/src/components/tooltip/tooltip.ts +286 -0
  53. package/src/styles/theme.css +2 -0
@@ -0,0 +1,249 @@
1
+ import {LitElement, html, css, PropertyValues} from "lit";
2
+ import {customElement, property, queryAll, state} from "lit/decorators.js";
3
+ import {classMap} from "lit/directives/class-map.js";
4
+ import {resetStyles} from "../../styles/reset.js";
5
+
6
+ @customElement("st-pin-input")
7
+ export class StPinInput extends LitElement {
8
+ static formAssociated = true;
9
+ private _internals: ElementInternals;
10
+
11
+ @property({type: String, reflect: true}) name = "";
12
+ @property({type: String}) label = "";
13
+ @property({type: Number}) length = 6;
14
+ @property({type: String}) value = "";
15
+
16
+ // Obscures the input for secure PINs
17
+ @property({type: Boolean}) mask = false;
18
+
19
+ @property({type: Boolean, reflect: true}) disabled = false;
20
+ @property({type: Boolean, reflect: true}) required = false;
21
+ @property({type: Boolean, reflect: true}) invalid = false;
22
+ @property({type: String}) error = "";
23
+
24
+ @queryAll(".pin-box") private _inputs!: NodeListOf<HTMLInputElement>;
25
+
26
+ @state() private _touched = false;
27
+
28
+ constructor() {
29
+ super();
30
+ this._internals = this.attachInternals();
31
+ }
32
+
33
+ static styles = [
34
+ resetStyles,
35
+ css`
36
+ :host {
37
+ display: block;
38
+ font-family: inherit;
39
+ }
40
+
41
+ label {
42
+ display: block;
43
+ font-size: var(--st-font-size-sm);
44
+ font-weight: 500;
45
+ color: var(--st-color-text-muted);
46
+ margin-bottom: var(--st-spacing-xs);
47
+ }
48
+
49
+ .pin-container {
50
+ display: flex;
51
+ gap: var(--st-spacing-sm);
52
+ }
53
+
54
+ .pin-box {
55
+ width: 48px;
56
+ height: 56px;
57
+ text-align: center;
58
+ font-size: var(--st-font-size-xl);
59
+ font-weight: var(--st-font-weight-semibold);
60
+ color: var(--st-color-text);
61
+ background: var(--st-color-surface);
62
+ border: 1px solid var(--st-color-border);
63
+ border-radius: var(--st-radius-md);
64
+ box-sizing: border-box;
65
+ transition:
66
+ border-color 0.2s,
67
+ box-shadow 0.2s,
68
+ background-color 0.2s;
69
+ /* iOS optimization for numeric pads */
70
+ caret-color: var(--st-color-primary);
71
+ }
72
+
73
+ .pin-box:focus {
74
+ outline: none;
75
+ border-color: var(--st-color-primary);
76
+ box-shadow: 0 0 0 2px var(--st-color-primary-muted, rgba(59, 130, 246, 0.25));
77
+ }
78
+
79
+ .pin-box:disabled {
80
+ background: var(--st-color-surface-muted, #f1f5f9);
81
+ color: var(--st-color-text-muted);
82
+ cursor: not-allowed;
83
+ }
84
+
85
+ .is-invalid .pin-box {
86
+ border-color: var(--st-color-danger, #dc2626);
87
+ }
88
+
89
+ .error-msg {
90
+ display: block;
91
+ margin-top: var(--st-spacing-xs);
92
+ font-size: var(--st-font-size-xs);
93
+ color: var(--st-color-danger, #dc2626);
94
+ }
95
+ `,
96
+ ];
97
+
98
+ override updated(changedProperties: PropertyValues) {
99
+ super.updated(changedProperties);
100
+ if (
101
+ changedProperties.has("value") ||
102
+ changedProperties.has("required") ||
103
+ changedProperties.has("error")
104
+ ) {
105
+ this._internals.setFormValue(this.value || null);
106
+ this._syncValidity();
107
+ }
108
+ }
109
+
110
+ private _syncValidity() {
111
+ this.invalid = false;
112
+
113
+ if (this.error) {
114
+ this._internals.setValidity({customError: true}, this.error);
115
+ this.invalid = true;
116
+ return;
117
+ }
118
+
119
+ if (this.required && (this.value.length < this.length || this.value.includes(" "))) {
120
+ this._internals.setValidity({valueMissing: true}, `Please enter all ${this.length} digits.`);
121
+ if (this._touched) this.invalid = true;
122
+ return;
123
+ }
124
+
125
+ this._internals.setValidity({});
126
+ }
127
+
128
+ public checkValidity() {
129
+ return this._internals.checkValidity();
130
+ }
131
+ public reportValidity() {
132
+ this._touched = true;
133
+ this._syncValidity();
134
+ return this._internals.reportValidity();
135
+ }
136
+
137
+ private _updateValue() {
138
+ const chars = Array.from(this._inputs).map((input) => input.value || " ");
139
+ this.value = chars.join("");
140
+
141
+ this.dispatchEvent(new Event("input", {bubbles: true, composed: true}));
142
+
143
+ // Only fire 'change' if the pin is completely filled
144
+ if (!this.value.includes(" ")) {
145
+ this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
146
+ }
147
+ }
148
+
149
+ // Handle typing and auto-advancing
150
+ private _handleInput(e: InputEvent, index: number) {
151
+ this._touched = true;
152
+ const input = e.target as HTMLInputElement;
153
+
154
+ // If the user typed a character, move to the next input
155
+ if (input.value && index < this.length - 1) {
156
+ this._inputs[index + 1].focus();
157
+ }
158
+
159
+ this._updateValue();
160
+ }
161
+
162
+ // Handle backspace rewinding and arrow keys
163
+ private _handleKeyDown(e: KeyboardEvent, index: number) {
164
+ const input = e.target as HTMLInputElement;
165
+
166
+ if (e.key === "Backspace") {
167
+ // If current box is empty, jump to previous box and clear it
168
+ if (!input.value && index > 0) {
169
+ e.preventDefault();
170
+ this._inputs[index - 1].value = "";
171
+ this._inputs[index - 1].focus();
172
+ this._updateValue();
173
+ }
174
+ } else if (e.key === "ArrowLeft" && index > 0) {
175
+ e.preventDefault();
176
+ this._inputs[index - 1].focus();
177
+ } else if (e.key === "ArrowRight" && index < this.length - 1) {
178
+ e.preventDefault();
179
+ this._inputs[index + 1].focus();
180
+ }
181
+ }
182
+
183
+ // Handle cross-field pasting (e.g. pasting "123456" into the first box)
184
+ private _handlePaste(e: ClipboardEvent, startIndex: number) {
185
+ e.preventDefault();
186
+ this._touched = true;
187
+
188
+ const pasteData = e.clipboardData?.getData("text") || "";
189
+ // Clean the string (optional: restrict to numbers if you want strict numeric PINs)
190
+ const chars = pasteData.replace(/\s/g, "").split("");
191
+
192
+ if (!chars.length) return;
193
+
194
+ let currentIndex = startIndex;
195
+ for (const char of chars) {
196
+ if (currentIndex < this.length) {
197
+ this._inputs[currentIndex].value = char;
198
+ currentIndex++;
199
+ }
200
+ }
201
+
202
+ // Focus the last filled input (or the very last input if we filled them all)
203
+ const focusIndex = Math.min(currentIndex, this.length - 1);
204
+ this._inputs[focusIndex].focus();
205
+
206
+ this._updateValue();
207
+ }
208
+
209
+ render() {
210
+ // Break the current value into an array of characters for rendering
211
+ const values = this.value.padEnd(this.length, " ").split("");
212
+
213
+ const isInvalid = this.invalid || !!this.error;
214
+
215
+ return html`
216
+ ${this.label ? html`<label>${this.label}</label>` : ""}
217
+
218
+ <div class=${classMap({"pin-container": true, "is-invalid": isInvalid})}>
219
+ ${values
220
+ .slice(0, this.length)
221
+ .map(
222
+ (char, index) => html`
223
+ <input
224
+ class="pin-box"
225
+ aria-label="Digit ${index + 1}"
226
+ type=${this.mask ? "password" : "text"}
227
+ inputmode="numeric"
228
+ autocomplete="${"one-time-code" as any}"
229
+ maxlength="1"
230
+ .value=${char.trim()}
231
+ ?disabled=${this.disabled}
232
+ @input=${(e: InputEvent) => this._handleInput(e, index)}
233
+ @keydown=${(e: KeyboardEvent) => this._handleKeyDown(e, index)}
234
+ @paste=${(e: ClipboardEvent) => this._handlePaste(e, index)}
235
+ />
236
+ `,
237
+ )}
238
+ </div>
239
+
240
+ ${isInvalid && this.error ? html`<span class="error-msg">${this.error}</span>` : ""}
241
+ `;
242
+ }
243
+ }
244
+
245
+ declare global {
246
+ interface HTMLElementTagNameMap {
247
+ "st-pin-input": StPinInput;
248
+ }
249
+ }
@@ -0,0 +1,192 @@
1
+ import {LitElement, html, css, PropertyValues} from "lit";
2
+ import {customElement, property, query, state, queryAssignedElements} from "lit/decorators.js";
3
+ import {resetStyles} from "../../styles/reset.js";
4
+
5
+ export type PopoverPosition = "top" | "bottom" | "left" | "right";
6
+
7
+ @customElement("st-popover")
8
+ export class StPopover extends LitElement {
9
+ @property({type: String}) position: PopoverPosition = "bottom";
10
+ @property({type: Number}) offset = 8;
11
+ @property({type: Boolean, reflect: true}) disabled = false;
12
+
13
+ @state() private _isOpen = false;
14
+
15
+ @query(".trigger-wrapper") private _triggerWrapper!: HTMLElement;
16
+ @query("#popover-content") private _popoverEl!: HTMLElement;
17
+
18
+ @queryAssignedElements({slot: "trigger"}) private _triggerElements!: HTMLElement[];
19
+
20
+ static styles = [
21
+ resetStyles,
22
+ css`
23
+ :host {
24
+ display: inline-block;
25
+ }
26
+
27
+ .trigger-wrapper {
28
+ display: inline-block;
29
+ cursor: pointer;
30
+ }
31
+
32
+ /* Native popover styles */
33
+ [popover] {
34
+ position: fixed;
35
+ margin: 0;
36
+ padding: 0;
37
+ border: none;
38
+ background: transparent;
39
+ overflow: visible;
40
+ /* 🚀 FIX: Erase the browser's native centering inset so our JS coordinates work */
41
+ inset: auto;
42
+ }
43
+
44
+ .popover-surface {
45
+ background: var(--st-color-surface);
46
+ border: 1px solid var(--st-color-border);
47
+ border-radius: var(--st-radius-md);
48
+ box-shadow:
49
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
50
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
51
+ padding: var(--st-spacing-md);
52
+ color: var(--st-color-text);
53
+
54
+ opacity: 0;
55
+ transform: scale(0.95);
56
+ transition:
57
+ opacity 0.15s ease,
58
+ transform 0.15s ease;
59
+ }
60
+
61
+ [popover]:popover-open .popover-surface {
62
+ opacity: 1;
63
+ transform: scale(1);
64
+ }
65
+
66
+ [popover]::backdrop {
67
+ background: transparent;
68
+ }
69
+ `,
70
+ ];
71
+
72
+ override updated(changedProperties: PropertyValues) {
73
+ super.updated(changedProperties);
74
+ if (changedProperties.has("_isOpen")) {
75
+ this._triggerElements?.forEach((el) => {
76
+ el.setAttribute("aria-expanded", String(this._isOpen));
77
+ });
78
+ }
79
+ }
80
+
81
+ private _handleTriggerSlotChange() {
82
+ this._triggerElements?.forEach((el) => {
83
+ el.setAttribute("aria-haspopup", "dialog");
84
+ el.setAttribute("aria-expanded", String(this._isOpen));
85
+ });
86
+ }
87
+
88
+ private _togglePopover() {
89
+ if (this.disabled) return;
90
+ this._isOpen ? this.hide() : this.show();
91
+ }
92
+
93
+ public show() {
94
+ if (this.disabled || this._isOpen) return;
95
+
96
+ // 🚀 FIX: Update state synchronously so the _reposition check passes instantly
97
+ this._isOpen = true;
98
+
99
+ this._popoverEl.showPopover();
100
+ this._reposition();
101
+
102
+ window.addEventListener("resize", this._reposition);
103
+ window.addEventListener("scroll", this._reposition, true);
104
+ }
105
+
106
+ public hide() {
107
+ if (!this._isOpen) return;
108
+
109
+ // 🚀 FIX: Update state synchronously
110
+ this._isOpen = false;
111
+
112
+ this._popoverEl.hidePopover();
113
+ }
114
+
115
+ private _handleNativeToggle(e: Event) {
116
+ const isOpening = (e as any).newState === "open";
117
+ this._isOpen = isOpening;
118
+
119
+ // This handles the cleanup when the user clicks outside or hits ESC
120
+ if (!isOpening) {
121
+ window.removeEventListener("resize", this._reposition);
122
+ window.removeEventListener("scroll", this._reposition, true);
123
+ }
124
+ }
125
+
126
+ private _reposition = () => {
127
+ if (!this._isOpen || !this._popoverEl || !this._triggerWrapper) return;
128
+
129
+ const triggerRect = this._triggerWrapper.getBoundingClientRect();
130
+ const popoverRect = this._popoverEl.getBoundingClientRect();
131
+
132
+ let top = 0;
133
+ let left = 0;
134
+
135
+ switch (this.position) {
136
+ case "bottom":
137
+ top = triggerRect.bottom + this.offset;
138
+ left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
139
+ break;
140
+ case "top":
141
+ top = triggerRect.top - popoverRect.height - this.offset;
142
+ left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
143
+ break;
144
+ case "right":
145
+ top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
146
+ left = triggerRect.right + this.offset;
147
+ break;
148
+ case "left":
149
+ top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
150
+ left = triggerRect.left - popoverRect.width - this.offset;
151
+ break;
152
+ }
153
+
154
+ const padding = 8;
155
+ if (left < padding) left = padding;
156
+ if (left + popoverRect.width > window.innerWidth - padding) {
157
+ left = window.innerWidth - popoverRect.width - padding;
158
+ }
159
+ if (top < padding) top = padding;
160
+ if (top + popoverRect.height > window.innerHeight - padding) {
161
+ top = window.innerHeight - popoverRect.height - padding;
162
+ }
163
+
164
+ this._popoverEl.style.top = `${top}px`;
165
+ this._popoverEl.style.left = `${left}px`;
166
+ };
167
+
168
+ render() {
169
+ return html`
170
+ <div
171
+ class="trigger-wrapper"
172
+ @click=${this._togglePopover}
173
+ @keydown=${(e: KeyboardEvent) =>
174
+ e.key === "Enter" || e.key === " " ? this._togglePopover() : null}
175
+ >
176
+ <slot name="trigger" @slotchange=${this._handleTriggerSlotChange}></slot>
177
+ </div>
178
+
179
+ <div id="popover-content" popover="auto" @toggle=${this._handleNativeToggle}>
180
+ <div class="popover-surface">
181
+ <slot name="content"></slot>
182
+ </div>
183
+ </div>
184
+ `;
185
+ }
186
+ }
187
+
188
+ declare global {
189
+ interface HTMLElementTagNameMap {
190
+ "st-popover": StPopover;
191
+ }
192
+ }
@@ -1,71 +1,14 @@
1
1
  import {LitElement, html, css, PropertyValues} from "lit";
2
2
  import {customElement, property, state, query} from "lit/decorators.js";
3
3
  import {classMap} from "lit/directives/class-map.js";
4
+ import {ifDefined} from "lit/directives/if-defined.js";
4
5
 
5
6
  export interface SelectOption {
6
7
  label: string;
7
8
  value: string;
8
9
  }
9
10
 
10
- export type SelectAutocomplete =
11
- | "on"
12
- | "off"
13
- | "name"
14
- | "honorific-prefix"
15
- | "given-name"
16
- | "additional-name"
17
- | "family-name"
18
- | "honorific-suffix"
19
- | "nickname"
20
- | "email"
21
- | "username"
22
- | "new-password"
23
- | "current-password"
24
- | "one-time-code"
25
- | "organization"
26
- | "organization-title"
27
- | "url"
28
- | "photo"
29
- | "webauthn"
30
- | "street-address"
31
- | "address-line1"
32
- | "address-line2"
33
- | "address-line3"
34
- | "address-level1"
35
- | "address-level2"
36
- | "address-level3"
37
- | "address-level4"
38
- | "country"
39
- | "country-name"
40
- | "postal-code"
41
- | "cc-name"
42
- | "cc-given-name"
43
- | "cc-additional-name"
44
- | "cc-family-name"
45
- | "cc-number"
46
- | "cc-exp"
47
- | "cc-exp-month"
48
- | "cc-exp-year"
49
- | "cc-csc"
50
- | "cc-type"
51
- | "transaction-currency"
52
- | "transaction-amount"
53
- | "language"
54
- | "bday"
55
- | "bday-day"
56
- | "bday-month"
57
- | "bday-year"
58
- | "sex"
59
- | "tel"
60
- | "tel-country-code"
61
- | "tel-national"
62
- | "tel-area-code"
63
- | "tel-local"
64
- | "tel-local-prefix"
65
- | "tel-local-suffix"
66
- | "tel-extension"
67
- | "impp"
68
- | (string & {});
11
+ export type SelectAutocomplete = "on" | "off" | "name" | "email" | (string & {});
69
12
 
70
13
  @customElement("st-select")
71
14
  export class StSelect extends LitElement {
@@ -74,6 +17,7 @@ export class StSelect extends LitElement {
74
17
 
75
18
  @property({type: String}) name = "";
76
19
  @property({type: String}) label = "";
20
+ @property({type: Boolean, attribute: "hide-label"}) hideLabel = false;
77
21
  @property({type: String}) value = "";
78
22
  @property({type: Boolean}) required = false;
79
23
  @property({type: Boolean}) multiple = false;
@@ -93,6 +37,9 @@ export class StSelect extends LitElement {
93
37
  @query(".combobox") private buttonElement!: HTMLElement;
94
38
  @query(".search-input") private searchInputEl?: HTMLInputElement;
95
39
 
40
+ private _labelId = `select-label-${Math.random().toString(36).substring(2, 9)}`;
41
+ private _listboxId = `select-listbox-${Math.random().toString(36).substring(2, 9)}`;
42
+
96
43
  static styles = css`
97
44
  :host {
98
45
  display: block;
@@ -108,6 +55,17 @@ export class StSelect extends LitElement {
108
55
  margin-bottom: var(--st-spacing-xs);
109
56
  }
110
57
 
58
+ .sr-only {
59
+ position: absolute;
60
+ width: 1px;
61
+ height: 1px;
62
+ padding: 0;
63
+ margin: -1px;
64
+ overflow: hidden;
65
+ clip: rect(0, 0, 0, 0);
66
+ border: 0;
67
+ }
68
+
111
69
  .combobox {
112
70
  display: flex;
113
71
  justify-content: space-between;
@@ -413,7 +371,11 @@ export class StSelect extends LitElement {
413
371
  : this.options;
414
372
 
415
373
  return html`
416
- ${this.label ? html`<label id="select-label">${this.label}</label>` : ""}
374
+ ${this.label
375
+ ? html`<label id=${this._labelId} class=${classMap({"sr-only": this.hideLabel})}
376
+ >${this.label}</label
377
+ >`
378
+ : ""}
417
379
 
418
380
  <button
419
381
  type="button"
@@ -426,8 +388,8 @@ export class StSelect extends LitElement {
426
388
  role="combobox"
427
389
  aria-haspopup="listbox"
428
390
  aria-expanded="${this.open}"
429
- aria-labelledby="select-label"
430
- aria-controls="select-listbox"
391
+ aria-labelledby=${ifDefined(this.label ? this._labelId : undefined)}
392
+ aria-controls=${this._listboxId}
431
393
  @click=${this.toggleOpen}
432
394
  @keydown=${this.handleButtonKeydown}
433
395
  >
@@ -470,7 +432,7 @@ export class StSelect extends LitElement {
470
432
  : ""}
471
433
 
472
434
  <ul
473
- id="select-listbox"
435
+ id=${this._listboxId}
474
436
  class="listbox"
475
437
  role="listbox"
476
438
  aria-multiselectable="${this.multiple}"