@placeholderco/placeholder-ui 1.0.3 → 1.0.6

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 (136) hide show
  1. package/LICENSE +26 -26
  2. package/README.md +179 -179
  3. package/dist/display/Alert.svelte +179 -179
  4. package/dist/display/Avatar.svelte +166 -166
  5. package/dist/display/LinkCollection.svelte +161 -161
  6. package/dist/display/Paper.svelte +118 -118
  7. package/dist/form/Autocomplete.svelte +223 -191
  8. package/dist/form/Autocomplete.svelte.d.ts +3 -1
  9. package/dist/form/AutocompleteMulti.svelte +356 -0
  10. package/dist/form/AutocompleteMulti.svelte.d.ts +28 -0
  11. package/dist/form/Checkbox.svelte +201 -201
  12. package/dist/form/Chips.svelte +128 -128
  13. package/dist/form/ComboBox.svelte +158 -158
  14. package/dist/form/ComboBox.svelte.d.ts +1 -1
  15. package/dist/form/ComboBoxItemBuilder.svelte +460 -460
  16. package/dist/form/ComboBoxMulti.svelte +197 -197
  17. package/dist/form/ComboBoxMulti.svelte.d.ts +1 -1
  18. package/dist/form/CronBuilder.svelte +693 -693
  19. package/dist/form/DatePicker.svelte +672 -672
  20. package/dist/form/DateTimePicker.svelte +712 -712
  21. package/dist/form/FileInput.svelte +235 -235
  22. package/dist/form/FormGroup.svelte +68 -68
  23. package/dist/form/Number.svelte +238 -238
  24. package/dist/form/PasswordInput.svelte +252 -252
  25. package/dist/form/RadioGroup.svelte +210 -210
  26. package/dist/form/Rating.svelte +235 -235
  27. package/dist/form/SegmentedControl.svelte +149 -149
  28. package/dist/form/Select.svelte +590 -590
  29. package/dist/form/Select.svelte.d.ts +1 -1
  30. package/dist/form/SelectMulti.svelte +613 -613
  31. package/dist/form/SelectMulti.svelte.d.ts +1 -1
  32. package/dist/form/Slider.svelte +358 -358
  33. package/dist/form/Switch.svelte +147 -147
  34. package/dist/form/TextArea.svelte +148 -148
  35. package/dist/form/Textbox.svelte +228 -228
  36. package/dist/form/TimePicker.svelte +267 -267
  37. package/dist/icon/Icon.svelte +52 -52
  38. package/dist/icon/alert-octagon.svg +5 -5
  39. package/dist/icon/alert-triangle.svg +5 -5
  40. package/dist/icon/archive.svg +1 -1
  41. package/dist/icon/arrow-down.svg +1 -1
  42. package/dist/icon/arrow-left.svg +1 -1
  43. package/dist/icon/arrow-right.svg +1 -1
  44. package/dist/icon/arrow-up.svg +1 -1
  45. package/dist/icon/at.svg +1 -1
  46. package/dist/icon/bell.svg +1 -1
  47. package/dist/icon/bookmark.svg +1 -1
  48. package/dist/icon/calendar.svg +1 -1
  49. package/dist/icon/camera.svg +1 -1
  50. package/dist/icon/chart-bar.svg +1 -1
  51. package/dist/icon/chart-line.svg +1 -1
  52. package/dist/icon/chart-pie.svg +1 -1
  53. package/dist/icon/checkbox.svg +1 -1
  54. package/dist/icon/checklist.svg +1 -1
  55. package/dist/icon/circle-check.svg +1 -1
  56. package/dist/icon/circle-x.svg +1 -1
  57. package/dist/icon/clock.svg +1 -1
  58. package/dist/icon/credit-card.svg +1 -1
  59. package/dist/icon/dots-vertical.svg +1 -1
  60. package/dist/icon/dots.svg +1 -1
  61. package/dist/icon/external-link.svg +1 -1
  62. package/dist/icon/eye-off.svg +1 -1
  63. package/dist/icon/eye.svg +1 -1
  64. package/dist/icon/filter.svg +1 -1
  65. package/dist/icon/fingerprint.svg +1 -1
  66. package/dist/icon/flag.svg +1 -1
  67. package/dist/icon/heart.svg +1 -1
  68. package/dist/icon/home.svg +1 -1
  69. package/dist/icon/key.svg +1 -1
  70. package/dist/icon/list-check.svg +1 -1
  71. package/dist/icon/login.svg +1 -1
  72. package/dist/icon/logout.svg +1 -1
  73. package/dist/icon/map-pin.svg +1 -1
  74. package/dist/icon/maximize.svg +1 -1
  75. package/dist/icon/microphone.svg +1 -1
  76. package/dist/icon/minimize.svg +1 -1
  77. package/dist/icon/note.svg +1 -1
  78. package/dist/icon/player-pause.svg +1 -1
  79. package/dist/icon/printer.svg +1 -1
  80. package/dist/icon/qrcode.svg +1 -1
  81. package/dist/icon/send.svg +1 -1
  82. package/dist/icon/settings.svg +1 -1
  83. package/dist/icon/share.svg +1 -1
  84. package/dist/icon/shopping-cart.svg +1 -1
  85. package/dist/icon/sort-ascending.svg +1 -1
  86. package/dist/icon/sort-descending.svg +1 -1
  87. package/dist/icon/star.svg +1 -1
  88. package/dist/icon/tag.svg +1 -1
  89. package/dist/icon/trending-down.svg +1 -1
  90. package/dist/icon/trending-up.svg +1 -1
  91. package/dist/icon/upload.svg +1 -1
  92. package/dist/icon/volume-off.svg +1 -1
  93. package/dist/icon/volume.svg +1 -1
  94. package/dist/icon/world.svg +1 -1
  95. package/dist/icon/zoom-in.svg +1 -1
  96. package/dist/icon/zoom-out.svg +1 -1
  97. package/dist/index.d.ts +1 -0
  98. package/dist/index.js +1 -0
  99. package/dist/layout/AppShell.svelte +169 -169
  100. package/dist/layout/CustomNavbar.svelte +61 -61
  101. package/dist/layout/Navbar.svelte +206 -206
  102. package/dist/layout/NavbarItemDisplay.svelte +29 -29
  103. package/dist/layout/Sidenav.svelte +712 -712
  104. package/dist/styles/components.css +199 -199
  105. package/dist/styles/dark.css +146 -146
  106. package/dist/styles/index.css +116 -116
  107. package/dist/styles/reset.css +110 -110
  108. package/dist/styles/semantic.css +86 -86
  109. package/dist/styles/tokens.css +203 -197
  110. package/dist/styles/utilities.css +523 -523
  111. package/dist/ui/Accordion.svelte +289 -289
  112. package/dist/ui/ActionIcon.svelte +76 -76
  113. package/dist/ui/Badge.svelte +329 -279
  114. package/dist/ui/Breadcrumbs.svelte +131 -131
  115. package/dist/ui/Button.svelte +432 -370
  116. package/dist/ui/ButtonVariant.d.ts +1 -1
  117. package/dist/ui/Dialog.svelte +307 -307
  118. package/dist/ui/Drawer.svelte +524 -524
  119. package/dist/ui/Dropdown.svelte +97 -97
  120. package/dist/ui/Dropzone.svelte +122 -122
  121. package/dist/ui/Link.svelte +32 -32
  122. package/dist/ui/Loader.svelte +70 -70
  123. package/dist/ui/LoadingOverlay.svelte +53 -53
  124. package/dist/ui/Pagination.svelte +135 -135
  125. package/dist/ui/Popover.svelte +225 -225
  126. package/dist/ui/Progress.svelte +191 -191
  127. package/dist/ui/RingProgress.svelte +141 -141
  128. package/dist/ui/Skeleton.svelte +85 -85
  129. package/dist/ui/Stepper.svelte +355 -355
  130. package/dist/ui/Table.svelte +345 -345
  131. package/dist/ui/Tabs.svelte +146 -146
  132. package/dist/ui/ThemeSwitcher.svelte +39 -39
  133. package/dist/ui/Timeline.svelte +225 -225
  134. package/dist/ui/Toaster.svelte +6 -6
  135. package/dist/ui/Tooltip.svelte +434 -434
  136. package/package.json +14 -14
@@ -1,694 +1,694 @@
1
- <script lang="ts">
2
- import FormGroup from './FormGroup.svelte';
3
- import Select from './Select.svelte';
4
- import Number from './Number.svelte';
5
- import Checkbox from './Checkbox.svelte';
6
- import type { ComboBoxItem } from '../models/ComboBoxItem.js';
7
- import { type Snippet, onMount, untrack } from 'svelte';
8
- import { CronParser } from '../util/CronParser.js';
9
-
10
- interface Props {
11
- label?: string;
12
- value?: string;
13
- placeholder?: string;
14
- class?: string;
15
- containerClass?: string;
16
- disabled?: boolean;
17
- required?: boolean;
18
- showError?: boolean;
19
- errorText?: string;
20
- tooltipContent?: Snippet;
21
- tooltipText?: string;
22
- tooltipLocation?: 'top' | 'bottom' | 'left' | 'right';
23
- onchange?: (expression: string) => void;
24
- }
25
-
26
- let {
27
- label = 'Schedule',
28
- value = $bindable('0 0 * * *'),
29
- placeholder = '',
30
- class: classes = '',
31
- containerClass = '',
32
- disabled = false,
33
- required = false,
34
- showError = false,
35
- errorText = '',
36
- tooltipContent,
37
- tooltipText,
38
- tooltipLocation = 'top',
39
- onchange
40
- }: Props = $props();
41
-
42
- type FrequencyType = 'minute' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom';
43
-
44
- let frequency = $state<FrequencyType>('daily');
45
- let isParsingExpression = false;
46
-
47
- // Minute settings
48
- let minuteInterval = $state(1);
49
-
50
- // Hourly settings
51
- let hourlyMinute = $state(0);
52
- let hourlyInterval = $state(1);
53
-
54
- // Daily settings
55
- let dailyHour = $state(0);
56
- let dailyMinute = $state(0);
57
- let dailyInterval = $state(1);
58
-
59
- // Weekly settings
60
- let weeklyDays = $state<number[]>([1]); // 0=Sunday, 1=Monday, etc.
61
- let weeklyHour = $state(0);
62
- let weeklyMinute = $state(0);
63
-
64
- // Monthly settings
65
- let monthlyDay = $state(1);
66
- let monthlyHour = $state(0);
67
- let monthlyMinute = $state(0);
68
-
69
- // Yearly settings
70
- let yearlyMonth = $state(1);
71
- let yearlyDay = $state(1);
72
- let yearlyHour = $state(0);
73
- let yearlyMinute = $state(0);
74
-
75
- // Custom settings
76
- let customMinute = $state('*');
77
- let customHour = $state('*');
78
- let customDayOfMonth = $state('*');
79
- let customMonth = $state('*');
80
- let customDayOfWeek = $state('*');
81
-
82
- const frequencyOptions: ComboBoxItem[] = [
83
- { label: 'Every Minute', value: 'minute' },
84
- { label: 'Hourly', value: 'hourly' },
85
- { label: 'Daily', value: 'daily' },
86
- { label: 'Weekly', value: 'weekly' },
87
- { label: 'Monthly', value: 'monthly' },
88
- { label: 'Yearly', value: 'yearly' },
89
- { label: 'Custom', value: 'custom' }
90
- ];
91
-
92
- const dayOptions: ComboBoxItem[] = Array.from({ length: 31 }, (_, i) => ({
93
- label: String(i + 1),
94
- value: String(i + 1)
95
- }));
96
-
97
- const hourOptions: ComboBoxItem[] = Array.from({ length: 24 }, (_, i) => ({
98
- label: i.toString().padStart(2, '0'),
99
- value: String(i)
100
- }));
101
-
102
- const minuteOptions: ComboBoxItem[] = Array.from({ length: 60 }, (_, i) => ({
103
- label: i.toString().padStart(2, '0'),
104
- value: String(i)
105
- }));
106
-
107
- const monthOptions: ComboBoxItem[] = [
108
- { label: 'January', value: '1' },
109
- { label: 'February', value: '2' },
110
- { label: 'March', value: '3' },
111
- { label: 'April', value: '4' },
112
- { label: 'May', value: '5' },
113
- { label: 'June', value: '6' },
114
- { label: 'July', value: '7' },
115
- { label: 'August', value: '8' },
116
- { label: 'September', value: '9' },
117
- { label: 'October', value: '10' },
118
- { label: 'November', value: '11' },
119
- { label: 'December', value: '12' }
120
- ];
121
-
122
- const weekDays = [
123
- { label: 'Sunday', value: 0 },
124
- { label: 'Monday', value: 1 },
125
- { label: 'Tuesday', value: 2 },
126
- { label: 'Wednesday', value: 3 },
127
- { label: 'Thursday', value: 4 },
128
- { label: 'Friday', value: 5 },
129
- { label: 'Saturday', value: 6 }
130
- ];
131
-
132
- function generateExpression(): string {
133
- let expr = '';
134
-
135
- switch (frequency) {
136
- case 'minute':
137
- expr = `*/${minuteInterval} * * * *`;
138
- break;
139
-
140
- case 'hourly':
141
- expr = `${hourlyMinute} */${hourlyInterval} * * *`;
142
- break;
143
-
144
- case 'daily':
145
- expr = `${dailyMinute} ${dailyHour} */${dailyInterval} * *`;
146
- break;
147
-
148
- case 'weekly':
149
- const days = weeklyDays.length > 0 ? weeklyDays.sort().join(',') : '*';
150
- expr = `${weeklyMinute} ${weeklyHour} * * ${days}`;
151
- break;
152
-
153
- case 'monthly':
154
- expr = `${monthlyMinute} ${monthlyHour} ${monthlyDay} * *`;
155
- break;
156
-
157
- case 'yearly':
158
- expr = `${yearlyMinute} ${yearlyHour} ${yearlyDay} ${yearlyMonth} *`;
159
- break;
160
-
161
- case 'custom':
162
- expr = `${customMinute} ${customHour} ${customDayOfMonth} ${customMonth} ${customDayOfWeek}`;
163
- break;
164
- }
165
-
166
- return expr;
167
- }
168
-
169
- function parseExpression(expr: string) {
170
- if (!expr || isParsingExpression) return;
171
-
172
- const parts = expr.trim().split(/\s+/);
173
- if (parts.length !== 5) return;
174
-
175
- const [min, hour, dayOfMonth, month, dayOfWeek] = parts;
176
-
177
- isParsingExpression = true;
178
-
179
- // Try to detect frequency type
180
- if (min.startsWith('*/') && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
181
- frequency = 'minute';
182
- minuteInterval = parseInt(min.split('/')[1]) || 1;
183
- } else if (hour.startsWith('*/') && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
184
- frequency = 'hourly';
185
- hourlyMinute = parseInt(min) || 0;
186
- hourlyInterval = parseInt(hour.split('/')[1]) || 1;
187
- } else if (dayOfMonth.startsWith('*/') && month === '*' && dayOfWeek === '*') {
188
- frequency = 'daily';
189
- dailyMinute = parseInt(min) || 0;
190
- dailyHour = parseInt(hour) || 0;
191
- dailyInterval = parseInt(dayOfMonth.split('/')[1]) || 1;
192
- } else if (dayOfMonth === '*' && month === '*' && dayOfWeek !== '*') {
193
- frequency = 'weekly';
194
- weeklyMinute = parseInt(min) || 0;
195
- weeklyHour = parseInt(hour) || 0;
196
- weeklyDays = dayOfWeek.split(',').map(d => parseInt(d)).filter(d => !isNaN(d));
197
- } else if (month === '*' && dayOfWeek === '*' && !dayOfMonth.includes('*')) {
198
- frequency = 'monthly';
199
- monthlyMinute = parseInt(min) || 0;
200
- monthlyHour = parseInt(hour) || 0;
201
- monthlyDay = parseInt(dayOfMonth) || 1;
202
- } else if (dayOfWeek === '*' && !month.includes('*') && !dayOfMonth.includes('*')) {
203
- frequency = 'yearly';
204
- yearlyMinute = parseInt(min) || 0;
205
- yearlyHour = parseInt(hour) || 0;
206
- yearlyDay = parseInt(dayOfMonth) || 1;
207
- yearlyMonth = parseInt(month) || 1;
208
- } else {
209
- frequency = 'custom';
210
- customMinute = min;
211
- customHour = hour;
212
- customDayOfMonth = dayOfMonth;
213
- customMonth = month;
214
- customDayOfWeek = dayOfWeek;
215
- }
216
-
217
- isParsingExpression = false;
218
- }
219
-
220
- function toggleWeekday(day: number) {
221
- if (weeklyDays.includes(day)) {
222
- weeklyDays = weeklyDays.filter(d => d !== day);
223
- } else {
224
- weeklyDays = [...weeklyDays, day];
225
- }
226
- }
227
-
228
- // Parse initial expression on mount
229
- onMount(() => {
230
- if (value) {
231
- parseExpression(value);
232
- }
233
- });
234
-
235
- // Generate expression when settings change
236
- $effect(() => {
237
- // Track dependencies
238
- frequency;
239
- minuteInterval;
240
- hourlyMinute;
241
- hourlyInterval;
242
- dailyHour;
243
- dailyMinute;
244
- dailyInterval;
245
- weeklyDays;
246
- weeklyHour;
247
- weeklyMinute;
248
- monthlyDay;
249
- monthlyHour;
250
- monthlyMinute;
251
- yearlyMonth;
252
- yearlyDay;
253
- yearlyHour;
254
- yearlyMinute;
255
- customMinute;
256
- customHour;
257
- customDayOfMonth;
258
- customMonth;
259
- customDayOfWeek;
260
-
261
- untrack(() => {
262
- if (!isParsingExpression) {
263
- const expr = generateExpression();
264
- value = expr;
265
- onchange?.(expr);
266
- }
267
- });
268
- });
269
-
270
- function getHumanReadable(): string {
271
- switch (frequency) {
272
- case 'minute':
273
- return minuteInterval === 1 ? 'Every minute' : `Every ${minuteInterval} minutes`;
274
-
275
- case 'hourly':
276
- const hrText = hourlyInterval === 1 ? 'hour' : `${hourlyInterval} hours`;
277
- return `Every ${hrText} at minute ${hourlyMinute}`;
278
-
279
- case 'daily':
280
- const dayText = dailyInterval === 1 ? 'day' : `${dailyInterval} days`;
281
- return `Every ${dayText} at ${dailyHour.toString().padStart(2, '0')}:${dailyMinute.toString().padStart(2, '0')}`;
282
-
283
- case 'weekly':
284
- const dayNames = weeklyDays.map(d => weekDays.find(wd => wd.value === d)?.label).join(', ');
285
- return `Every ${dayNames} at ${weeklyHour.toString().padStart(2, '0')}:${weeklyMinute.toString().padStart(2, '0')}`;
286
-
287
- case 'monthly':
288
- return `Day ${monthlyDay} of every month at ${monthlyHour.toString().padStart(2, '0')}:${monthlyMinute.toString().padStart(2, '0')}`;
289
-
290
- case 'yearly':
291
- const monthName = monthOptions.find(m => m.value === yearlyMonth.toString())?.label;
292
- return `${monthName} ${yearlyDay} at ${yearlyHour.toString().padStart(2, '0')}:${yearlyMinute.toString().padStart(2, '0')}`;
293
-
294
- case 'custom':
295
- return value;
296
-
297
- default:
298
- return value;
299
- }
300
- }
301
- </script>
302
-
303
- <FormGroup
304
- {label}
305
- {required}
306
- {showError}
307
- {errorText}
308
- {tooltipContent}
309
- {tooltipText}
310
- {tooltipLocation}
311
- class={containerClass}
312
- >
313
- <div class="cron-builder {classes}" class:disabled>
314
- <div class="cron-frequency">
315
- <Select
316
- label="Frequency"
317
- bind:value={frequency}
318
- options={frequencyOptions}
319
- {disabled}
320
- />
321
- </div>
322
-
323
- <div class="cron-settings">
324
- {#if frequency === 'minute'}
325
- <Number
326
- label="Interval (minutes)"
327
- bind:value={minuteInterval}
328
- min={1}
329
- max={59}
330
- {disabled}
331
- />
332
- {:else if frequency === 'hourly'}
333
- <Number
334
- label="Interval (hours)"
335
- bind:value={hourlyInterval}
336
- min={1}
337
- max={23}
338
- {disabled}
339
- />
340
- <Number
341
- label="At minute"
342
- bind:value={hourlyMinute}
343
- min={0}
344
- max={59}
345
- {disabled}
346
- />
347
- {:else if frequency === 'daily'}
348
- <Number
349
- label="Interval (days)"
350
- bind:value={dailyInterval}
351
- min={1}
352
- max={31}
353
- {disabled}
354
- />
355
- <Number
356
- label="Hour"
357
- bind:value={dailyHour}
358
- min={0}
359
- max={23}
360
- {disabled}
361
- />
362
- <Number
363
- label="Minute"
364
- bind:value={dailyMinute}
365
- min={0}
366
- max={59}
367
- {disabled}
368
- />
369
- {:else if frequency === 'weekly'}
370
- <div class="weekday-selector">
371
- <span class="weekday-label">Days of Week</span>
372
- <div class="weekday-buttons">
373
- {#each weekDays as day}
374
- <button
375
- type="button"
376
- class="weekday-btn"
377
- class:selected={weeklyDays.includes(day.value)}
378
- onclick={() => toggleWeekday(day.value)}
379
- {disabled}
380
- >
381
- {day.label.substring(0, 3)}
382
- </button>
383
- {/each}
384
- </div>
385
- </div>
386
- <Number
387
- label="Hour"
388
- bind:value={weeklyHour}
389
- min={0}
390
- max={23}
391
- {disabled}
392
- />
393
- <Number
394
- label="Minute"
395
- bind:value={weeklyMinute}
396
- min={0}
397
- max={59}
398
- {disabled}
399
- />
400
- {:else if frequency === 'monthly'}
401
- <Number
402
- label="Day of Month"
403
- bind:value={monthlyDay}
404
- min={1}
405
- max={31}
406
- {disabled}
407
- />
408
- <Number
409
- label="Hour"
410
- bind:value={monthlyHour}
411
- min={0}
412
- max={23}
413
- {disabled}
414
- />
415
- <Number
416
- label="Minute"
417
- bind:value={monthlyMinute}
418
- min={0}
419
- max={59}
420
- {disabled}
421
- />
422
- {:else if frequency === 'yearly'}
423
- <Number
424
- label="Month"
425
- bind:value={yearlyMonth}
426
- min={1}
427
- max={12}
428
- {disabled}
429
- />
430
- <Number
431
- label="Day"
432
- bind:value={yearlyDay}
433
- min={1}
434
- max={31}
435
- {disabled}
436
- />
437
- <Number
438
- label="Hour"
439
- bind:value={yearlyHour}
440
- min={0}
441
- max={23}
442
- {disabled}
443
- />
444
- <Number
445
- label="Minute"
446
- bind:value={yearlyMinute}
447
- min={0}
448
- max={59}
449
- {disabled}
450
- />
451
- {:else if frequency === 'custom'}
452
- <div class="custom-inputs">
453
- <FormGroup label="Minute">
454
- <input
455
- type="text"
456
- bind:value={customMinute}
457
- {disabled}
458
- class="form-control"
459
- placeholder="0-59, *, */5"
460
- />
461
- </FormGroup>
462
- <FormGroup label="Hour">
463
- <input
464
- type="text"
465
- bind:value={customHour}
466
- {disabled}
467
- class="form-control"
468
- placeholder="0-23, *, */2"
469
- />
470
- </FormGroup>
471
- <FormGroup label="Day of Month">
472
- <input
473
- type="text"
474
- bind:value={customDayOfMonth}
475
- {disabled}
476
- class="form-control"
477
- placeholder="1-31, *, */5"
478
- />
479
- </FormGroup>
480
- <FormGroup label="Month">
481
- <input
482
- type="text"
483
- bind:value={customMonth}
484
- {disabled}
485
- class="form-control"
486
- placeholder="1-12, *, JAN-DEC"
487
- />
488
- </FormGroup>
489
- <FormGroup label="Day of Week">
490
- <input
491
- type="text"
492
- bind:value={customDayOfWeek}
493
- {disabled}
494
- class="form-control"
495
- placeholder="0-6, *, SUN-SAT"
496
- />
497
- </FormGroup>
498
- </div>
499
- {/if}
500
- </div>
501
-
502
- <div class="cron-output">
503
- <div class="cron-expression">
504
- <span class="cron-label">CRON Expression</span>
505
- <code>{value}</code>
506
- </div>
507
- <div class="cron-readable">
508
- <span class="cron-label">Description</span>
509
- <div class="readable-text">{getHumanReadable()}</div>
510
- </div>
511
- <div class="cron-next-run">
512
- <span class="cron-label">Next Run</span>
513
- <div class="next-run-text">{CronParser.getNextRunDescription(value)}</div>
514
- </div>
515
- </div>
516
- </div>
517
- </FormGroup>
518
-
519
- <style>
520
- .cron-builder {
521
- display: flex;
522
- flex-direction: column;
523
- gap: var(--pui-spacing-4);
524
- }
525
-
526
- .cron-builder.disabled {
527
- opacity: 0.6;
528
- pointer-events: none;
529
- }
530
-
531
- .cron-frequency {
532
- width: 100%;
533
- }
534
-
535
- .cron-settings {
536
- display: grid;
537
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
538
- gap: var(--pui-spacing-4);
539
- }
540
-
541
- .weekday-selector {
542
- grid-column: 1 / -1;
543
- }
544
-
545
- .weekday-label {
546
- display: block;
547
- font-size: var(--pui-font-size-sm);
548
- font-weight: var(--pui-font-weight-semibold);
549
- color: var(--pui-text-primary);
550
- margin-bottom: var(--pui-spacing-2);
551
- }
552
-
553
- .weekday-buttons {
554
- display: flex;
555
- gap: var(--pui-spacing-2);
556
- flex-wrap: wrap;
557
- }
558
-
559
- .weekday-btn {
560
- padding: var(--pui-spacing-2) var(--pui-spacing-4);
561
- border: 1px solid var(--pui-border-default);
562
- background: var(--pui-input-bg);
563
- color: var(--pui-text-primary);
564
- border-radius: var(--pui-radius-md);
565
- cursor: pointer;
566
- font-size: var(--pui-font-size-sm);
567
- font-weight: var(--pui-font-weight-medium);
568
- transition: all var(--pui-transition-fast) var(--pui-ease-in-out);
569
- }
570
-
571
- .weekday-btn:hover:not(:disabled) {
572
- background: var(--pui-bg-hover);
573
- border-color: var(--pui-color-primary);
574
- }
575
-
576
- .weekday-btn.selected {
577
- background: var(--pui-color-primary);
578
- color: var(--pui-color-white);
579
- border-color: var(--pui-color-primary);
580
- }
581
-
582
- .weekday-btn:disabled {
583
- cursor: not-allowed;
584
- }
585
-
586
- .custom-inputs {
587
- display: grid;
588
- grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
589
- gap: var(--pui-spacing-4);
590
- grid-column: 1 / -1;
591
- }
592
-
593
- .form-control {
594
- width: 100%;
595
- padding: var(--pui-spacing-2);
596
- border: 1px solid var(--pui-border-default);
597
- border-radius: var(--pui-radius-md);
598
- background: var(--pui-input-bg);
599
- color: var(--pui-text-primary);
600
- font-size: var(--pui-font-size-sm);
601
- }
602
-
603
- .form-control:focus {
604
- outline: none;
605
- border-color: var(--pui-color-primary);
606
- }
607
-
608
- .form-control:disabled {
609
- opacity: 0.6;
610
- cursor: not-allowed;
611
- }
612
-
613
- .cron-output {
614
- display: grid;
615
- grid-template-columns: 1fr 1fr 1fr;
616
- gap: var(--pui-spacing-4);
617
- padding: var(--pui-spacing-4);
618
- background: var(--pui-bg-subtle);
619
- border-radius: var(--pui-radius-md);
620
- border: 1px solid var(--pui-border-default);
621
- }
622
-
623
- .cron-expression,
624
- .cron-readable,
625
- .cron-next-run {
626
- display: flex;
627
- flex-direction: column;
628
- gap: var(--pui-spacing-2);
629
- }
630
-
631
- .cron-output .cron-label {
632
- font-size: var(--pui-font-size-xs);
633
- font-weight: var(--pui-font-weight-semibold);
634
- color: var(--pui-text-muted);
635
- text-transform: uppercase;
636
- letter-spacing: 0.05em;
637
- }
638
-
639
- .cron-expression code {
640
- font-family: 'Courier New', monospace;
641
- font-size: var(--pui-font-size-sm);
642
- padding: var(--pui-spacing-2);
643
- background: var(--pui-input-bg);
644
- border: 1px solid var(--pui-border-default);
645
- border-radius: var(--pui-radius-sm);
646
- color: var(--pui-color-primary);
647
- font-weight: var(--pui-font-weight-semibold);
648
- }
649
-
650
- .readable-text,
651
- .next-run-text {
652
- font-size: var(--pui-font-size-sm);
653
- color: var(--pui-text-primary);
654
- padding: var(--pui-spacing-2);
655
- background: var(--pui-input-bg);
656
- border: 1px solid var(--pui-border-default);
657
- border-radius: var(--pui-radius-sm);
658
- }
659
-
660
- :global(.dark) .weekday-btn:hover:not(:disabled) {
661
- background: var(--pui-bg-hover);
662
- border-color: var(--pui-color-secondary);
663
- }
664
-
665
- :global(.dark) .weekday-btn.selected {
666
- background: var(--pui-color-secondary);
667
- color: var(--pui-color-primary);
668
- border-color: var(--pui-color-secondary);
669
- }
670
-
671
- :global(.dark) .cron-output {
672
- background: var(--pui-bg-subtle);
673
- }
674
-
675
- :global(.dark) .cron-expression code {
676
- color: var(--pui-color-secondary);
677
- }
678
-
679
- @media (max-width: 1024px) {
680
- .cron-output {
681
- grid-template-columns: 1fr;
682
- }
683
- }
684
-
685
- @media (max-width: 768px) {
686
- .cron-settings {
687
- grid-template-columns: 1fr;
688
- }
689
-
690
- .custom-inputs {
691
- grid-template-columns: 1fr;
692
- }
693
- }
1
+ <script lang="ts">
2
+ import FormGroup from './FormGroup.svelte';
3
+ import Select from './Select.svelte';
4
+ import Number from './Number.svelte';
5
+ import Checkbox from './Checkbox.svelte';
6
+ import type { ComboBoxItem } from '../models/ComboBoxItem.js';
7
+ import { type Snippet, onMount, untrack } from 'svelte';
8
+ import { CronParser } from '../util/CronParser.js';
9
+
10
+ interface Props {
11
+ label?: string;
12
+ value?: string;
13
+ placeholder?: string;
14
+ class?: string;
15
+ containerClass?: string;
16
+ disabled?: boolean;
17
+ required?: boolean;
18
+ showError?: boolean;
19
+ errorText?: string;
20
+ tooltipContent?: Snippet;
21
+ tooltipText?: string;
22
+ tooltipLocation?: 'top' | 'bottom' | 'left' | 'right';
23
+ onchange?: (expression: string) => void;
24
+ }
25
+
26
+ let {
27
+ label = 'Schedule',
28
+ value = $bindable('0 0 * * *'),
29
+ placeholder = '',
30
+ class: classes = '',
31
+ containerClass = '',
32
+ disabled = false,
33
+ required = false,
34
+ showError = false,
35
+ errorText = '',
36
+ tooltipContent,
37
+ tooltipText,
38
+ tooltipLocation = 'top',
39
+ onchange
40
+ }: Props = $props();
41
+
42
+ type FrequencyType = 'minute' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom';
43
+
44
+ let frequency = $state<FrequencyType>('daily');
45
+ let isParsingExpression = false;
46
+
47
+ // Minute settings
48
+ let minuteInterval = $state(1);
49
+
50
+ // Hourly settings
51
+ let hourlyMinute = $state(0);
52
+ let hourlyInterval = $state(1);
53
+
54
+ // Daily settings
55
+ let dailyHour = $state(0);
56
+ let dailyMinute = $state(0);
57
+ let dailyInterval = $state(1);
58
+
59
+ // Weekly settings
60
+ let weeklyDays = $state<number[]>([1]); // 0=Sunday, 1=Monday, etc.
61
+ let weeklyHour = $state(0);
62
+ let weeklyMinute = $state(0);
63
+
64
+ // Monthly settings
65
+ let monthlyDay = $state(1);
66
+ let monthlyHour = $state(0);
67
+ let monthlyMinute = $state(0);
68
+
69
+ // Yearly settings
70
+ let yearlyMonth = $state(1);
71
+ let yearlyDay = $state(1);
72
+ let yearlyHour = $state(0);
73
+ let yearlyMinute = $state(0);
74
+
75
+ // Custom settings
76
+ let customMinute = $state('*');
77
+ let customHour = $state('*');
78
+ let customDayOfMonth = $state('*');
79
+ let customMonth = $state('*');
80
+ let customDayOfWeek = $state('*');
81
+
82
+ const frequencyOptions: ComboBoxItem[] = [
83
+ { label: 'Every Minute', value: 'minute' },
84
+ { label: 'Hourly', value: 'hourly' },
85
+ { label: 'Daily', value: 'daily' },
86
+ { label: 'Weekly', value: 'weekly' },
87
+ { label: 'Monthly', value: 'monthly' },
88
+ { label: 'Yearly', value: 'yearly' },
89
+ { label: 'Custom', value: 'custom' }
90
+ ];
91
+
92
+ const dayOptions: ComboBoxItem[] = Array.from({ length: 31 }, (_, i) => ({
93
+ label: String(i + 1),
94
+ value: String(i + 1)
95
+ }));
96
+
97
+ const hourOptions: ComboBoxItem[] = Array.from({ length: 24 }, (_, i) => ({
98
+ label: i.toString().padStart(2, '0'),
99
+ value: String(i)
100
+ }));
101
+
102
+ const minuteOptions: ComboBoxItem[] = Array.from({ length: 60 }, (_, i) => ({
103
+ label: i.toString().padStart(2, '0'),
104
+ value: String(i)
105
+ }));
106
+
107
+ const monthOptions: ComboBoxItem[] = [
108
+ { label: 'January', value: '1' },
109
+ { label: 'February', value: '2' },
110
+ { label: 'March', value: '3' },
111
+ { label: 'April', value: '4' },
112
+ { label: 'May', value: '5' },
113
+ { label: 'June', value: '6' },
114
+ { label: 'July', value: '7' },
115
+ { label: 'August', value: '8' },
116
+ { label: 'September', value: '9' },
117
+ { label: 'October', value: '10' },
118
+ { label: 'November', value: '11' },
119
+ { label: 'December', value: '12' }
120
+ ];
121
+
122
+ const weekDays = [
123
+ { label: 'Sunday', value: 0 },
124
+ { label: 'Monday', value: 1 },
125
+ { label: 'Tuesday', value: 2 },
126
+ { label: 'Wednesday', value: 3 },
127
+ { label: 'Thursday', value: 4 },
128
+ { label: 'Friday', value: 5 },
129
+ { label: 'Saturday', value: 6 }
130
+ ];
131
+
132
+ function generateExpression(): string {
133
+ let expr = '';
134
+
135
+ switch (frequency) {
136
+ case 'minute':
137
+ expr = `*/${minuteInterval} * * * *`;
138
+ break;
139
+
140
+ case 'hourly':
141
+ expr = `${hourlyMinute} */${hourlyInterval} * * *`;
142
+ break;
143
+
144
+ case 'daily':
145
+ expr = `${dailyMinute} ${dailyHour} */${dailyInterval} * *`;
146
+ break;
147
+
148
+ case 'weekly':
149
+ const days = weeklyDays.length > 0 ? weeklyDays.sort().join(',') : '*';
150
+ expr = `${weeklyMinute} ${weeklyHour} * * ${days}`;
151
+ break;
152
+
153
+ case 'monthly':
154
+ expr = `${monthlyMinute} ${monthlyHour} ${monthlyDay} * *`;
155
+ break;
156
+
157
+ case 'yearly':
158
+ expr = `${yearlyMinute} ${yearlyHour} ${yearlyDay} ${yearlyMonth} *`;
159
+ break;
160
+
161
+ case 'custom':
162
+ expr = `${customMinute} ${customHour} ${customDayOfMonth} ${customMonth} ${customDayOfWeek}`;
163
+ break;
164
+ }
165
+
166
+ return expr;
167
+ }
168
+
169
+ function parseExpression(expr: string) {
170
+ if (!expr || isParsingExpression) return;
171
+
172
+ const parts = expr.trim().split(/\s+/);
173
+ if (parts.length !== 5) return;
174
+
175
+ const [min, hour, dayOfMonth, month, dayOfWeek] = parts;
176
+
177
+ isParsingExpression = true;
178
+
179
+ // Try to detect frequency type
180
+ if (min.startsWith('*/') && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
181
+ frequency = 'minute';
182
+ minuteInterval = parseInt(min.split('/')[1]) || 1;
183
+ } else if (hour.startsWith('*/') && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
184
+ frequency = 'hourly';
185
+ hourlyMinute = parseInt(min) || 0;
186
+ hourlyInterval = parseInt(hour.split('/')[1]) || 1;
187
+ } else if (dayOfMonth.startsWith('*/') && month === '*' && dayOfWeek === '*') {
188
+ frequency = 'daily';
189
+ dailyMinute = parseInt(min) || 0;
190
+ dailyHour = parseInt(hour) || 0;
191
+ dailyInterval = parseInt(dayOfMonth.split('/')[1]) || 1;
192
+ } else if (dayOfMonth === '*' && month === '*' && dayOfWeek !== '*') {
193
+ frequency = 'weekly';
194
+ weeklyMinute = parseInt(min) || 0;
195
+ weeklyHour = parseInt(hour) || 0;
196
+ weeklyDays = dayOfWeek.split(',').map(d => parseInt(d)).filter(d => !isNaN(d));
197
+ } else if (month === '*' && dayOfWeek === '*' && !dayOfMonth.includes('*')) {
198
+ frequency = 'monthly';
199
+ monthlyMinute = parseInt(min) || 0;
200
+ monthlyHour = parseInt(hour) || 0;
201
+ monthlyDay = parseInt(dayOfMonth) || 1;
202
+ } else if (dayOfWeek === '*' && !month.includes('*') && !dayOfMonth.includes('*')) {
203
+ frequency = 'yearly';
204
+ yearlyMinute = parseInt(min) || 0;
205
+ yearlyHour = parseInt(hour) || 0;
206
+ yearlyDay = parseInt(dayOfMonth) || 1;
207
+ yearlyMonth = parseInt(month) || 1;
208
+ } else {
209
+ frequency = 'custom';
210
+ customMinute = min;
211
+ customHour = hour;
212
+ customDayOfMonth = dayOfMonth;
213
+ customMonth = month;
214
+ customDayOfWeek = dayOfWeek;
215
+ }
216
+
217
+ isParsingExpression = false;
218
+ }
219
+
220
+ function toggleWeekday(day: number) {
221
+ if (weeklyDays.includes(day)) {
222
+ weeklyDays = weeklyDays.filter(d => d !== day);
223
+ } else {
224
+ weeklyDays = [...weeklyDays, day];
225
+ }
226
+ }
227
+
228
+ // Parse initial expression on mount
229
+ onMount(() => {
230
+ if (value) {
231
+ parseExpression(value);
232
+ }
233
+ });
234
+
235
+ // Generate expression when settings change
236
+ $effect(() => {
237
+ // Track dependencies
238
+ frequency;
239
+ minuteInterval;
240
+ hourlyMinute;
241
+ hourlyInterval;
242
+ dailyHour;
243
+ dailyMinute;
244
+ dailyInterval;
245
+ weeklyDays;
246
+ weeklyHour;
247
+ weeklyMinute;
248
+ monthlyDay;
249
+ monthlyHour;
250
+ monthlyMinute;
251
+ yearlyMonth;
252
+ yearlyDay;
253
+ yearlyHour;
254
+ yearlyMinute;
255
+ customMinute;
256
+ customHour;
257
+ customDayOfMonth;
258
+ customMonth;
259
+ customDayOfWeek;
260
+
261
+ untrack(() => {
262
+ if (!isParsingExpression) {
263
+ const expr = generateExpression();
264
+ value = expr;
265
+ onchange?.(expr);
266
+ }
267
+ });
268
+ });
269
+
270
+ function getHumanReadable(): string {
271
+ switch (frequency) {
272
+ case 'minute':
273
+ return minuteInterval === 1 ? 'Every minute' : `Every ${minuteInterval} minutes`;
274
+
275
+ case 'hourly':
276
+ const hrText = hourlyInterval === 1 ? 'hour' : `${hourlyInterval} hours`;
277
+ return `Every ${hrText} at minute ${hourlyMinute}`;
278
+
279
+ case 'daily':
280
+ const dayText = dailyInterval === 1 ? 'day' : `${dailyInterval} days`;
281
+ return `Every ${dayText} at ${dailyHour.toString().padStart(2, '0')}:${dailyMinute.toString().padStart(2, '0')}`;
282
+
283
+ case 'weekly':
284
+ const dayNames = weeklyDays.map(d => weekDays.find(wd => wd.value === d)?.label).join(', ');
285
+ return `Every ${dayNames} at ${weeklyHour.toString().padStart(2, '0')}:${weeklyMinute.toString().padStart(2, '0')}`;
286
+
287
+ case 'monthly':
288
+ return `Day ${monthlyDay} of every month at ${monthlyHour.toString().padStart(2, '0')}:${monthlyMinute.toString().padStart(2, '0')}`;
289
+
290
+ case 'yearly':
291
+ const monthName = monthOptions.find(m => m.value === yearlyMonth.toString())?.label;
292
+ return `${monthName} ${yearlyDay} at ${yearlyHour.toString().padStart(2, '0')}:${yearlyMinute.toString().padStart(2, '0')}`;
293
+
294
+ case 'custom':
295
+ return value;
296
+
297
+ default:
298
+ return value;
299
+ }
300
+ }
301
+ </script>
302
+
303
+ <FormGroup
304
+ {label}
305
+ {required}
306
+ {showError}
307
+ {errorText}
308
+ {tooltipContent}
309
+ {tooltipText}
310
+ {tooltipLocation}
311
+ class={containerClass}
312
+ >
313
+ <div class="cron-builder {classes}" class:disabled>
314
+ <div class="cron-frequency">
315
+ <Select
316
+ label="Frequency"
317
+ bind:value={frequency}
318
+ options={frequencyOptions}
319
+ {disabled}
320
+ />
321
+ </div>
322
+
323
+ <div class="cron-settings">
324
+ {#if frequency === 'minute'}
325
+ <Number
326
+ label="Interval (minutes)"
327
+ bind:value={minuteInterval}
328
+ min={1}
329
+ max={59}
330
+ {disabled}
331
+ />
332
+ {:else if frequency === 'hourly'}
333
+ <Number
334
+ label="Interval (hours)"
335
+ bind:value={hourlyInterval}
336
+ min={1}
337
+ max={23}
338
+ {disabled}
339
+ />
340
+ <Number
341
+ label="At minute"
342
+ bind:value={hourlyMinute}
343
+ min={0}
344
+ max={59}
345
+ {disabled}
346
+ />
347
+ {:else if frequency === 'daily'}
348
+ <Number
349
+ label="Interval (days)"
350
+ bind:value={dailyInterval}
351
+ min={1}
352
+ max={31}
353
+ {disabled}
354
+ />
355
+ <Number
356
+ label="Hour"
357
+ bind:value={dailyHour}
358
+ min={0}
359
+ max={23}
360
+ {disabled}
361
+ />
362
+ <Number
363
+ label="Minute"
364
+ bind:value={dailyMinute}
365
+ min={0}
366
+ max={59}
367
+ {disabled}
368
+ />
369
+ {:else if frequency === 'weekly'}
370
+ <div class="weekday-selector">
371
+ <span class="weekday-label">Days of Week</span>
372
+ <div class="weekday-buttons">
373
+ {#each weekDays as day}
374
+ <button
375
+ type="button"
376
+ class="weekday-btn"
377
+ class:selected={weeklyDays.includes(day.value)}
378
+ onclick={() => toggleWeekday(day.value)}
379
+ {disabled}
380
+ >
381
+ {day.label.substring(0, 3)}
382
+ </button>
383
+ {/each}
384
+ </div>
385
+ </div>
386
+ <Number
387
+ label="Hour"
388
+ bind:value={weeklyHour}
389
+ min={0}
390
+ max={23}
391
+ {disabled}
392
+ />
393
+ <Number
394
+ label="Minute"
395
+ bind:value={weeklyMinute}
396
+ min={0}
397
+ max={59}
398
+ {disabled}
399
+ />
400
+ {:else if frequency === 'monthly'}
401
+ <Number
402
+ label="Day of Month"
403
+ bind:value={monthlyDay}
404
+ min={1}
405
+ max={31}
406
+ {disabled}
407
+ />
408
+ <Number
409
+ label="Hour"
410
+ bind:value={monthlyHour}
411
+ min={0}
412
+ max={23}
413
+ {disabled}
414
+ />
415
+ <Number
416
+ label="Minute"
417
+ bind:value={monthlyMinute}
418
+ min={0}
419
+ max={59}
420
+ {disabled}
421
+ />
422
+ {:else if frequency === 'yearly'}
423
+ <Number
424
+ label="Month"
425
+ bind:value={yearlyMonth}
426
+ min={1}
427
+ max={12}
428
+ {disabled}
429
+ />
430
+ <Number
431
+ label="Day"
432
+ bind:value={yearlyDay}
433
+ min={1}
434
+ max={31}
435
+ {disabled}
436
+ />
437
+ <Number
438
+ label="Hour"
439
+ bind:value={yearlyHour}
440
+ min={0}
441
+ max={23}
442
+ {disabled}
443
+ />
444
+ <Number
445
+ label="Minute"
446
+ bind:value={yearlyMinute}
447
+ min={0}
448
+ max={59}
449
+ {disabled}
450
+ />
451
+ {:else if frequency === 'custom'}
452
+ <div class="custom-inputs">
453
+ <FormGroup label="Minute">
454
+ <input
455
+ type="text"
456
+ bind:value={customMinute}
457
+ {disabled}
458
+ class="form-control"
459
+ placeholder="0-59, *, */5"
460
+ />
461
+ </FormGroup>
462
+ <FormGroup label="Hour">
463
+ <input
464
+ type="text"
465
+ bind:value={customHour}
466
+ {disabled}
467
+ class="form-control"
468
+ placeholder="0-23, *, */2"
469
+ />
470
+ </FormGroup>
471
+ <FormGroup label="Day of Month">
472
+ <input
473
+ type="text"
474
+ bind:value={customDayOfMonth}
475
+ {disabled}
476
+ class="form-control"
477
+ placeholder="1-31, *, */5"
478
+ />
479
+ </FormGroup>
480
+ <FormGroup label="Month">
481
+ <input
482
+ type="text"
483
+ bind:value={customMonth}
484
+ {disabled}
485
+ class="form-control"
486
+ placeholder="1-12, *, JAN-DEC"
487
+ />
488
+ </FormGroup>
489
+ <FormGroup label="Day of Week">
490
+ <input
491
+ type="text"
492
+ bind:value={customDayOfWeek}
493
+ {disabled}
494
+ class="form-control"
495
+ placeholder="0-6, *, SUN-SAT"
496
+ />
497
+ </FormGroup>
498
+ </div>
499
+ {/if}
500
+ </div>
501
+
502
+ <div class="cron-output">
503
+ <div class="cron-expression">
504
+ <span class="cron-label">CRON Expression</span>
505
+ <code>{value}</code>
506
+ </div>
507
+ <div class="cron-readable">
508
+ <span class="cron-label">Description</span>
509
+ <div class="readable-text">{getHumanReadable()}</div>
510
+ </div>
511
+ <div class="cron-next-run">
512
+ <span class="cron-label">Next Run</span>
513
+ <div class="next-run-text">{CronParser.getNextRunDescription(value)}</div>
514
+ </div>
515
+ </div>
516
+ </div>
517
+ </FormGroup>
518
+
519
+ <style>
520
+ .cron-builder {
521
+ display: flex;
522
+ flex-direction: column;
523
+ gap: var(--pui-spacing-4);
524
+ }
525
+
526
+ .cron-builder.disabled {
527
+ opacity: 0.6;
528
+ pointer-events: none;
529
+ }
530
+
531
+ .cron-frequency {
532
+ width: 100%;
533
+ }
534
+
535
+ .cron-settings {
536
+ display: grid;
537
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
538
+ gap: var(--pui-spacing-4);
539
+ }
540
+
541
+ .weekday-selector {
542
+ grid-column: 1 / -1;
543
+ }
544
+
545
+ .weekday-label {
546
+ display: block;
547
+ font-size: var(--pui-font-size-sm);
548
+ font-weight: var(--pui-font-weight-semibold);
549
+ color: var(--pui-text-primary);
550
+ margin-bottom: var(--pui-spacing-2);
551
+ }
552
+
553
+ .weekday-buttons {
554
+ display: flex;
555
+ gap: var(--pui-spacing-2);
556
+ flex-wrap: wrap;
557
+ }
558
+
559
+ .weekday-btn {
560
+ padding: var(--pui-spacing-2) var(--pui-spacing-4);
561
+ border: 1px solid var(--pui-border-default);
562
+ background: var(--pui-input-bg);
563
+ color: var(--pui-text-primary);
564
+ border-radius: var(--pui-radius-md);
565
+ cursor: pointer;
566
+ font-size: var(--pui-font-size-sm);
567
+ font-weight: var(--pui-font-weight-medium);
568
+ transition: all var(--pui-transition-fast) var(--pui-ease-in-out);
569
+ }
570
+
571
+ .weekday-btn:hover:not(:disabled) {
572
+ background: var(--pui-bg-hover);
573
+ border-color: var(--pui-color-primary);
574
+ }
575
+
576
+ .weekday-btn.selected {
577
+ background: var(--pui-color-primary);
578
+ color: var(--pui-color-white);
579
+ border-color: var(--pui-color-primary);
580
+ }
581
+
582
+ .weekday-btn:disabled {
583
+ cursor: not-allowed;
584
+ }
585
+
586
+ .custom-inputs {
587
+ display: grid;
588
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
589
+ gap: var(--pui-spacing-4);
590
+ grid-column: 1 / -1;
591
+ }
592
+
593
+ .form-control {
594
+ width: 100%;
595
+ padding: var(--pui-spacing-2);
596
+ border: 1px solid var(--pui-border-default);
597
+ border-radius: var(--pui-radius-md);
598
+ background: var(--pui-input-bg);
599
+ color: var(--pui-text-primary);
600
+ font-size: var(--pui-font-size-sm);
601
+ }
602
+
603
+ .form-control:focus {
604
+ outline: none;
605
+ border-color: var(--pui-color-primary);
606
+ }
607
+
608
+ .form-control:disabled {
609
+ opacity: 0.6;
610
+ cursor: not-allowed;
611
+ }
612
+
613
+ .cron-output {
614
+ display: grid;
615
+ grid-template-columns: 1fr 1fr 1fr;
616
+ gap: var(--pui-spacing-4);
617
+ padding: var(--pui-spacing-4);
618
+ background: var(--pui-bg-subtle);
619
+ border-radius: var(--pui-radius-md);
620
+ border: 1px solid var(--pui-border-default);
621
+ }
622
+
623
+ .cron-expression,
624
+ .cron-readable,
625
+ .cron-next-run {
626
+ display: flex;
627
+ flex-direction: column;
628
+ gap: var(--pui-spacing-2);
629
+ }
630
+
631
+ .cron-output .cron-label {
632
+ font-size: var(--pui-font-size-xs);
633
+ font-weight: var(--pui-font-weight-semibold);
634
+ color: var(--pui-text-muted);
635
+ text-transform: uppercase;
636
+ letter-spacing: 0.05em;
637
+ }
638
+
639
+ .cron-expression code {
640
+ font-family: 'Courier New', monospace;
641
+ font-size: var(--pui-font-size-sm);
642
+ padding: var(--pui-spacing-2);
643
+ background: var(--pui-input-bg);
644
+ border: 1px solid var(--pui-border-default);
645
+ border-radius: var(--pui-radius-sm);
646
+ color: var(--pui-color-primary);
647
+ font-weight: var(--pui-font-weight-semibold);
648
+ }
649
+
650
+ .readable-text,
651
+ .next-run-text {
652
+ font-size: var(--pui-font-size-sm);
653
+ color: var(--pui-text-primary);
654
+ padding: var(--pui-spacing-2);
655
+ background: var(--pui-input-bg);
656
+ border: 1px solid var(--pui-border-default);
657
+ border-radius: var(--pui-radius-sm);
658
+ }
659
+
660
+ :global(.dark) .weekday-btn:hover:not(:disabled) {
661
+ background: var(--pui-bg-hover);
662
+ border-color: var(--pui-color-secondary);
663
+ }
664
+
665
+ :global(.dark) .weekday-btn.selected {
666
+ background: var(--pui-color-secondary);
667
+ color: var(--pui-color-primary);
668
+ border-color: var(--pui-color-secondary);
669
+ }
670
+
671
+ :global(.dark) .cron-output {
672
+ background: var(--pui-bg-subtle);
673
+ }
674
+
675
+ :global(.dark) .cron-expression code {
676
+ color: var(--pui-color-secondary);
677
+ }
678
+
679
+ @media (max-width: 1024px) {
680
+ .cron-output {
681
+ grid-template-columns: 1fr;
682
+ }
683
+ }
684
+
685
+ @media (max-width: 768px) {
686
+ .cron-settings {
687
+ grid-template-columns: 1fr;
688
+ }
689
+
690
+ .custom-inputs {
691
+ grid-template-columns: 1fr;
692
+ }
693
+ }
694
694
  </style>