@sondor-technology/sondor-components 1.0.7 → 1.0.9
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,529 @@
|
|
|
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
|
+
private _lastGeneratedOutput = "";
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this._internals = this.attachInternals();
|
|
30
|
+
|
|
31
|
+
const today = new Date();
|
|
32
|
+
const pad = (n: number) => n.toString().padStart(2, "0");
|
|
33
|
+
this._date = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static styles = [
|
|
37
|
+
resetStyles,
|
|
38
|
+
css`
|
|
39
|
+
:host {
|
|
40
|
+
display: block;
|
|
41
|
+
background: var(--st-color-surface);
|
|
42
|
+
border: 1px solid var(--st-color-border);
|
|
43
|
+
border-radius: var(--st-radius-lg);
|
|
44
|
+
padding: var(--st-spacing-lg);
|
|
45
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.builder-grid {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: var(--st-spacing-lg);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.row {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
gap: var(--st-spacing-xs);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
label {
|
|
61
|
+
font-size: var(--st-font-size-sm);
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
color: var(--st-color-text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.helper-text {
|
|
67
|
+
font-size: var(--st-font-size-xs);
|
|
68
|
+
color: var(--st-color-text-muted);
|
|
69
|
+
margin-top: 4px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.cron-segments {
|
|
73
|
+
display: flex;
|
|
74
|
+
gap: 4px;
|
|
75
|
+
margin-top: var(--st-spacing-xs);
|
|
76
|
+
margin-bottom: var(--st-spacing-xs);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.segment-badge {
|
|
80
|
+
flex: 1;
|
|
81
|
+
text-align: center;
|
|
82
|
+
font-size: 11px;
|
|
83
|
+
padding: 4px;
|
|
84
|
+
background: var(--st-color-surface-muted, #f1f5f9);
|
|
85
|
+
border-radius: var(--st-radius-sm);
|
|
86
|
+
color: var(--st-color-text-muted);
|
|
87
|
+
border: 1px solid var(--st-color-border);
|
|
88
|
+
transition: all 0.2s;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.segment-badge.active {
|
|
92
|
+
background: var(--st-color-primary, #eff6ff);
|
|
93
|
+
color: var(--st-color-surface);
|
|
94
|
+
border-color: var(--st-color-primary);
|
|
95
|
+
font-weight: 600;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.success-text {
|
|
99
|
+
color: var(--st-color-success, #16a34a);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.error-text {
|
|
103
|
+
color: var(--st-color-danger, #dc2626);
|
|
104
|
+
font-weight: 500;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.output-preview {
|
|
108
|
+
margin-top: var(--st-spacing-md);
|
|
109
|
+
padding: var(--st-spacing-md);
|
|
110
|
+
background: var(--st-color-surface-muted, #f8fafc);
|
|
111
|
+
border-radius: var(--st-radius-md);
|
|
112
|
+
border: 1px dashed var(--st-color-border);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.output-preview code {
|
|
116
|
+
font-family: monospace;
|
|
117
|
+
color: var(--st-color-primary);
|
|
118
|
+
font-weight: bold;
|
|
119
|
+
font-size: var(--st-font-size-lg);
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
override willUpdate(changedProperties: PropertyValues) {
|
|
125
|
+
super.willUpdate(changedProperties);
|
|
126
|
+
|
|
127
|
+
// Check if value changed, is truthy (not ""), and wasn't generated by the component itself
|
|
128
|
+
if (changedProperties.has("value") && this.value && this.value !== this._lastGeneratedOutput) {
|
|
129
|
+
this._parseIncomingValue(this.value);
|
|
130
|
+
this._generateOutput(true); // true = silent (don't dispatch 'change' event)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
override firstUpdated(changedProperties: PropertyValues) {
|
|
135
|
+
super.firstUpdated(changedProperties);
|
|
136
|
+
|
|
137
|
+
// If the component mounted without an external value, generate the default cron string
|
|
138
|
+
if (!this.value) {
|
|
139
|
+
this._generateOutput(true);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._defaultValue = this.value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private _parseIncomingValue(val: string) {
|
|
146
|
+
if (!val) return;
|
|
147
|
+
|
|
148
|
+
// 1. One-Off Checks
|
|
149
|
+
if (val.includes("T")) {
|
|
150
|
+
this._frequency = "one-off";
|
|
151
|
+
const [dateStr, timeStr] = val.split("T");
|
|
152
|
+
this._date = dateStr;
|
|
153
|
+
this._time = timeStr;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const parts = val.trim().split(/\s+/);
|
|
158
|
+
if (parts.length !== 5) {
|
|
159
|
+
this._frequency = "custom";
|
|
160
|
+
this._customCron = val;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 🚀 THE FIX: Check mathematical validity FIRST.
|
|
165
|
+
// If it's broken (like DOW 15), force it to Custom so the error UI displays.
|
|
166
|
+
const analysis = this._translateCron(val);
|
|
167
|
+
if (!analysis.valid) {
|
|
168
|
+
this._frequency = "custom";
|
|
169
|
+
this._customCron = val;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Safe mapping for valid cron strings
|
|
174
|
+
const [min, hour, dom, mon, dow] = parts;
|
|
175
|
+
|
|
176
|
+
if (/^\d+$/.test(min) && /^\d+$/.test(hour)) {
|
|
177
|
+
this._time = `${hour.padStart(2, "0")}:${min.padStart(2, "0")}`;
|
|
178
|
+
|
|
179
|
+
if (dom === "*" && mon === "*" && dow === "*") {
|
|
180
|
+
this._frequency = "daily";
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Ensure weekly is only triggered for simple lists (e.g. "1,3,5" or "1"), not steps/ranges
|
|
184
|
+
else if (dom === "*" && mon === "*" && /^[0-7](,[0-7])*$/.test(dow)) {
|
|
185
|
+
this._frequency = "weekly";
|
|
186
|
+
this._days = dow;
|
|
187
|
+
return;
|
|
188
|
+
} else if (/^\d+$/.test(dom) && mon === "*" && dow === "*") {
|
|
189
|
+
this._frequency = "monthly";
|
|
190
|
+
this._monthDay = dom;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 3. Fallback for valid but complex expressions (e.g., */15 * * * *)
|
|
196
|
+
this._frequency = "custom";
|
|
197
|
+
this._customCron = val;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private _translateCron(cron: string): {valid: boolean; text: string} {
|
|
201
|
+
const parts = cron.trim().split(/\s+/);
|
|
202
|
+
|
|
203
|
+
if (parts.length !== 5) {
|
|
204
|
+
return {
|
|
205
|
+
valid: false,
|
|
206
|
+
text: `A cron expression must contain exactly 5 segments (currently ${parts.length}).`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const types = ["Minute", "Hour", "Day of Month", "Month", "Day of Week"];
|
|
211
|
+
|
|
212
|
+
const bounds = [
|
|
213
|
+
{min: 0, max: 59}, // Minute
|
|
214
|
+
{min: 0, max: 23}, // Hour
|
|
215
|
+
{min: 1, max: 31}, // DOM
|
|
216
|
+
{min: 1, max: 12}, // Month
|
|
217
|
+
{min: 0, max: 7}, // DOW
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < 5; i++) {
|
|
221
|
+
const part = parts[i];
|
|
222
|
+
const type = types[i];
|
|
223
|
+
const bound = bounds[i];
|
|
224
|
+
|
|
225
|
+
// Prevent consecutive or hanging symbols like 1- or */
|
|
226
|
+
if (/(^[\/\-\,])|([\/\-\,]$)|([\/\-\,]{2,})/.test(part)) {
|
|
227
|
+
return {valid: false, text: `Invalid formatting in ${type} segment.`};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// First 3 segments (min, hour, dom) cannot have letters
|
|
231
|
+
if (i < 3 && /[A-Za-z]/.test(part)) {
|
|
232
|
+
return {valid: false, text: `${type} cannot contain letters.`};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Last 2 segments can only have specific valid keywords
|
|
236
|
+
if (/[A-Za-z]/.test(part)) {
|
|
237
|
+
const words = part.match(/[A-Za-z]+/g) || [];
|
|
238
|
+
const validWords =
|
|
239
|
+
i === 3
|
|
240
|
+
? ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
|
|
241
|
+
: ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
|
242
|
+
|
|
243
|
+
for (const word of words) {
|
|
244
|
+
if (!validWords.includes(word.toUpperCase())) {
|
|
245
|
+
return {valid: false, text: `"${word}" is not a valid ${type}.`};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const numbers = part.match(/\d+/g);
|
|
251
|
+
if (numbers) {
|
|
252
|
+
for (const numStr of numbers) {
|
|
253
|
+
const num = parseInt(numStr, 10);
|
|
254
|
+
if (num < bound.min || num > bound.max) {
|
|
255
|
+
return {
|
|
256
|
+
valid: false,
|
|
257
|
+
text: `${type} values must be between ${bound.min} and ${bound.max}.`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const [min, hour, dom, mon, dow] = parts;
|
|
265
|
+
let desc = "Runs ";
|
|
266
|
+
|
|
267
|
+
if (min === "*") desc += "every minute";
|
|
268
|
+
else if (min.startsWith("*/")) desc += `every ${min.substring(2)} minutes`;
|
|
269
|
+
else desc += `at minute ${min}`;
|
|
270
|
+
|
|
271
|
+
if (hour !== "*") {
|
|
272
|
+
if (hour.startsWith("*/")) desc += `, past every ${hour.substring(2)} hours`;
|
|
273
|
+
else desc += `, past hour ${hour}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (dom !== "*") {
|
|
277
|
+
if (dom.startsWith("*/")) desc += `, every ${dom.substring(2)} days`;
|
|
278
|
+
else desc += `, on day ${dom} of the month`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (mon !== "*") {
|
|
282
|
+
if (mon.startsWith("*/")) desc += `, every ${mon.substring(2)} months`;
|
|
283
|
+
else desc += `, in month ${mon}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (dow !== "*") {
|
|
287
|
+
const days = [
|
|
288
|
+
"Sunday",
|
|
289
|
+
"Monday",
|
|
290
|
+
"Tuesday",
|
|
291
|
+
"Wednesday",
|
|
292
|
+
"Thursday",
|
|
293
|
+
"Friday",
|
|
294
|
+
"Saturday",
|
|
295
|
+
"Sunday",
|
|
296
|
+
];
|
|
297
|
+
let dayStr = dow;
|
|
298
|
+
|
|
299
|
+
if (dow.length === 1 && /[0-7]/.test(dow)) {
|
|
300
|
+
dayStr = days[parseInt(dow, 10)];
|
|
301
|
+
} else if (dow.includes("-") && /[0-7]-[0-7]/.test(dow)) {
|
|
302
|
+
dayStr = dow
|
|
303
|
+
.split("-")
|
|
304
|
+
.map((d) => days[parseInt(d, 10)])
|
|
305
|
+
.join(" through ");
|
|
306
|
+
}
|
|
307
|
+
desc += `, on ${dayStr}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {valid: true, text: desc + "."};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private _generateOutput(silent = false) {
|
|
314
|
+
let output = "";
|
|
315
|
+
this._customCronError = "";
|
|
316
|
+
this._customCronText = "";
|
|
317
|
+
this._internals.setValidity({});
|
|
318
|
+
|
|
319
|
+
if (this._frequency === "custom") {
|
|
320
|
+
const analysis = this._translateCron(this._customCron);
|
|
321
|
+
|
|
322
|
+
if (!analysis.valid) {
|
|
323
|
+
this._customCronError = analysis.text;
|
|
324
|
+
this._internals.setValidity({customError: true}, analysis.text);
|
|
325
|
+
} else {
|
|
326
|
+
this._customCronText = analysis.text;
|
|
327
|
+
}
|
|
328
|
+
output = this._customCron;
|
|
329
|
+
} else if (this._frequency === "one-off") {
|
|
330
|
+
output = `${this._date}T${this._time}`;
|
|
331
|
+
} else {
|
|
332
|
+
const [hh, mm] = this._time.split(":");
|
|
333
|
+
const minute = mm ? parseInt(mm, 10).toString() : "0";
|
|
334
|
+
const hour = hh ? parseInt(hh, 10).toString() : "0";
|
|
335
|
+
|
|
336
|
+
switch (this._frequency) {
|
|
337
|
+
case "daily":
|
|
338
|
+
output = `${minute} ${hour} * * *`;
|
|
339
|
+
break;
|
|
340
|
+
case "weekly":
|
|
341
|
+
const safeDays = this._days || "1";
|
|
342
|
+
output = `${minute} ${hour} * * ${safeDays}`;
|
|
343
|
+
break;
|
|
344
|
+
case "monthly":
|
|
345
|
+
output = `${minute} ${hour} ${this._monthDay} * *`;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this._lastGeneratedOutput = output;
|
|
351
|
+
this.value = output;
|
|
352
|
+
this._internals.setFormValue(output);
|
|
353
|
+
|
|
354
|
+
// Only dispatch if the user actually clicked/typed something
|
|
355
|
+
if (!silent) {
|
|
356
|
+
this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private _handleCustomCronInput(e: any) {
|
|
361
|
+
const inputEl = e.target;
|
|
362
|
+
const sanitized = inputEl.value.replace(/[^0-9A-Za-z\s\*\/\-\,]/g, "");
|
|
363
|
+
|
|
364
|
+
if (inputEl.value !== sanitized) {
|
|
365
|
+
inputEl.value = sanitized;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this._customCron = sanitized;
|
|
369
|
+
this._generateOutput();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private _handleFrequencyChange(e: any) {
|
|
373
|
+
this._frequency = e.target.value;
|
|
374
|
+
this._generateOutput();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
public checkValidity() {
|
|
378
|
+
return this._internals.checkValidity();
|
|
379
|
+
}
|
|
380
|
+
public reportValidity() {
|
|
381
|
+
return this._internals.reportValidity();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
formResetCallback() {
|
|
385
|
+
this.value = this._defaultValue;
|
|
386
|
+
this._frequency = "weekly";
|
|
387
|
+
this._generateOutput();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
render() {
|
|
391
|
+
const frequencyOptions = [
|
|
392
|
+
{label: "One-Off Execution", value: "one-off"},
|
|
393
|
+
{label: "Daily", value: "daily"},
|
|
394
|
+
{label: "Weekly", value: "weekly"},
|
|
395
|
+
{label: "Monthly", value: "monthly"},
|
|
396
|
+
{label: "Custom Cron", value: "custom"},
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
const monthDayOptions = Array.from({length: 31}, (_, i) => ({
|
|
400
|
+
label: `${i + 1}${i === 0 ? "st" : i === 1 ? "nd" : i === 2 ? "rd" : "th"}`,
|
|
401
|
+
value: String(i + 1),
|
|
402
|
+
}));
|
|
403
|
+
|
|
404
|
+
// Calculate which segment the user is currently typing in
|
|
405
|
+
const activeSegmentIndex = Math.min(this._customCron.trimStart().split(/\s+/).length - 1, 4);
|
|
406
|
+
|
|
407
|
+
return html`
|
|
408
|
+
<div class="builder-grid">
|
|
409
|
+
<div class="row">
|
|
410
|
+
<st-select
|
|
411
|
+
label="Schedule Type"
|
|
412
|
+
.options=${frequencyOptions}
|
|
413
|
+
.value=${this._frequency}
|
|
414
|
+
@change=${this._handleFrequencyChange}
|
|
415
|
+
></st-select>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
${this._frequency === "weekly"
|
|
419
|
+
? html`
|
|
420
|
+
<div class="row">
|
|
421
|
+
<label>Run on days</label>
|
|
422
|
+
<st-toggle-group
|
|
423
|
+
multiple
|
|
424
|
+
.value=${this._days}
|
|
425
|
+
@change=${(e: any) => {
|
|
426
|
+
this._days = e.target.value;
|
|
427
|
+
this._generateOutput();
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
<st-toggle-button value="1">Mon</st-toggle-button>
|
|
431
|
+
<st-toggle-button value="2">Tue</st-toggle-button>
|
|
432
|
+
<st-toggle-button value="3">Wed</st-toggle-button>
|
|
433
|
+
<st-toggle-button value="4">Thu</st-toggle-button>
|
|
434
|
+
<st-toggle-button value="5">Fri</st-toggle-button>
|
|
435
|
+
<st-toggle-button value="6">Sat</st-toggle-button>
|
|
436
|
+
<st-toggle-button value="0">Sun</st-toggle-button>
|
|
437
|
+
</st-toggle-group>
|
|
438
|
+
</div>
|
|
439
|
+
`
|
|
440
|
+
: ""}
|
|
441
|
+
${this._frequency === "monthly"
|
|
442
|
+
? html`
|
|
443
|
+
<div class="row">
|
|
444
|
+
<st-select
|
|
445
|
+
label="Run on day of month"
|
|
446
|
+
.options=${monthDayOptions}
|
|
447
|
+
.value=${this._monthDay}
|
|
448
|
+
@change=${(e: any) => {
|
|
449
|
+
this._monthDay = e.target.value;
|
|
450
|
+
this._generateOutput();
|
|
451
|
+
}}
|
|
452
|
+
></st-select>
|
|
453
|
+
</div>
|
|
454
|
+
`
|
|
455
|
+
: ""}
|
|
456
|
+
${this._frequency === "one-off"
|
|
457
|
+
? html`
|
|
458
|
+
<div class="row">
|
|
459
|
+
<label>Execution Date</label>
|
|
460
|
+
<st-calendar
|
|
461
|
+
.value=${this._date}
|
|
462
|
+
@change=${(e: any) => {
|
|
463
|
+
this._date = e.target.value;
|
|
464
|
+
this._generateOutput();
|
|
465
|
+
}}
|
|
466
|
+
></st-calendar>
|
|
467
|
+
</div>
|
|
468
|
+
`
|
|
469
|
+
: ""}
|
|
470
|
+
${this._frequency === "custom"
|
|
471
|
+
? html`
|
|
472
|
+
<div class="row">
|
|
473
|
+
<st-input
|
|
474
|
+
label="Raw Cron Expression"
|
|
475
|
+
placeholder="*/15 * * * *"
|
|
476
|
+
.value=${this._customCron}
|
|
477
|
+
.error=${this._customCronError}
|
|
478
|
+
?invalid=${!!this._customCronError}
|
|
479
|
+
@input=${this._handleCustomCronInput}
|
|
480
|
+
></st-input>
|
|
481
|
+
<div class="cron-segments" aria-hidden="true">
|
|
482
|
+
${["Minute", "Hour", "Day", "Month", "Weekday"].map(
|
|
483
|
+
(label, i) => html`
|
|
484
|
+
<div class="segment-badge ${activeSegmentIndex === i ? "active" : ""}">
|
|
485
|
+
${label}
|
|
486
|
+
</div>
|
|
487
|
+
`,
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
${this._customCronText && !this._customCronError
|
|
492
|
+
? html`
|
|
493
|
+
<div class="helper-text success-text">
|
|
494
|
+
<strong>Schedule:</strong> ${this._customCronText}
|
|
495
|
+
</div>
|
|
496
|
+
`
|
|
497
|
+
: ""}
|
|
498
|
+
</div>
|
|
499
|
+
`
|
|
500
|
+
: ""}
|
|
501
|
+
${this._frequency !== "custom"
|
|
502
|
+
? html`
|
|
503
|
+
<div class="row">
|
|
504
|
+
<st-time-picker
|
|
505
|
+
label="Execution Time"
|
|
506
|
+
.value=${this._time}
|
|
507
|
+
@change=${(e: any) => {
|
|
508
|
+
this._time = e.target.value;
|
|
509
|
+
this._generateOutput();
|
|
510
|
+
}}
|
|
511
|
+
></st-time-picker>
|
|
512
|
+
</div>
|
|
513
|
+
`
|
|
514
|
+
: ""}
|
|
515
|
+
|
|
516
|
+
<div class="output-preview">
|
|
517
|
+
<div class="helper-text">Generated Payload</div>
|
|
518
|
+
<code>${this.value}</code>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
`;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
declare global {
|
|
526
|
+
interface HTMLElementTagNameMap {
|
|
527
|
+
"st-cron-builder": StCronBuilder;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
233
|
-
? html`<div id="
|
|
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
|
+
}
|