def grid_dimensions(N: int, nrows: int, ncols: int) -> tuple[int, int]:
    """
    Returns image grid dimensions (rows and columns) based
    on the total number of items ``N``.

    Parameters
    ----------
    N : int
        Total number of items
    nrows : int
        Number or rows, -1 means determined by the number of items
    ncols : int
        Number of columns, -1 means determined by the number of items

    Returns
    -------
    tuple[int, int]
        Number of rows and columns for a grid that fits all items
    """

    if nrows == -1 and ncols == -1:

        # start with a square grid
        nrows = ncols = int(N**0.5)

        # increase the number of cols until all items fit in the grid
        while nrows * ncols < N:
            ncols += 1

        return nrows, ncols

    if nrows == -1:

        if N % ncols == 0:
            nrows = N // ncols
        else:
            nrows = N // ncols + 1

    elif ncols == -1:

        if N % nrows == 0:
            ncols = N // nrows
        else:
            ncols = N // nrows + 1

    else:

        if nrows * ncols < N:

            raise ValueError(
                f'{N} items cannot be placed in a {nrows} × {ncols} grid')

    return nrows, ncols