+ {/* Preset */}
+
+
+
+
- {preset === "custom" ? (
+ {preset === "every_minute" && (
+
+ No options — runs every minute, around the clock.
+
+ )}
+
+ {preset === "every_n_minutes" && (
+
+
+
+
+ update({ n: clamp(+e.target.value || 1, 1, 59) })}
+ />
+ minutes
+
+
+ {[1, 5, 10, 15, 20, 30].map((v) => (
+ update({ n: v })}
+ >
+ {v}
+
+ ))}
+
+
+
+
+ )}
+
+ {preset === "hourly" && (
+
{
- setCustomCron(e.target.value);
- emitChange("custom", hour, minute, dayOfWeek, dayOfMonth, e.target.value);
- }}
+ type="number"
+ min={0}
+ max={59}
+ className="w-24 font-mono"
+ value={state.minutePast}
+ onChange={(e) => update({ minutePast: clamp(+e.target.value || 0, 0, 59) })}
+ />
+
+ Runs once an hour at :{pad(state.minutePast)}
+
+
+ )}
+
+ {preset === "every_n_hours" && (
+
+
+
+
+ update({ n: clamp(+e.target.value || 1, 1, 23) })}
+ />
+ hours
+
+
+ {[1, 2, 3, 4, 6, 8, 12].map((v) => (
+ update({ n: v })}
+ >
+ {v}
+
+ ))}
+
+
+
+
+ update({ minutePast: clamp(+e.target.value || 0, 0, 59) })}
+ />
+
+
+
+ )}
+
+ {preset === "daily" && (
+
+
+
update({ times })} />
+ {state.times.length > 1 && (
+
+ All times in one schedule share the same minute. Changing one minute updates them all.
+
+ )}
+
+ )}
+
+ {preset === "weekdays" && (
+
+
+
+
update({ days })} />
+
+ {(
+ [
+ ["Weekdays", [1, 2, 3, 4, 5]],
+ ["Weekends", [0, 6]],
+ ["All days", [0, 1, 2, 3, 4, 5, 6]],
+ ["Mon · Wed · Fri", [1, 3, 5]],
+ ["Tue · Thu", [2, 4]],
+ ] as const
+ ).map(([label, days]) => (
+ update({ days: [...days] })}
+ >
+ {label}
+
+ ))}
+
+
+
+
+
update({ times })} />
+ {state.times.length > 1 && (
+
+ All times in one schedule share the same minute. Changing one minute updates them all.
+
+ )}
+
+
+ )}
+
+ {preset === "monthly" && (
+
+
+
+
update({ domDays })} />
+
+ {(
+ [
+ ["1st only", [1]],
+ ["15th only", [15]],
+ ["1st & 15th", [1, 15]],
+ ["Last day (28th)", [28]],
+ ] as const
+ ).map(([label, days]) => (
+ update({ domDays: [...days] })}
+ >
+ {label}
+
+ ))}
+
+
+ Days 29–31 are skipped in months that don't have them.
+
+
+
+
+
update({ times })} />
+ {state.times.length > 1 && (
+
+ All times in one schedule share the same minute. Changing one minute updates them all.
+
+ )}
+
+
+ )}
+
+ {preset === "custom" && (
+
+
+ update({ custom: e.target.value })}
placeholder="0 10 * * *"
className="font-mono text-sm"
/>
@@ -222,123 +891,150 @@ export function ScheduleEditor({
Five fields: minute hour day-of-month month day-of-week
- ) : (
-
- {preset !== "every_minute" && preset !== "every_hour" && (
- <>
-
at
-
-
:
-
- >
- )}
-
- {preset === "every_hour" && (
- <>
-
at minute
-
- >
- )}
-
- {preset === "weekly" && (
- <>
-
on
-
- {DAYS_OF_WEEK.map((d) => (
-
- ))}
-
- >
- )}
-
- {preset === "monthly" && (
- <>
-
on day
-
- >
- )}
-
)}
+
+
+
+
+
+ Summary —
+ {describeSchedule(buildCronFromState(state))}
+
+
+ {buildCronFromState(state)}
+
+
);
}
+
+function WindowAndWeekdaysToggles({
+ state,
+ update,
+}: {
+ state: EditorState;
+ update: (patch: Partial