@stigmer/react 3.5.2 → 3.6.0

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 (193) hide show
  1. package/agent/AgentPicker.d.ts +10 -1
  2. package/agent/AgentPicker.d.ts.map +1 -1
  3. package/agent/AgentPicker.js +2 -2
  4. package/agent/AgentPicker.js.map +1 -1
  5. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  6. package/channel/AgentChannelsPanel.js +22 -5
  7. package/channel/AgentChannelsPanel.js.map +1 -1
  8. package/channel/ChannelTemplatesDialog.d.ts +53 -0
  9. package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
  10. package/channel/ChannelTemplatesDialog.js +150 -0
  11. package/channel/ChannelTemplatesDialog.js.map +1 -0
  12. package/channel/index.d.ts +8 -0
  13. package/channel/index.d.ts.map +1 -1
  14. package/channel/index.js +4 -0
  15. package/channel/index.js.map +1 -1
  16. package/channel/providers.d.ts +12 -0
  17. package/channel/providers.d.ts.map +1 -1
  18. package/channel/providers.js +14 -2
  19. package/channel/providers.js.map +1 -1
  20. package/channel/templatePresentation.d.ts +37 -0
  21. package/channel/templatePresentation.d.ts.map +1 -0
  22. package/channel/templatePresentation.js +69 -0
  23. package/channel/templatePresentation.js.map +1 -0
  24. package/channel/useChannelTemplateList.d.ts +64 -0
  25. package/channel/useChannelTemplateList.d.ts.map +1 -0
  26. package/channel/useChannelTemplateList.js +69 -0
  27. package/channel/useChannelTemplateList.js.map +1 -0
  28. package/channel/useChannelTemplateReadiness.d.ts +53 -0
  29. package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
  30. package/channel/useChannelTemplateReadiness.js +42 -0
  31. package/channel/useChannelTemplateReadiness.js.map +1 -0
  32. package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
  33. package/dashboard/useDashboardFailedRuns.js +2 -0
  34. package/dashboard/useDashboardFailedRuns.js.map +1 -1
  35. package/index.d.ts +4 -2
  36. package/index.d.ts.map +1 -1
  37. package/index.js +9 -1
  38. package/index.js.map +1 -1
  39. package/library/useExportResource.d.ts +4 -3
  40. package/library/useExportResource.d.ts.map +1 -1
  41. package/library/useExportResource.js +1 -1
  42. package/library/useExportResource.js.map +1 -1
  43. package/organization/OrgProvider.d.ts +11 -4
  44. package/organization/OrgProvider.d.ts.map +1 -1
  45. package/organization/OrgProvider.js +28 -4
  46. package/organization/OrgProvider.js.map +1 -1
  47. package/package.json +4 -4
  48. package/resource-detail/useDeleteResource.d.ts +1 -1
  49. package/resource-detail/useDeleteResource.d.ts.map +1 -1
  50. package/resource-detail/useDeleteResource.js +5 -0
  51. package/resource-detail/useDeleteResource.js.map +1 -1
  52. package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
  53. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  54. package/resource-workbench/components/ResourceWorkbench.js +6 -2
  55. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  56. package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
  57. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  58. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  59. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  60. package/schedule/CadenceField.d.ts +31 -0
  61. package/schedule/CadenceField.d.ts.map +1 -0
  62. package/schedule/CadenceField.js +116 -0
  63. package/schedule/CadenceField.js.map +1 -0
  64. package/schedule/ScheduleDetailView.d.ts +58 -0
  65. package/schedule/ScheduleDetailView.d.ts.map +1 -0
  66. package/schedule/ScheduleDetailView.js +236 -0
  67. package/schedule/ScheduleDetailView.js.map +1 -0
  68. package/schedule/ScheduleForm.d.ts +43 -0
  69. package/schedule/ScheduleForm.d.ts.map +1 -0
  70. package/schedule/ScheduleForm.js +129 -0
  71. package/schedule/ScheduleForm.js.map +1 -0
  72. package/schedule/ScheduleRowActions.d.ts +20 -0
  73. package/schedule/ScheduleRowActions.d.ts.map +1 -0
  74. package/schedule/ScheduleRowActions.js +54 -0
  75. package/schedule/ScheduleRowActions.js.map +1 -0
  76. package/schedule/TimeZoneField.d.ts +32 -0
  77. package/schedule/TimeZoneField.d.ts.map +1 -0
  78. package/schedule/TimeZoneField.js +104 -0
  79. package/schedule/TimeZoneField.js.map +1 -0
  80. package/schedule/cadence.d.ts +95 -0
  81. package/schedule/cadence.d.ts.map +1 -0
  82. package/schedule/cadence.js +242 -0
  83. package/schedule/cadence.js.map +1 -0
  84. package/schedule/index.d.ts +31 -0
  85. package/schedule/index.d.ts.map +1 -0
  86. package/schedule/index.js +16 -0
  87. package/schedule/index.js.map +1 -0
  88. package/schedule/scheduleColumns.d.ts +26 -0
  89. package/schedule/scheduleColumns.d.ts.map +1 -0
  90. package/schedule/scheduleColumns.js +77 -0
  91. package/schedule/scheduleColumns.js.map +1 -0
  92. package/schedule/scheduleListFn.d.ts +50 -0
  93. package/schedule/scheduleListFn.d.ts.map +1 -0
  94. package/schedule/scheduleListFn.js +65 -0
  95. package/schedule/scheduleListFn.js.map +1 -0
  96. package/schedule/scheduleState.d.ts +43 -0
  97. package/schedule/scheduleState.d.ts.map +1 -0
  98. package/schedule/scheduleState.js +67 -0
  99. package/schedule/scheduleState.js.map +1 -0
  100. package/schedule/useCreateSchedule.d.ts +57 -0
  101. package/schedule/useCreateSchedule.d.ts.map +1 -0
  102. package/schedule/useCreateSchedule.js +61 -0
  103. package/schedule/useCreateSchedule.js.map +1 -0
  104. package/schedule/useResumeSchedule.d.ts +30 -0
  105. package/schedule/useResumeSchedule.d.ts.map +1 -0
  106. package/schedule/useResumeSchedule.js +45 -0
  107. package/schedule/useResumeSchedule.js.map +1 -0
  108. package/schedule/useSchedule.d.ts +39 -0
  109. package/schedule/useSchedule.d.ts.map +1 -0
  110. package/schedule/useSchedule.js +44 -0
  111. package/schedule/useSchedule.js.map +1 -0
  112. package/schedule/useScheduleCount.d.ts +38 -0
  113. package/schedule/useScheduleCount.d.ts.map +1 -0
  114. package/schedule/useScheduleCount.js +30 -0
  115. package/schedule/useScheduleCount.js.map +1 -0
  116. package/schedule/useScheduleList.d.ts +48 -0
  117. package/schedule/useScheduleList.d.ts.map +1 -0
  118. package/schedule/useScheduleList.js +43 -0
  119. package/schedule/useScheduleList.js.map +1 -0
  120. package/schedule/useSetScheduleEnabled.d.ts +31 -0
  121. package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
  122. package/schedule/useSetScheduleEnabled.js +53 -0
  123. package/schedule/useSetScheduleEnabled.js.map +1 -0
  124. package/schedule/useTriggerSchedule.d.ts +28 -0
  125. package/schedule/useTriggerSchedule.d.ts.map +1 -0
  126. package/schedule/useTriggerSchedule.js +43 -0
  127. package/schedule/useTriggerSchedule.js.map +1 -0
  128. package/search/useResourceCount.d.ts +2 -1
  129. package/search/useResourceCount.d.ts.map +1 -1
  130. package/search/useResourceCount.js +6 -1
  131. package/search/useResourceCount.js.map +1 -1
  132. package/src/agent/AgentPicker.tsx +13 -1
  133. package/src/channel/AgentChannelsPanel.tsx +43 -1
  134. package/src/channel/ChannelTemplatesDialog.tsx +433 -0
  135. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
  136. package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
  137. package/src/channel/__tests__/providers.test.ts +9 -0
  138. package/src/channel/__tests__/templatePresentation.test.ts +96 -0
  139. package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
  140. package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
  141. package/src/channel/index.ts +15 -0
  142. package/src/channel/providers.ts +26 -2
  143. package/src/channel/templatePresentation.ts +82 -0
  144. package/src/channel/useChannelTemplateList.ts +123 -0
  145. package/src/channel/useChannelTemplateReadiness.ts +69 -0
  146. package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
  147. package/src/dashboard/useDashboardFailedRuns.ts +2 -0
  148. package/src/execution/__tests__/PlanArtifactCard.test.tsx +23 -2
  149. package/src/index.ts +65 -0
  150. package/src/library/useExportResource.ts +4 -3
  151. package/src/organization/OrgProvider.tsx +29 -4
  152. package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
  153. package/src/resource-detail/useDeleteResource.ts +11 -1
  154. package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
  155. package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
  156. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  157. package/src/schedule/CadenceField.tsx +365 -0
  158. package/src/schedule/ScheduleDetailView.tsx +583 -0
  159. package/src/schedule/ScheduleForm.tsx +405 -0
  160. package/src/schedule/ScheduleRowActions.tsx +124 -0
  161. package/src/schedule/TimeZoneField.tsx +199 -0
  162. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +260 -0
  163. package/src/schedule/__tests__/cadence.test.ts +278 -0
  164. package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
  165. package/src/schedule/__tests__/scheduleCreation.test.tsx +340 -0
  166. package/src/schedule/__tests__/scheduleHooks.test.tsx +312 -0
  167. package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
  168. package/src/schedule/__tests__/scheduleState.test.ts +133 -0
  169. package/src/schedule/cadence.ts +298 -0
  170. package/src/schedule/index.ts +57 -0
  171. package/src/schedule/scheduleColumns.tsx +118 -0
  172. package/src/schedule/scheduleListFn.ts +108 -0
  173. package/src/schedule/scheduleState.ts +96 -0
  174. package/src/schedule/useCreateSchedule.ts +88 -0
  175. package/src/schedule/useResumeSchedule.ts +67 -0
  176. package/src/schedule/useSchedule.ts +67 -0
  177. package/src/schedule/useScheduleCount.ts +57 -0
  178. package/src/schedule/useScheduleList.ts +83 -0
  179. package/src/schedule/useSetScheduleEnabled.ts +84 -0
  180. package/src/schedule/useTriggerSchedule.ts +65 -0
  181. package/src/search/__tests__/useResourceCount.test.tsx +89 -0
  182. package/src/search/useResourceCount.ts +8 -2
  183. package/src/workflow/WorkflowDashboard.tsx +1 -0
  184. package/src/workflow/useWorkflowExecutionList.ts +13 -2
  185. package/styles.css +1 -1
  186. package/workflow/CanvasTransitionEdge.d.ts +1 -1
  187. package/workflow/WorkflowDashboard.d.ts.map +1 -1
  188. package/workflow/WorkflowDashboard.js +1 -0
  189. package/workflow/WorkflowDashboard.js.map +1 -1
  190. package/workflow/useWorkflowExecutionList.d.ts +10 -0
  191. package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
  192. package/workflow/useWorkflowExecutionList.js +3 -2
  193. package/workflow/useWorkflowExecutionList.js.map +1 -1
@@ -0,0 +1,365 @@
1
+ "use client";
2
+
3
+ import { useCallback, useId } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import {
6
+ cadenceToCron,
7
+ describeCadence,
8
+ formatTime,
9
+ validateCron,
10
+ WEEKDAY_LABELS,
11
+ type CadenceKind,
12
+ type CadencePreset,
13
+ } from "./cadence.js";
14
+
15
+ /** Props for {@link CadenceField}. */
16
+ export interface CadenceFieldProps {
17
+ /** The current cadence. */
18
+ readonly value: CadencePreset;
19
+ /** Called with the new cadence on every edit. */
20
+ readonly onChange: (preset: CadencePreset) => void;
21
+ /** Time zone name, echoed into the summary line. */
22
+ readonly timeZone?: string;
23
+ /** Prevents interaction when `true`. */
24
+ readonly disabled?: boolean;
25
+ }
26
+
27
+ const KIND_OPTIONS: readonly { kind: CadenceKind; label: string }[] = [
28
+ { kind: "hourly", label: "Hourly" },
29
+ { kind: "daily", label: "Daily" },
30
+ { kind: "weekly", label: "Weekly" },
31
+ { kind: "monthly", label: "Monthly" },
32
+ { kind: "custom", label: "Custom cron" },
33
+ ];
34
+
35
+ /**
36
+ * Human-friendly cadence builder for schedules.
37
+ *
38
+ * Presents preset cadences (hourly / daily / weekly / monthly) with
39
+ * plain time-and-day inputs, generating the 5-field cron expression by
40
+ * construction — the user never writes cron unless they choose the
41
+ * Custom escape hatch, where the raw expression is validated instantly
42
+ * with the platform's own lexical rules ({@link validateCron}).
43
+ *
44
+ * A live plain-English summary ({@link describeCadence}) always states
45
+ * what was chosen. It describes the preset, not a computed fire-time
46
+ * forecast — the authoritative next fire time is published by the
47
+ * server as `status.next_fire_at` after apply.
48
+ *
49
+ * Controlled component over {@link CadencePreset}; convert to the spec
50
+ * string with {@link cadenceToCron} at submit time.
51
+ */
52
+ export function CadenceField({
53
+ value,
54
+ onChange,
55
+ timeZone,
56
+ disabled,
57
+ }: CadenceFieldProps) {
58
+ const groupId = useId();
59
+
60
+ const switchKind = useCallback(
61
+ (kind: CadenceKind) => {
62
+ if (kind === value.kind) return;
63
+ onChange(presetForKind(kind, value));
64
+ },
65
+ [value, onChange],
66
+ );
67
+
68
+ const cronError =
69
+ value.kind === "custom" && value.cron.trim() !== ""
70
+ ? validateCron(value.cron.trim())
71
+ : null;
72
+
73
+ return (
74
+ <div className="space-y-2">
75
+ {/* Preset selector — segmented radio group, matching ScopeToggle's pattern */}
76
+ <div
77
+ role="radiogroup"
78
+ aria-label="Cadence"
79
+ aria-disabled={disabled || undefined}
80
+ className={cn(
81
+ "inline-flex flex-wrap rounded-md bg-muted p-0.5",
82
+ disabled && "pointer-events-none opacity-50",
83
+ )}
84
+ >
85
+ {KIND_OPTIONS.map((option) => {
86
+ const isSelected = value.kind === option.kind;
87
+ return (
88
+ <button
89
+ key={option.kind}
90
+ type="button"
91
+ role="radio"
92
+ aria-checked={isSelected}
93
+ tabIndex={isSelected ? 0 : -1}
94
+ disabled={disabled}
95
+ onClick={() => switchKind(option.kind)}
96
+ onKeyDown={(e) => {
97
+ if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
98
+ e.preventDefault();
99
+ const idx = KIND_OPTIONS.findIndex((o) => o.kind === value.kind);
100
+ const delta = e.key === "ArrowRight" ? 1 : -1;
101
+ const next =
102
+ KIND_OPTIONS[
103
+ (idx + delta + KIND_OPTIONS.length) % KIND_OPTIONS.length
104
+ ];
105
+ switchKind(next.kind);
106
+ }
107
+ }}
108
+ className={cn(
109
+ "inline-flex cursor-pointer items-center rounded-sm px-2.5 py-1 text-xs font-medium transition-colors",
110
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
111
+ isSelected
112
+ ? "bg-background text-foreground shadow-sm"
113
+ : "text-muted-foreground hover:text-foreground",
114
+ )}
115
+ >
116
+ {option.label}
117
+ </button>
118
+ );
119
+ })}
120
+ </div>
121
+
122
+ {/* Per-preset inputs */}
123
+ {value.kind === "hourly" && (
124
+ <div className="flex items-center gap-2 text-xs text-foreground">
125
+ <label htmlFor={`${groupId}-minute`}>At minute</label>
126
+ <input
127
+ id={`${groupId}-minute`}
128
+ type="number"
129
+ min={0}
130
+ max={59}
131
+ value={value.minute}
132
+ onChange={(e) =>
133
+ onChange({
134
+ kind: "hourly",
135
+ minute: clampInt(e.target.value, 0, 59, value.minute),
136
+ })
137
+ }
138
+ disabled={disabled}
139
+ className={numberInputClasses}
140
+ />
141
+ </div>
142
+ )}
143
+
144
+ {value.kind === "daily" && (
145
+ <TimeInput
146
+ id={`${groupId}-time`}
147
+ hour={value.hour}
148
+ minute={value.minute}
149
+ disabled={disabled}
150
+ onChange={(hour, minute) => onChange({ kind: "daily", hour, minute })}
151
+ />
152
+ )}
153
+
154
+ {value.kind === "weekly" && (
155
+ <div className="space-y-2">
156
+ <div
157
+ role="group"
158
+ aria-label="Days of week"
159
+ className="flex flex-wrap gap-1"
160
+ >
161
+ {WEEKDAY_LABELS.map((label, day) => {
162
+ const isOn = value.days.includes(day);
163
+ // The last selected day cannot be removed — a weekly
164
+ // schedule with zero days is not expressible in cron.
165
+ const isLastSelected = isOn && value.days.length === 1;
166
+ return (
167
+ <button
168
+ key={label}
169
+ type="button"
170
+ aria-pressed={isOn}
171
+ aria-label={label}
172
+ title={label}
173
+ disabled={disabled || isLastSelected}
174
+ onClick={() =>
175
+ onChange({
176
+ ...value,
177
+ days: isOn
178
+ ? value.days.filter((d) => d !== day)
179
+ : [...value.days, day].sort((a, b) => a - b),
180
+ })
181
+ }
182
+ className={cn(
183
+ "inline-flex h-7 w-9 items-center justify-center rounded-md border text-xs font-medium transition-colors",
184
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
185
+ "disabled:pointer-events-none",
186
+ isOn
187
+ ? "border-primary bg-primary text-primary-foreground"
188
+ : "border-input bg-background text-muted-foreground hover:text-foreground",
189
+ isLastSelected && "opacity-70",
190
+ )}
191
+ >
192
+ {label.slice(0, 3)}
193
+ </button>
194
+ );
195
+ })}
196
+ </div>
197
+ <TimeInput
198
+ id={`${groupId}-time`}
199
+ hour={value.hour}
200
+ minute={value.minute}
201
+ disabled={disabled}
202
+ onChange={(hour, minute) => onChange({ ...value, hour, minute })}
203
+ />
204
+ </div>
205
+ )}
206
+
207
+ {value.kind === "monthly" && (
208
+ <div className="space-y-2">
209
+ <div className="flex items-center gap-2 text-xs text-foreground">
210
+ <label htmlFor={`${groupId}-day`}>On day</label>
211
+ <input
212
+ id={`${groupId}-day`}
213
+ type="number"
214
+ min={1}
215
+ max={31}
216
+ value={value.day}
217
+ onChange={(e) =>
218
+ onChange({
219
+ ...value,
220
+ day: clampInt(e.target.value, 1, 31, value.day),
221
+ })
222
+ }
223
+ disabled={disabled}
224
+ className={numberInputClasses}
225
+ />
226
+ <span className="text-muted-foreground">of every month</span>
227
+ </div>
228
+ {value.day >= 29 && (
229
+ <p className="text-[0.65rem] text-muted-foreground">
230
+ Months without day {value.day} are skipped — February never
231
+ fires a day-{value.day} schedule
232
+ {value.day === 29 ? " except in leap years" : ""}.
233
+ </p>
234
+ )}
235
+ <TimeInput
236
+ id={`${groupId}-time`}
237
+ hour={value.hour}
238
+ minute={value.minute}
239
+ disabled={disabled}
240
+ onChange={(hour, minute) => onChange({ ...value, hour, minute })}
241
+ />
242
+ </div>
243
+ )}
244
+
245
+ {value.kind === "custom" && (
246
+ <div className="space-y-1">
247
+ <input
248
+ type="text"
249
+ aria-label="Cron expression"
250
+ placeholder="e.g. 0 9 * * MON-FRI"
251
+ value={value.cron}
252
+ onChange={(e) => onChange({ kind: "custom", cron: e.target.value })}
253
+ disabled={disabled}
254
+ className={cn(
255
+ "w-full rounded-md border border-input bg-background px-2.5 py-1.5 font-mono text-xs text-foreground",
256
+ "placeholder:font-sans placeholder:text-muted-foreground",
257
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
258
+ "disabled:pointer-events-none disabled:opacity-50",
259
+ cronError && "border-destructive",
260
+ )}
261
+ />
262
+ {cronError ? (
263
+ <p className="text-[0.65rem] text-destructive" role="alert">
264
+ {cronError}
265
+ </p>
266
+ ) : (
267
+ <p className="text-[0.65rem] text-muted-foreground">
268
+ 5 fields (minute hour day-of-month month day-of-week) or
269
+ @hourly, @daily, @weekly, @monthly, @yearly.
270
+ </p>
271
+ )}
272
+ </div>
273
+ )}
274
+
275
+ {/* Plain-English summary of the chosen cadence */}
276
+ {!(value.kind === "custom" && (cronError || value.cron.trim() === "")) && (
277
+ <p className="text-xs text-muted-foreground" data-testid="cadence-summary">
278
+ {describeCadence(value, timeZone)}
279
+ </p>
280
+ )}
281
+ </div>
282
+ );
283
+ }
284
+
285
+ // ---------------------------------------------------------------------------
286
+ // Private helpers
287
+ // ---------------------------------------------------------------------------
288
+
289
+ const numberInputClasses = cn(
290
+ "w-16 rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground",
291
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
292
+ "disabled:pointer-events-none disabled:opacity-50",
293
+ );
294
+
295
+ function TimeInput({
296
+ id,
297
+ hour,
298
+ minute,
299
+ disabled,
300
+ onChange,
301
+ }: {
302
+ readonly id: string;
303
+ readonly hour: number;
304
+ readonly minute: number;
305
+ readonly disabled?: boolean;
306
+ readonly onChange: (hour: number, minute: number) => void;
307
+ }) {
308
+ return (
309
+ <div className="flex items-center gap-2 text-xs text-foreground">
310
+ <label htmlFor={id}>At</label>
311
+ <input
312
+ id={id}
313
+ type="time"
314
+ value={formatTime(hour, minute)}
315
+ onChange={(e) => {
316
+ const [h, m] = e.target.value.split(":").map(Number);
317
+ if (Number.isInteger(h) && Number.isInteger(m)) onChange(h, m);
318
+ }}
319
+ disabled={disabled}
320
+ className={cn(
321
+ "rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground",
322
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
323
+ "disabled:pointer-events-none disabled:opacity-50",
324
+ )}
325
+ />
326
+ </div>
327
+ );
328
+ }
329
+
330
+ /**
331
+ * Next preset when the user switches kind, carrying the time of day
332
+ * across so "Daily at 09:30" → Weekly keeps 09:30. Switching to Custom
333
+ * prefills the generated cron from the outgoing preset — the escape
334
+ * hatch starts from what the user already built, not a blank box.
335
+ */
336
+ function presetForKind(kind: CadenceKind, previous: CadencePreset): CadencePreset {
337
+ const hour = "hour" in previous ? previous.hour : 9;
338
+ const minute = "minute" in previous ? previous.minute : 0;
339
+ switch (kind) {
340
+ case "hourly":
341
+ return { kind, minute };
342
+ case "daily":
343
+ return { kind, hour, minute };
344
+ case "weekly":
345
+ return { kind, days: [1], hour, minute };
346
+ case "monthly":
347
+ return { kind, day: 1, hour, minute };
348
+ case "custom":
349
+ return {
350
+ kind,
351
+ cron: previous.kind === "custom" ? previous.cron : cadenceToCron(previous),
352
+ };
353
+ }
354
+ }
355
+
356
+ function clampInt(
357
+ raw: string,
358
+ min: number,
359
+ max: number,
360
+ fallback: number,
361
+ ): number {
362
+ const n = Number(raw);
363
+ if (!Number.isInteger(n)) return fallback;
364
+ return Math.min(max, Math.max(min, n));
365
+ }