mirror of
https://github.com/priyanshujain/margin-calendar.git
synced 2026-10-02 11:07:04 +00:00
fix bug on hour fold update
This commit is contained in:
1 parent
f7d3505ab2
commit
af8e66e84d
13 files changed
+243
-65
No files matched your search
@@ -39,15 +39,13 @@ export const GridDay = memo(function GridDay({
|
||||
{packed.map((p) => {
|
||||
const { startMin, endMin } = dayMinutes(p.event, dayStart);
|
||||
const top = timeToY(layout, startMin);
|
||||
const raw = timeToY(layout, endMin) - top;
|
||||
// Both ends inside the same strip: it is hiding in there, and the strip carries the count.
|
||||
if (raw < 1) return null;
|
||||
const height = Math.max(timeToY(layout, endMin) - top, MIN_BLOCK_H);
|
||||
return (
|
||||
<GridEvent
|
||||
key={p.event.id}
|
||||
item={p.event}
|
||||
top={top}
|
||||
height={Math.max(raw, MIN_BLOCK_H)}
|
||||
height={height}
|
||||
left={p.left}
|
||||
width={p.width}
|
||||
selected={sameKey(selected, keyOf(p.event.instance))}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Instance } from "../ipc";
|
||||
import { bandAt, busyHours, type Placed } from "./GridModel";
|
||||
|
||||
const at = (hour: number, minute = 0) => new Date(2026, 0, 5, hour, minute).getTime();
|
||||
|
||||
/** Only the times matter to the hour scan, so the instance behind them is left blank. */
|
||||
const placed = (from: [number, number], to: [number, number]): Placed => ({
|
||||
instance: {} as Instance,
|
||||
id: `${from[0]}:${from[1]}`,
|
||||
startMs: at(...from),
|
||||
endMs: at(...to),
|
||||
});
|
||||
|
||||
describe("busyHours", () => {
|
||||
it("marks every whole hour an event touches", () => {
|
||||
const busy = busyHours([placed([14, 30], [16, 0]), placed([16, 0], [16, 15])]);
|
||||
const hours = busy.flatMap((b, h) => (b ? [h] : []));
|
||||
expect(hours).toEqual([14, 15, 16]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bandAt", () => {
|
||||
const items = [placed([9, 0], [9, 30]), placed([18, 0], [18, 30])];
|
||||
const bounds = { start: 7, end: 20 };
|
||||
|
||||
it("is the whole empty run around the hour, stopped by the events and the bounds", () => {
|
||||
expect(bandAt(items, bounds, 12)).toEqual({ start: 10, end: 18 });
|
||||
expect(bandAt(items, bounds, 19)).toEqual({ start: 19, end: 20 });
|
||||
});
|
||||
|
||||
it("offers nothing on a busy hour, since a fold there would come straight back", () => {
|
||||
expect(bandAt(items, bounds, 9)).toBeNull();
|
||||
});
|
||||
});
|
||||
Binary file not shown.
@@ -9,14 +9,13 @@ import { create } from "zustand";
|
||||
import {
|
||||
DEFAULT_BOUNDS,
|
||||
addFold,
|
||||
foldAt,
|
||||
normalizeFolds,
|
||||
unfoldStrip,
|
||||
type Bounds,
|
||||
type FitLayout,
|
||||
type Fold,
|
||||
} from "../grid/fit";
|
||||
import { loadFolds, saveFolds } from "../grid/folds";
|
||||
import { bandAt, type Placed } from "./GridModel";
|
||||
import { bandAt, busyHours, type Placed } from "./GridModel";
|
||||
|
||||
export type DragMode = "create" | "move" | "resize-start" | "resize-end";
|
||||
|
||||
@@ -82,9 +81,7 @@ export const useGrid = create<GridState>((set, get) => ({
|
||||
set({ folds });
|
||||
},
|
||||
unfold: (range) => {
|
||||
const folds = saveFolds(
|
||||
normalizeFolds(get().folds).filter((f) => f.end <= range.start || f.start >= range.end),
|
||||
);
|
||||
const folds = saveFolds(unfoldStrip(get().folds, busyHours(get().items), range));
|
||||
// An out-of-bounds band has no fold to remove, so widening the floor is what holds it open.
|
||||
const base = get().floor ?? get().layout?.bounds ?? DEFAULT_BOUNDS;
|
||||
set({
|
||||
@@ -93,7 +90,7 @@ export const useGrid = create<GridState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
toggleFold: (hour) => {
|
||||
const { layout, hoverHour, items, folds } = get();
|
||||
const { layout, hoverHour, items } = get();
|
||||
if (!layout) return;
|
||||
const at = hour ?? hoverHour;
|
||||
if (at === null || at === undefined) return;
|
||||
@@ -103,11 +100,8 @@ export const useGrid = create<GridState>((set, get) => ({
|
||||
get().unfold({ start: segment.start / 60, end: segment.end / 60 });
|
||||
return;
|
||||
}
|
||||
if (foldAt(folds, h)) {
|
||||
get().unfold({ start: h, end: h + 1 });
|
||||
return;
|
||||
}
|
||||
get().fold(bandAt(items, layout.bounds, h));
|
||||
const band = bandAt(items, layout.bounds, h);
|
||||
if (band) get().fold(band);
|
||||
},
|
||||
unfoldAll: () => {
|
||||
saveFolds([]);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// A folded band. The empty hours outside the bounds fold themselves; an interior band only folds
|
||||
// because you asked for it, with `z` or with the chip that hangs off an empty run. Either way the
|
||||
// strip says what it covers, how much is hiding in there, and which way it goes: the whole band is
|
||||
// the button, and the mark next to its range says Expand.
|
||||
// strip says what it covers and which way it goes: the whole band is the button, and the mark next
|
||||
// to its range says Expand. It never has anything to count: an hour with an event in it is not
|
||||
// folded, whatever fold it sits inside.
|
||||
|
||||
import { memo } from "react";
|
||||
import type { Segment } from "../grid/fit";
|
||||
@@ -11,11 +12,10 @@ import { Icon } from "./Icon";
|
||||
|
||||
interface GridStripProps {
|
||||
segment: Segment;
|
||||
hidden: number;
|
||||
onUnfold: (range: { start: number; end: number }) => void;
|
||||
}
|
||||
|
||||
export const GridStrip = memo(function GridStrip({ segment, hidden, onUnfold }: GridStripProps) {
|
||||
export const GridStrip = memo(function GridStrip({ segment, onUnfold }: GridStripProps) {
|
||||
const range = { start: segment.start / 60, end: segment.end / 60 };
|
||||
const label = `Expand ${rangeLabel(range)}`;
|
||||
return (
|
||||
@@ -34,7 +34,6 @@ export const GridStrip = memo(function GridStrip({ segment, hidden, onUnfold }:
|
||||
<span className="grid-fold-verb">Expand</span>
|
||||
</span>
|
||||
<span className="grid-strip-rule" />
|
||||
{hidden > 0 && <span className="grid-strip-count">{hidden}</span>}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
computeBounds,
|
||||
computeFit,
|
||||
timeToY,
|
||||
trimFolds,
|
||||
yToTime,
|
||||
type Bounds,
|
||||
type Fold,
|
||||
@@ -51,6 +52,7 @@ import { GridGaps } from "./GridGaps";
|
||||
import { GridGhost } from "./GridGhost";
|
||||
import {
|
||||
MIN_EVENT_MINUTES,
|
||||
busyHours,
|
||||
clampMinutes,
|
||||
dayMinutes,
|
||||
hourLabel,
|
||||
@@ -264,7 +266,10 @@ export function GridView({ defaultCalendarId }: GridViewProps) {
|
||||
// What the events and the user asked for is what the hysteresis remembers. The clock's hour is
|
||||
// a pin laid over the top of it every hour, so it never accumulates into the axis it pins.
|
||||
previous.current = bounds;
|
||||
if (!nowBand) return computeFit({ bounds, folds, viewportHeight: viewportH });
|
||||
// A fold hides empty hours. Any hour an event on screen covers is given back at full scale,
|
||||
// however it got there: typed into the palette, moved in the editor, or synced in from Google.
|
||||
const shown = trimFolds(folds, busyHours(timed));
|
||||
if (!nowBand) return computeFit({ bounds, folds: shown, viewportHeight: viewportH });
|
||||
|
||||
// Everything the widening reached over folds behind it. At half eleven at night the axis is
|
||||
// one row longer and one strip taller, rather than four rows of empty evening shorter.
|
||||
@@ -276,7 +281,7 @@ export function GridView({ defaultCalendarId }: GridViewProps) {
|
||||
start: Math.min(bounds.start, nowBand.start),
|
||||
end: Math.max(bounds.end, nowBand.end),
|
||||
};
|
||||
return computeFit({ bounds: axis, folds: [...folds, ...reach], viewportHeight: viewportH });
|
||||
return computeFit({ bounds: axis, folds: [...shown, ...reach], viewportHeight: viewportH });
|
||||
}, [timed, folds, floor, viewportH, nowBand]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -320,16 +325,6 @@ export function GridView({ defaultCalendarId }: GridViewProps) {
|
||||
return out;
|
||||
}, [layout]);
|
||||
|
||||
/** What each strip is hiding: events with nowhere to show, not events it merely clips. */
|
||||
const hiddenIn = useCallback(
|
||||
(start: number, end: number) =>
|
||||
timed.filter((item) => {
|
||||
const { startMin, endMin } = dayMinutes(item, startOfDay(item.startMs));
|
||||
return startMin >= start && endMin <= end;
|
||||
}).length,
|
||||
[timed],
|
||||
);
|
||||
|
||||
const unfold = useGrid((s) => s.unfold);
|
||||
const onUnfold = useCallback((range: Fold) => unfold(range), [unfold]);
|
||||
|
||||
@@ -781,12 +776,7 @@ export function GridView({ defaultCalendarId }: GridViewProps) {
|
||||
|
||||
{layout.segments.map((segment) =>
|
||||
segment.kind === "strip" ? (
|
||||
<GridStrip
|
||||
key={`s${segment.start}`}
|
||||
segment={segment}
|
||||
hidden={hiddenIn(segment.start, segment.end)}
|
||||
onUnfold={onUnfold}
|
||||
/>
|
||||
<GridStrip key={`s${segment.start}`} segment={segment} onUnfold={onUnfold} />
|
||||
) : null,
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user