@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,452 @@
1
+ import {LitElement, html, css, PropertyValues} from "lit";
2
+ import {customElement, property, state} from "lit/decorators.js";
3
+ import {resetStyles} from "../../styles/reset";
4
+
5
+ @customElement("st-cron-builder")
6
+ export class StCronBuilder extends LitElement {
7
+ static formAssociated = true;
8
+ private _internals: ElementInternals;
9
+
10
+ @property({type: String, reflect: true}) name = "";
11
+ @property({type: String}) value = "";
12
+ @property({type: Boolean, reflect: true}) required = false;
13
+
14
+ @state() private _frequency = "weekly";
15
+ @state() private _time = "12:00";
16
+ @state() private _days = "1,3,5";
17
+ @state() private _monthDay = "1";
18
+ @state() private _date = "";
19
+
20
+ @state() private _customCron = "0 * * * *";
21
+ @state() private _customCronError = "";
22
+ @state() private _customCronText = "";
23
+
24
+ private _defaultValue = "";
25
+
26
+ constructor() {
27
+ super();
28
+ this._internals = this.attachInternals();
29
+
30
+ const today = new Date();
31
+ const pad = (n: number) => n.toString().padStart(2, "0");
32
+ this._date = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
33
+ }
34
+
35
+ static styles = [
36
+ resetStyles,
37
+ css`
38
+ :host {
39
+ display: block;
40
+ background: var(--st-color-surface);
41
+ border: 1px solid var(--st-color-border);
42
+ border-radius: var(--st-radius-lg);
43
+ padding: var(--st-spacing-lg);
44
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
45
+ }
46
+
47
+ .builder-grid {
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: var(--st-spacing-lg);
51
+ }
52
+
53
+ .row {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: var(--st-spacing-xs);
57
+ }
58
+
59
+ label {
60
+ font-size: var(--st-font-size-sm);
61
+ font-weight: 600;
62
+ color: var(--st-color-text);
63
+ }
64
+
65
+ .helper-text {
66
+ font-size: var(--st-font-size-xs);
67
+ color: var(--st-color-text-muted);
68
+ margin-top: 4px;
69
+ }
70
+
71
+ .cron-segments {
72
+ display: flex;
73
+ gap: 4px;
74
+ margin-top: var(--st-spacing-xs);
75
+ margin-bottom: var(--st-spacing-xs);
76
+ }
77
+
78
+ .segment-badge {
79
+ flex: 1;
80
+ text-align: center;
81
+ font-size: 11px;
82
+ padding: 4px;
83
+ background: var(--st-color-surface-muted, #f1f5f9);
84
+ border-radius: var(--st-radius-sm);
85
+ color: var(--st-color-text-muted);
86
+ border: 1px solid var(--st-color-border);
87
+ transition: all 0.2s;
88
+ }
89
+
90
+ .segment-badge.active {
91
+ background: var(--st-color-primary, #eff6ff);
92
+ color: var(--st-color-surface);
93
+ border-color: var(--st-color-primary);
94
+ font-weight: 600;
95
+ }
96
+
97
+ .success-text {
98
+ color: var(--st-color-success, #16a34a);
99
+ }
100
+
101
+ .error-text {
102
+ color: var(--st-color-danger, #dc2626);
103
+ font-weight: 500;
104
+ }
105
+
106
+ .output-preview {
107
+ margin-top: var(--st-spacing-md);
108
+ padding: var(--st-spacing-md);
109
+ background: var(--st-color-surface-muted, #f8fafc);
110
+ border-radius: var(--st-radius-md);
111
+ border: 1px dashed var(--st-color-border);
112
+ }
113
+
114
+ .output-preview code {
115
+ font-family: monospace;
116
+ color: var(--st-color-primary);
117
+ font-weight: bold;
118
+ font-size: var(--st-font-size-lg);
119
+ }
120
+ `,
121
+ ];
122
+
123
+ override firstUpdated() {
124
+ this._generateOutput();
125
+ this._defaultValue = this.value;
126
+ }
127
+
128
+ private _translateCron(cron: string): {valid: boolean; text: string} {
129
+ const parts = cron.trim().split(/\s+/);
130
+
131
+ if (parts.length !== 5) {
132
+ return {
133
+ valid: false,
134
+ text: `A cron expression must contain exactly 5 segments (currently ${parts.length}).`,
135
+ };
136
+ }
137
+
138
+ const types = ["Minute", "Hour", "Day of Month", "Month", "Day of Week"];
139
+
140
+ const bounds = [
141
+ {min: 0, max: 59}, // Minute
142
+ {min: 0, max: 23}, // Hour
143
+ {min: 1, max: 31}, // DOM
144
+ {min: 1, max: 12}, // Month
145
+ {min: 0, max: 7}, // DOW
146
+ ];
147
+
148
+ for (let i = 0; i < 5; i++) {
149
+ const part = parts[i];
150
+ const type = types[i];
151
+ const bound = bounds[i];
152
+
153
+ // Prevent consecutive or hanging symbols like 1- or */
154
+ if (/(^[\/\-\,])|([\/\-\,]$)|([\/\-\,]{2,})/.test(part)) {
155
+ return {valid: false, text: `Invalid formatting in ${type} segment.`};
156
+ }
157
+
158
+ // First 3 segments (min, hour, dom) cannot have letters
159
+ if (i < 3 && /[A-Za-z]/.test(part)) {
160
+ return {valid: false, text: `${type} cannot contain letters.`};
161
+ }
162
+
163
+ // Last 2 segments can only have specific valid keywords
164
+ if (/[A-Za-z]/.test(part)) {
165
+ const words = part.match(/[A-Za-z]+/g) || [];
166
+ const validWords =
167
+ i === 3
168
+ ? ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
169
+ : ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
170
+
171
+ for (const word of words) {
172
+ if (!validWords.includes(word.toUpperCase())) {
173
+ return {valid: false, text: `"${word}" is not a valid ${type}.`};
174
+ }
175
+ }
176
+ }
177
+
178
+ const numbers = part.match(/\d+/g);
179
+ if (numbers) {
180
+ for (const numStr of numbers) {
181
+ const num = parseInt(numStr, 10);
182
+ if (num < bound.min || num > bound.max) {
183
+ return {
184
+ valid: false,
185
+ text: `${type} values must be between ${bound.min} and ${bound.max}.`,
186
+ };
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ const [min, hour, dom, mon, dow] = parts;
193
+ let desc = "Runs ";
194
+
195
+ if (min === "*") desc += "every minute";
196
+ else if (min.startsWith("*/")) desc += `every ${min.substring(2)} minutes`;
197
+ else desc += `at minute ${min}`;
198
+
199
+ if (hour !== "*") {
200
+ if (hour.startsWith("*/")) desc += `, past every ${hour.substring(2)} hours`;
201
+ else desc += `, past hour ${hour}`;
202
+ }
203
+
204
+ if (dom !== "*") {
205
+ if (dom.startsWith("*/")) desc += `, every ${dom.substring(2)} days`;
206
+ else desc += `, on day ${dom} of the month`;
207
+ }
208
+
209
+ if (mon !== "*") {
210
+ if (mon.startsWith("*/")) desc += `, every ${mon.substring(2)} months`;
211
+ else desc += `, in month ${mon}`;
212
+ }
213
+
214
+ if (dow !== "*") {
215
+ const days = [
216
+ "Sunday",
217
+ "Monday",
218
+ "Tuesday",
219
+ "Wednesday",
220
+ "Thursday",
221
+ "Friday",
222
+ "Saturday",
223
+ "Sunday",
224
+ ];
225
+ let dayStr = dow;
226
+
227
+ if (dow.length === 1 && /[0-7]/.test(dow)) {
228
+ dayStr = days[parseInt(dow, 10)];
229
+ } else if (dow.includes("-") && /[0-7]-[0-7]/.test(dow)) {
230
+ dayStr = dow
231
+ .split("-")
232
+ .map((d) => days[parseInt(d, 10)])
233
+ .join(" through ");
234
+ }
235
+ desc += `, on ${dayStr}`;
236
+ }
237
+
238
+ return {valid: true, text: desc + "."};
239
+ }
240
+
241
+ private _generateOutput() {
242
+ let output = "";
243
+ this._customCronError = "";
244
+ this._customCronText = "";
245
+ this._internals.setValidity({});
246
+
247
+ if (this._frequency === "custom") {
248
+ const analysis = this._translateCron(this._customCron);
249
+
250
+ if (!analysis.valid) {
251
+ this._customCronError = analysis.text;
252
+ this._internals.setValidity({customError: true}, analysis.text);
253
+ } else {
254
+ this._customCronText = analysis.text;
255
+ }
256
+ output = this._customCron;
257
+ } else if (this._frequency === "one-off") {
258
+ output = `${this._date}T${this._time}`;
259
+ } else {
260
+ const [hh, mm] = this._time.split(":");
261
+ const minute = mm ? parseInt(mm, 10).toString() : "0";
262
+ const hour = hh ? parseInt(hh, 10).toString() : "0";
263
+
264
+ switch (this._frequency) {
265
+ case "daily":
266
+ output = `${minute} ${hour} * * *`;
267
+ break;
268
+ case "weekly":
269
+ const safeDays = this._days || "1";
270
+ output = `${minute} ${hour} * * ${safeDays}`;
271
+ break;
272
+ case "monthly":
273
+ output = `${minute} ${hour} ${this._monthDay} * *`;
274
+ break;
275
+ }
276
+ }
277
+
278
+ this.value = output;
279
+ this._internals.setFormValue(output);
280
+ this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
281
+ }
282
+
283
+ private _handleCustomCronInput(e: any) {
284
+ const inputEl = e.target;
285
+ const sanitized = inputEl.value.replace(/[^0-9A-Za-z\s\*\/\-\,]/g, "");
286
+
287
+ if (inputEl.value !== sanitized) {
288
+ inputEl.value = sanitized;
289
+ }
290
+
291
+ this._customCron = sanitized;
292
+ this._generateOutput();
293
+ }
294
+
295
+ private _handleFrequencyChange(e: any) {
296
+ this._frequency = e.target.value;
297
+ this._generateOutput();
298
+ }
299
+
300
+ public checkValidity() {
301
+ return this._internals.checkValidity();
302
+ }
303
+ public reportValidity() {
304
+ return this._internals.reportValidity();
305
+ }
306
+
307
+ formResetCallback() {
308
+ this.value = this._defaultValue;
309
+ this._frequency = "weekly";
310
+ this._generateOutput();
311
+ }
312
+
313
+ render() {
314
+ const frequencyOptions = [
315
+ {label: "One-Off Execution", value: "one-off"},
316
+ {label: "Daily", value: "daily"},
317
+ {label: "Weekly", value: "weekly"},
318
+ {label: "Monthly", value: "monthly"},
319
+ {label: "Custom Cron", value: "custom"},
320
+ ];
321
+
322
+ const monthDayOptions = Array.from({length: 31}, (_, i) => ({
323
+ label: `${i + 1}${i === 0 ? "st" : i === 1 ? "nd" : i === 2 ? "rd" : "th"}`,
324
+ value: String(i + 1),
325
+ }));
326
+
327
+ // Calculate which segment the user is currently typing in
328
+ const activeSegmentIndex = Math.min(this._customCron.trimStart().split(/\s+/).length - 1, 4);
329
+
330
+ return html`
331
+ <div class="builder-grid">
332
+ <div class="row">
333
+ <st-select
334
+ label="Schedule Type"
335
+ .options=${frequencyOptions}
336
+ .value=${this._frequency}
337
+ @change=${this._handleFrequencyChange}
338
+ ></st-select>
339
+ </div>
340
+
341
+ ${this._frequency === "weekly"
342
+ ? html`
343
+ <div class="row">
344
+ <label>Run on days</label>
345
+ <st-toggle-group
346
+ multiple
347
+ .value=${this._days}
348
+ @change=${(e: any) => {
349
+ this._days = e.target.value;
350
+ this._generateOutput();
351
+ }}
352
+ >
353
+ <st-toggle-button value="1">Mon</st-toggle-button>
354
+ <st-toggle-button value="2">Tue</st-toggle-button>
355
+ <st-toggle-button value="3">Wed</st-toggle-button>
356
+ <st-toggle-button value="4">Thu</st-toggle-button>
357
+ <st-toggle-button value="5">Fri</st-toggle-button>
358
+ <st-toggle-button value="6">Sat</st-toggle-button>
359
+ <st-toggle-button value="0">Sun</st-toggle-button>
360
+ </st-toggle-group>
361
+ </div>
362
+ `
363
+ : ""}
364
+ ${this._frequency === "monthly"
365
+ ? html`
366
+ <div class="row">
367
+ <st-select
368
+ label="Run on day of month"
369
+ .options=${monthDayOptions}
370
+ .value=${this._monthDay}
371
+ @change=${(e: any) => {
372
+ this._monthDay = e.target.value;
373
+ this._generateOutput();
374
+ }}
375
+ ></st-select>
376
+ </div>
377
+ `
378
+ : ""}
379
+ ${this._frequency === "one-off"
380
+ ? html`
381
+ <div class="row">
382
+ <label>Execution Date</label>
383
+ <st-calendar
384
+ .value=${this._date}
385
+ @change=${(e: any) => {
386
+ this._date = e.target.value;
387
+ this._generateOutput();
388
+ }}
389
+ ></st-calendar>
390
+ </div>
391
+ `
392
+ : ""}
393
+ ${this._frequency === "custom"
394
+ ? html`
395
+ <div class="row">
396
+ <st-input
397
+ label="Raw Cron Expression"
398
+ placeholder="*/15 * * * *"
399
+ .value=${this._customCron}
400
+ .error=${this._customCronError}
401
+ ?invalid=${!!this._customCronError}
402
+ @input=${this._handleCustomCronInput}
403
+ ></st-input>
404
+ <div class="cron-segments" aria-hidden="true">
405
+ ${["Minute", "Hour", "Day", "Month", "Weekday"].map(
406
+ (label, i) => html`
407
+ <div class="segment-badge ${activeSegmentIndex === i ? "active" : ""}">
408
+ ${label}
409
+ </div>
410
+ `,
411
+ )}
412
+ </div>
413
+
414
+ ${this._customCronText && !this._customCronError
415
+ ? html`
416
+ <div class="helper-text success-text">
417
+ <strong>Schedule:</strong> ${this._customCronText}
418
+ </div>
419
+ `
420
+ : ""}
421
+ </div>
422
+ `
423
+ : ""}
424
+ ${this._frequency !== "custom"
425
+ ? html`
426
+ <div class="row">
427
+ <st-time-picker
428
+ label="Execution Time"
429
+ .value=${this._time}
430
+ @change=${(e: any) => {
431
+ this._time = e.target.value;
432
+ this._generateOutput();
433
+ }}
434
+ ></st-time-picker>
435
+ </div>
436
+ `
437
+ : ""}
438
+
439
+ <div class="output-preview">
440
+ <div class="helper-text">Generated Payload</div>
441
+ <code>${this.value}</code>
442
+ </div>
443
+ </div>
444
+ `;
445
+ }
446
+ }
447
+
448
+ declare global {
449
+ interface HTMLElementTagNameMap {
450
+ "st-cron-builder": StCronBuilder;
451
+ }
452
+ }
@@ -18,6 +18,7 @@ export class StInput extends LitElement {
18
18
  @property({type: String}) placeholder = "";
19
19
  @property({type: String}) type: InputType = "text";
20
20
  @property({attribute: "help-text"}) helpText = "";
21
+ @property({type: String}) error = "";
21
22
  @property({ type: String }) autocomplete?: string;
22
23
 
23
24
  @property({type: Boolean, reflect: true}) required = false;
@@ -34,7 +35,7 @@ export class StInput extends LitElement {
34
35
  this._internals = this.attachInternals();
35
36
 
36
37
  this.addEventListener("focusout", () => {
37
- this.invalid = !this._inputEl.validity.valid;
38
+ this.invalid = !!this.error || !this._inputEl.validity.valid;
38
39
  });
39
40
 
40
41
  this.addEventListener("invalid", () => {
@@ -115,16 +116,24 @@ export class StInput extends LitElement {
115
116
  margin: 0;
116
117
  }
117
118
 
119
+ /* 🚀 NEW: Explicit error text styling */
120
+ .error-text {
121
+ font-size: var(--st-font-size-xs);
122
+ color: var(--st-color-error, #dc2626);
123
+ margin: 0;
124
+ font-weight: 500;
125
+ }
126
+
118
127
  :host([invalid]) .input-wrapper {
119
- border-color: var(--st-color-error);
128
+ border-color: var(--st-color-error, #dc2626);
120
129
  }
121
130
 
122
131
  :host([invalid]) .input-wrapper:focus-within {
123
- box-shadow: 0 0 0 1px var(--st-color-error);
132
+ box-shadow: 0 0 0 1px var(--st-color-error, #dc2626);
124
133
  }
125
134
 
126
135
  :host([invalid]) .help-text {
127
- color: var(--st-color-error);
136
+ color: var(--st-color-error, #dc2626);
128
137
  }
129
138
 
130
139
  :host([disabled]) .input-wrapper {
@@ -149,16 +158,30 @@ export class StInput extends LitElement {
149
158
  if (changedProperties.has("value")) {
150
159
  this._internals.setFormValue(this.value);
151
160
  }
152
- this._syncValidity();
161
+
162
+ if (changedProperties.has("value") || changedProperties.has("error") || changedProperties.has("required")) {
163
+ this._syncValidity();
164
+ }
153
165
  }
154
166
 
155
167
  private _syncValidity() {
156
168
  if (!this._inputEl) return;
169
+
170
+ if (this.error) {
171
+ this._internals.setValidity({ customError: true }, this.error, this._inputEl);
172
+ this.invalid = true;
173
+ return;
174
+ }
175
+
157
176
  this._internals.setValidity(
158
177
  this._inputEl.validity,
159
178
  this._inputEl.validationMessage,
160
179
  this._inputEl,
161
180
  );
181
+
182
+ if (this.invalid && this._inputEl.validity.valid) {
183
+ this.invalid = false;
184
+ }
162
185
  }
163
186
 
164
187
  formResetCallback() {
@@ -179,7 +202,7 @@ export class StInput extends LitElement {
179
202
  const target = e.target as HTMLInputElement;
180
203
  this.value = target.value;
181
204
 
182
- if (this.invalid && target.validity.valid) {
205
+ if (this.invalid && target.validity.valid && !this.error) {
183
206
  this.invalid = false;
184
207
  }
185
208
 
@@ -190,10 +213,9 @@ export class StInput extends LitElement {
190
213
  this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
191
214
  }
192
215
 
193
- render() {
194
- const helpTextId = this.helpText ? "help-text" : undefined;
216
+ render() {
217
+ const descriptionId = this.error ? "error-text" : (this.helpText ? "help-text" : undefined);
195
218
 
196
- // Security: Prevent OS/Extensions from logging sensitive data via spellcheck
197
219
  const isSensitive = this.type === "password" || this.type === "email";
198
220
  const safeSpellcheck = isSensitive ? "false" : undefined;
199
221
  const safeAutocapitalize = isSensitive ? "none" : undefined;
@@ -221,7 +243,7 @@ render() {
221
243
  spellcheck=${ifDefined(safeSpellcheck)}
222
244
  autocapitalize=${ifDefined(safeAutocapitalize)}
223
245
  aria-invalid=${this.invalid ? "true" : "false"}
224
- aria-describedby=${ifDefined(helpTextId)}
246
+ aria-describedby=${ifDefined(descriptionId)}
225
247
  @input=${this._handleInput}
226
248
  @change=${this._handleChange}
227
249
  />
@@ -229,9 +251,11 @@ render() {
229
251
  <slot name="suffix"></slot>
230
252
  </div>
231
253
 
232
- ${this.helpText
233
- ? html`<div id="help-text" class="help-text">${this.helpText}</div>`
234
- : ""}
254
+ ${this.error
255
+ ? html`<div id="error-text" class="error-text">${this.error}</div>`
256
+ : this.helpText
257
+ ? html`<div id="help-text" class="help-text">${this.helpText}</div>`
258
+ : ""}
235
259
  </div>
236
260
  `;
237
261
  }
@@ -241,4 +265,4 @@ declare global {
241
265
  interface HTMLElementTagNameMap {
242
266
  "st-input": StInput;
243
267
  }
244
- }
268
+ }