calendar-ui-layout 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jiku0730
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # calendar-ui-layout
2
+
3
+ **Headless time-grid layout engine.** Give it a day's worth of timed events; get
4
+ back where each one goes — column packing, overlap resolution, and expansion —
5
+ as plain numbers. No UI, no framework, no DOM, zero runtime dependencies.
6
+
7
+ This is the hard part every "headless" calendar makes you write yourself. The
8
+ competition ([`@verbpatch/*`](https://www.verbpatch.com/calendar),
9
+ react-headless-agenda) hands you time slots and then, in their own docs, tells
10
+ you to *"calculate the width and horizontal position of events that occur at the
11
+ same time"* on your own. This package **is** that calculation.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install calendar-ui-layout
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import { layoutDay } from "calendar-ui-layout";
23
+
24
+ const positioned = layoutDay(
25
+ [
26
+ { id: "a", start: 9 * 60, end: 10 * 60 + 30 }, // minutes from midnight
27
+ { id: "b", start: 9 * 60, end: 9 * 60 + 30 },
28
+ { id: "c", start: 9 * 60 + 30, end: 10 * 60 },
29
+ ],
30
+ { dayStart: 8 * 60, dayEnd: 18 * 60 },
31
+ );
32
+
33
+ for (const p of positioned) {
34
+ // p.data → your original event, untouched
35
+ // p.top / p.height → vertical geometry, fractions 0..1 of the window
36
+ // p.left / p.width → horizontal geometry, fractions 0..1
37
+ // p.columnIndex / columnCount → integer column math, if you'd rather do px yourself
38
+ // p.span → how many columns wide after expansion
39
+ }
40
+ ```
41
+
42
+ Render however you like — the numbers are the whole point:
43
+
44
+ ```tsx
45
+ <div style={{ position: "relative" }}>
46
+ {positioned.map((p) => (
47
+ <div
48
+ key={p.data.id}
49
+ style={{
50
+ position: "absolute",
51
+ top: `${p.top * 100}%`,
52
+ height: `${p.height * 100}%`,
53
+ left: `${p.left * 100}%`,
54
+ width: `${p.width * 100}%`,
55
+ }}
56
+ >
57
+ {String(p.data.id)}
58
+ </div>
59
+ ))}
60
+ </div>
61
+ ```
62
+
63
+ ## Why numeric, not `Date`?
64
+
65
+ `start`/`end` are numbers in one consistent unit (minutes from midnight is the
66
+ natural choice). Turning a `Date` — with its timezone and DST rules — into a
67
+ minute offset is a separate, thorny concern; keeping it out of the layout core
68
+ makes this engine **pure and deterministic**: same input, same output, fully
69
+ unit-testable, no clock involved. Timezone/DST/recurrence handling lives in
70
+ higher layers (and leans on battle-tested libraries), not here.
71
+
72
+ ## API
73
+
74
+ ### `layoutDay(events, options?) → PositionedInterval[]`
75
+
76
+ - `events`: `{ id, start, end, ...yourFields }[]` — `end` is exclusive.
77
+ - `options`:
78
+ - `dayStart` (default `0`), `dayEnd` (default `1440`) — the visible window; events are clamped to it and fully-outside events are dropped.
79
+ - `minDuration` (default `0`) — floor height so zero-length events stay clickable.
80
+ - `expand` (default `true`) — widen events into free right-hand columns (the "wide when alone" look). Set `false` to keep every event one column wide.
81
+
82
+ Guarantees (enforced by the test suite):
83
+
84
+ - Events overlapping in time never overlap in space.
85
+ - Touching events (`end === start`) do **not** count as overlapping.
86
+ - `left + width ≤ 1` and `top + height ≤ 1` always.
87
+ - Deterministic and order-independent: shuffling the input yields the same layout.
88
+
89
+ ## Develop
90
+
91
+ ```sh
92
+ npm test # 18 tests: clustering, packing, expansion, edge cases, invariants
93
+ npm run typecheck # strict, noUncheckedIndexedAccess
94
+ npm run demo # ASCII render of a sample day — see the overlap resolution
95
+ ```
96
+
97
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ // src/layout.ts
4
+ function overlaps(aStart, aEnd, bStart, bEnd) {
5
+ return aStart < bEnd && bStart < aEnd;
6
+ }
7
+ function layoutDay(events, options = {}) {
8
+ const dayStart = options.dayStart ?? 0;
9
+ const dayEnd = options.dayEnd ?? 1440;
10
+ const expand = options.expand ?? true;
11
+ const minDuration = options.minDuration ?? 0;
12
+ const range = dayEnd - dayStart;
13
+ if (range <= 0) return [];
14
+ const norm = [];
15
+ for (const e of events) {
16
+ let start = Math.max(e.start, dayStart);
17
+ let end = Math.min(e.end, dayEnd);
18
+ if (minDuration > 0 && end - start < minDuration) {
19
+ end = Math.min(start + minDuration, dayEnd);
20
+ start = Math.max(dayStart, end - minDuration);
21
+ }
22
+ if (end <= start) continue;
23
+ norm.push({ data: e, start, end });
24
+ }
25
+ if (norm.length === 0) return [];
26
+ norm.sort(
27
+ (a, b) => a.start - b.start || b.end - a.end || String(a.data.id).localeCompare(String(b.data.id))
28
+ );
29
+ const result = [];
30
+ let i = 0;
31
+ while (i < norm.length) {
32
+ let clusterEnd = norm[i].end;
33
+ let j = i + 1;
34
+ while (j < norm.length && norm[j].start < clusterEnd) {
35
+ clusterEnd = Math.max(clusterEnd, norm[j].end);
36
+ j++;
37
+ }
38
+ layoutCluster(norm.slice(i, j), { dayStart, range, expand }, result);
39
+ i = j;
40
+ }
41
+ return result;
42
+ }
43
+ function layoutCluster(cluster, ctx, out) {
44
+ const columns = [];
45
+ const columnOf = /* @__PURE__ */ new Map();
46
+ for (const ev of cluster) {
47
+ let placed = false;
48
+ for (let c = 0; c < columns.length; c++) {
49
+ const col = columns[c];
50
+ const last = col[col.length - 1];
51
+ if (last.end <= ev.start) {
52
+ col.push(ev);
53
+ columnOf.set(ev, c);
54
+ placed = true;
55
+ break;
56
+ }
57
+ }
58
+ if (!placed) {
59
+ columnOf.set(ev, columns.length);
60
+ columns.push([ev]);
61
+ }
62
+ }
63
+ const columnCount = columns.length;
64
+ for (const ev of cluster) {
65
+ const columnIndex = columnOf.get(ev);
66
+ const span = ctx.expand ? spanFor(ev, columnIndex, columnCount, columns) : 1;
67
+ out.push({
68
+ data: ev.data,
69
+ columnIndex,
70
+ columnCount,
71
+ span,
72
+ top: (ev.start - ctx.dayStart) / ctx.range,
73
+ height: (ev.end - ev.start) / ctx.range,
74
+ left: columnIndex / columnCount,
75
+ width: span / columnCount
76
+ });
77
+ }
78
+ }
79
+ function spanFor(ev, columnIndex, columnCount, columns) {
80
+ let span = 1;
81
+ for (let c = columnIndex + 1; c < columnCount; c++) {
82
+ const col = columns[c];
83
+ const blocked = col.some((o) => overlaps(ev.start, ev.end, o.start, o.end));
84
+ if (blocked) break;
85
+ span++;
86
+ }
87
+ return span;
88
+ }
89
+
90
+ exports.layoutDay = layoutDay;
91
+ //# sourceMappingURL=index.cjs.map
92
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/layout.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Interval, LayoutOptions, PositionedInterval } from \"./types.js\";\n\n/** Internal, normalized (clamped) representation of an interval. */\ninterface Normalized<T extends Interval> {\n data: T;\n start: number;\n end: number;\n}\n\n/** Two half-open intervals overlap iff each starts before the other ends. */\nfunction overlaps(aStart: number, aEnd: number, bStart: number, bEnd: number): boolean {\n return aStart < bEnd && bStart < aEnd;\n}\n\n/**\n * Lay out timed events for a single day/column axis, resolving overlaps into\n * side-by-side columns.\n *\n * Algorithm (the same shape Google Calendar uses):\n * 1. **Normalize** — clamp every event to the day window, drop anything that\n * falls outside it, optionally enforce a minimum duration.\n * 2. **Sort** — by start ascending, then by end descending (longer events\n * first), then by id for a stable, deterministic order.\n * 3. **Cluster** — sweep the sorted list into maximal runs of transitively\n * overlapping events. Columns are counted per cluster, so a busy morning\n * never shrinks a quiet afternoon.\n * 4. **Pack** — within a cluster, greedily drop each event into the first\n * column whose previous event has already ended; open a new column only\n * when none is free.\n * 5. **Expand** — optionally widen each event rightward across columns that are\n * free for its entire duration.\n *\n * The engine is pure and deterministic: same input → same output, no clock, no\n * DOM, no framework.\n */\nexport function layoutDay<T extends Interval>(\n events: readonly T[],\n options: LayoutOptions = {},\n): PositionedInterval<T>[] {\n const dayStart = options.dayStart ?? 0;\n const dayEnd = options.dayEnd ?? 1440;\n const expand = options.expand ?? true;\n const minDuration = options.minDuration ?? 0;\n\n const range = dayEnd - dayStart;\n if (range <= 0) return [];\n\n // 1. Normalize: clamp to window, apply minDuration, drop out-of-window.\n const norm: Normalized<T>[] = [];\n for (const e of events) {\n let start = Math.max(e.start, dayStart);\n let end = Math.min(e.end, dayEnd);\n if (minDuration > 0 && end - start < minDuration) {\n end = Math.min(start + minDuration, dayEnd);\n // If clamping to the window pushed us against the wall, pull start back\n // so a min-duration event near dayEnd still renders its full min height.\n start = Math.max(dayStart, end - minDuration);\n }\n if (end <= start) continue;\n norm.push({ data: e, start, end });\n }\n if (norm.length === 0) return [];\n\n // 2. Sort: start asc, end desc, id asc.\n norm.sort(\n (a, b) =>\n a.start - b.start ||\n b.end - a.end ||\n String(a.data.id).localeCompare(String(b.data.id)),\n );\n\n const result: PositionedInterval<T>[] = [];\n\n // 3. Cluster + 4. Pack + 5. Expand, one maximal overlap-run at a time.\n let i = 0;\n while (i < norm.length) {\n let clusterEnd = norm[i]!.end;\n let j = i + 1;\n while (j < norm.length && norm[j]!.start < clusterEnd) {\n clusterEnd = Math.max(clusterEnd, norm[j]!.end);\n j++;\n }\n layoutCluster(norm.slice(i, j), { dayStart, range, expand }, result);\n i = j;\n }\n\n return result;\n}\n\ninterface ClusterContext {\n dayStart: number;\n range: number;\n expand: boolean;\n}\n\nfunction layoutCluster<T extends Interval>(\n cluster: readonly Normalized<T>[],\n ctx: ClusterContext,\n out: PositionedInterval<T>[],\n): void {\n // 4. Greedy column packing. `columns[c]` holds the events placed in column c,\n // in time order; the last one determines whether the column is free.\n const columns: Normalized<T>[][] = [];\n const columnOf = new Map<Normalized<T>, number>();\n\n for (const ev of cluster) {\n let placed = false;\n for (let c = 0; c < columns.length; c++) {\n const col = columns[c]!;\n const last = col[col.length - 1]!;\n if (last.end <= ev.start) {\n col.push(ev);\n columnOf.set(ev, c);\n placed = true;\n break;\n }\n }\n if (!placed) {\n columnOf.set(ev, columns.length);\n columns.push([ev]);\n }\n }\n\n const columnCount = columns.length;\n\n // 5. Expansion + geometry.\n for (const ev of cluster) {\n const columnIndex = columnOf.get(ev)!;\n const span = ctx.expand\n ? spanFor(ev, columnIndex, columnCount, columns)\n : 1;\n\n out.push({\n data: ev.data,\n columnIndex,\n columnCount,\n span,\n top: (ev.start - ctx.dayStart) / ctx.range,\n height: (ev.end - ev.start) / ctx.range,\n left: columnIndex / columnCount,\n width: span / columnCount,\n });\n }\n}\n\n/**\n * How many columns `ev` can occupy: start at its own column and extend right\n * while every event in the next column clears its time span.\n */\nfunction spanFor<T extends Interval>(\n ev: Normalized<T>,\n columnIndex: number,\n columnCount: number,\n columns: readonly Normalized<T>[][],\n): number {\n let span = 1;\n for (let c = columnIndex + 1; c < columnCount; c++) {\n const col = columns[c]!;\n const blocked = col.some((o) => overlaps(ev.start, ev.end, o.start, o.end));\n if (blocked) break;\n span++;\n }\n return span;\n}\n"]}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A time interval to be laid out. `start`/`end` are numbers in a single
3
+ * consistent unit — typically **minutes from midnight** — where `end` is
4
+ * exclusive. Keeping the core purely numeric makes it deterministic and
5
+ * timezone/DST-agnostic: converting a `Date` (with its zone/DST rules) into a
6
+ * minute offset is the caller's concern, not the layout engine's.
7
+ *
8
+ * Extra fields are preserved untouched on the way through, so callers can carry
9
+ * their own event payload (title, color, resourceId, …).
10
+ */
11
+ interface Interval {
12
+ id: string | number;
13
+ /** Inclusive start, in minutes (or any consistent unit). */
14
+ start: number;
15
+ /** Exclusive end, in the same unit as `start`. */
16
+ end: number;
17
+ [key: string]: unknown;
18
+ }
19
+ interface LayoutOptions {
20
+ /** Start of the visible day window, same unit as intervals. Default 0. */
21
+ dayStart?: number;
22
+ /** End of the visible day window (exclusive), same unit. Default 1440 (24h). */
23
+ dayEnd?: number;
24
+ /**
25
+ * Minimum duration used for layout so zero/near-zero-length events still get
26
+ * a clickable height. Applied after clamping to the window. Default 0 (off).
27
+ */
28
+ minDuration?: number;
29
+ /**
30
+ * When true (default), an event widens to fill columns to its right that are
31
+ * free for its whole duration — the "wide when alone" look of Google Calendar.
32
+ * When false, every event keeps a single-column width (`span` is always 1).
33
+ */
34
+ expand?: boolean;
35
+ }
36
+ /**
37
+ * The result of laying out one interval.
38
+ *
39
+ * Two coordinate systems are provided so any renderer can consume it:
40
+ * - **Structural** (`columnIndex`, `columnCount`, `span`): integer column math,
41
+ * for callers that want to compute their own pixel geometry.
42
+ * - **Fractional** (`top`, `height`, `left`, `width`): all in the range 0..1
43
+ * relative to the day window, ready to drop into `style` as percentages.
44
+ *
45
+ * `left + width` never exceeds 1, and `top + height` never exceeds 1.
46
+ */
47
+ interface PositionedInterval<T extends Interval = Interval> {
48
+ /** The original interval, unmodified. */
49
+ data: T;
50
+ /** Zero-based column this event occupies within its overlap cluster. */
51
+ columnIndex: number;
52
+ /** Total number of columns in this event's overlap cluster (>= 1). */
53
+ columnCount: number;
54
+ /** How many columns wide the event is after expansion (>= 1). */
55
+ span: number;
56
+ /** Fraction (0..1) from the top of the day window to the event's start. */
57
+ top: number;
58
+ /** Fraction (0..1) of the day window covered by the event's duration. */
59
+ height: number;
60
+ /** Fraction (0..1) from the left edge to the event's column. */
61
+ left: number;
62
+ /** Fraction (0..1) of the width the event occupies. */
63
+ width: number;
64
+ }
65
+
66
+ /**
67
+ * Lay out timed events for a single day/column axis, resolving overlaps into
68
+ * side-by-side columns.
69
+ *
70
+ * Algorithm (the same shape Google Calendar uses):
71
+ * 1. **Normalize** — clamp every event to the day window, drop anything that
72
+ * falls outside it, optionally enforce a minimum duration.
73
+ * 2. **Sort** — by start ascending, then by end descending (longer events
74
+ * first), then by id for a stable, deterministic order.
75
+ * 3. **Cluster** — sweep the sorted list into maximal runs of transitively
76
+ * overlapping events. Columns are counted per cluster, so a busy morning
77
+ * never shrinks a quiet afternoon.
78
+ * 4. **Pack** — within a cluster, greedily drop each event into the first
79
+ * column whose previous event has already ended; open a new column only
80
+ * when none is free.
81
+ * 5. **Expand** — optionally widen each event rightward across columns that are
82
+ * free for its entire duration.
83
+ *
84
+ * The engine is pure and deterministic: same input → same output, no clock, no
85
+ * DOM, no framework.
86
+ */
87
+ declare function layoutDay<T extends Interval>(events: readonly T[], options?: LayoutOptions): PositionedInterval<T>[];
88
+
89
+ export { type Interval, type LayoutOptions, type PositionedInterval, layoutDay };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A time interval to be laid out. `start`/`end` are numbers in a single
3
+ * consistent unit — typically **minutes from midnight** — where `end` is
4
+ * exclusive. Keeping the core purely numeric makes it deterministic and
5
+ * timezone/DST-agnostic: converting a `Date` (with its zone/DST rules) into a
6
+ * minute offset is the caller's concern, not the layout engine's.
7
+ *
8
+ * Extra fields are preserved untouched on the way through, so callers can carry
9
+ * their own event payload (title, color, resourceId, …).
10
+ */
11
+ interface Interval {
12
+ id: string | number;
13
+ /** Inclusive start, in minutes (or any consistent unit). */
14
+ start: number;
15
+ /** Exclusive end, in the same unit as `start`. */
16
+ end: number;
17
+ [key: string]: unknown;
18
+ }
19
+ interface LayoutOptions {
20
+ /** Start of the visible day window, same unit as intervals. Default 0. */
21
+ dayStart?: number;
22
+ /** End of the visible day window (exclusive), same unit. Default 1440 (24h). */
23
+ dayEnd?: number;
24
+ /**
25
+ * Minimum duration used for layout so zero/near-zero-length events still get
26
+ * a clickable height. Applied after clamping to the window. Default 0 (off).
27
+ */
28
+ minDuration?: number;
29
+ /**
30
+ * When true (default), an event widens to fill columns to its right that are
31
+ * free for its whole duration — the "wide when alone" look of Google Calendar.
32
+ * When false, every event keeps a single-column width (`span` is always 1).
33
+ */
34
+ expand?: boolean;
35
+ }
36
+ /**
37
+ * The result of laying out one interval.
38
+ *
39
+ * Two coordinate systems are provided so any renderer can consume it:
40
+ * - **Structural** (`columnIndex`, `columnCount`, `span`): integer column math,
41
+ * for callers that want to compute their own pixel geometry.
42
+ * - **Fractional** (`top`, `height`, `left`, `width`): all in the range 0..1
43
+ * relative to the day window, ready to drop into `style` as percentages.
44
+ *
45
+ * `left + width` never exceeds 1, and `top + height` never exceeds 1.
46
+ */
47
+ interface PositionedInterval<T extends Interval = Interval> {
48
+ /** The original interval, unmodified. */
49
+ data: T;
50
+ /** Zero-based column this event occupies within its overlap cluster. */
51
+ columnIndex: number;
52
+ /** Total number of columns in this event's overlap cluster (>= 1). */
53
+ columnCount: number;
54
+ /** How many columns wide the event is after expansion (>= 1). */
55
+ span: number;
56
+ /** Fraction (0..1) from the top of the day window to the event's start. */
57
+ top: number;
58
+ /** Fraction (0..1) of the day window covered by the event's duration. */
59
+ height: number;
60
+ /** Fraction (0..1) from the left edge to the event's column. */
61
+ left: number;
62
+ /** Fraction (0..1) of the width the event occupies. */
63
+ width: number;
64
+ }
65
+
66
+ /**
67
+ * Lay out timed events for a single day/column axis, resolving overlaps into
68
+ * side-by-side columns.
69
+ *
70
+ * Algorithm (the same shape Google Calendar uses):
71
+ * 1. **Normalize** — clamp every event to the day window, drop anything that
72
+ * falls outside it, optionally enforce a minimum duration.
73
+ * 2. **Sort** — by start ascending, then by end descending (longer events
74
+ * first), then by id for a stable, deterministic order.
75
+ * 3. **Cluster** — sweep the sorted list into maximal runs of transitively
76
+ * overlapping events. Columns are counted per cluster, so a busy morning
77
+ * never shrinks a quiet afternoon.
78
+ * 4. **Pack** — within a cluster, greedily drop each event into the first
79
+ * column whose previous event has already ended; open a new column only
80
+ * when none is free.
81
+ * 5. **Expand** — optionally widen each event rightward across columns that are
82
+ * free for its entire duration.
83
+ *
84
+ * The engine is pure and deterministic: same input → same output, no clock, no
85
+ * DOM, no framework.
86
+ */
87
+ declare function layoutDay<T extends Interval>(events: readonly T[], options?: LayoutOptions): PositionedInterval<T>[];
88
+
89
+ export { type Interval, type LayoutOptions, type PositionedInterval, layoutDay };
package/dist/index.js ADDED
@@ -0,0 +1,90 @@
1
+ // src/layout.ts
2
+ function overlaps(aStart, aEnd, bStart, bEnd) {
3
+ return aStart < bEnd && bStart < aEnd;
4
+ }
5
+ function layoutDay(events, options = {}) {
6
+ const dayStart = options.dayStart ?? 0;
7
+ const dayEnd = options.dayEnd ?? 1440;
8
+ const expand = options.expand ?? true;
9
+ const minDuration = options.minDuration ?? 0;
10
+ const range = dayEnd - dayStart;
11
+ if (range <= 0) return [];
12
+ const norm = [];
13
+ for (const e of events) {
14
+ let start = Math.max(e.start, dayStart);
15
+ let end = Math.min(e.end, dayEnd);
16
+ if (minDuration > 0 && end - start < minDuration) {
17
+ end = Math.min(start + minDuration, dayEnd);
18
+ start = Math.max(dayStart, end - minDuration);
19
+ }
20
+ if (end <= start) continue;
21
+ norm.push({ data: e, start, end });
22
+ }
23
+ if (norm.length === 0) return [];
24
+ norm.sort(
25
+ (a, b) => a.start - b.start || b.end - a.end || String(a.data.id).localeCompare(String(b.data.id))
26
+ );
27
+ const result = [];
28
+ let i = 0;
29
+ while (i < norm.length) {
30
+ let clusterEnd = norm[i].end;
31
+ let j = i + 1;
32
+ while (j < norm.length && norm[j].start < clusterEnd) {
33
+ clusterEnd = Math.max(clusterEnd, norm[j].end);
34
+ j++;
35
+ }
36
+ layoutCluster(norm.slice(i, j), { dayStart, range, expand }, result);
37
+ i = j;
38
+ }
39
+ return result;
40
+ }
41
+ function layoutCluster(cluster, ctx, out) {
42
+ const columns = [];
43
+ const columnOf = /* @__PURE__ */ new Map();
44
+ for (const ev of cluster) {
45
+ let placed = false;
46
+ for (let c = 0; c < columns.length; c++) {
47
+ const col = columns[c];
48
+ const last = col[col.length - 1];
49
+ if (last.end <= ev.start) {
50
+ col.push(ev);
51
+ columnOf.set(ev, c);
52
+ placed = true;
53
+ break;
54
+ }
55
+ }
56
+ if (!placed) {
57
+ columnOf.set(ev, columns.length);
58
+ columns.push([ev]);
59
+ }
60
+ }
61
+ const columnCount = columns.length;
62
+ for (const ev of cluster) {
63
+ const columnIndex = columnOf.get(ev);
64
+ const span = ctx.expand ? spanFor(ev, columnIndex, columnCount, columns) : 1;
65
+ out.push({
66
+ data: ev.data,
67
+ columnIndex,
68
+ columnCount,
69
+ span,
70
+ top: (ev.start - ctx.dayStart) / ctx.range,
71
+ height: (ev.end - ev.start) / ctx.range,
72
+ left: columnIndex / columnCount,
73
+ width: span / columnCount
74
+ });
75
+ }
76
+ }
77
+ function spanFor(ev, columnIndex, columnCount, columns) {
78
+ let span = 1;
79
+ for (let c = columnIndex + 1; c < columnCount; c++) {
80
+ const col = columns[c];
81
+ const blocked = col.some((o) => overlaps(ev.start, ev.end, o.start, o.end));
82
+ if (blocked) break;
83
+ span++;
84
+ }
85
+ return span;
86
+ }
87
+
88
+ export { layoutDay };
89
+ //# sourceMappingURL=index.js.map
90
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/layout.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Interval, LayoutOptions, PositionedInterval } from \"./types.js\";\n\n/** Internal, normalized (clamped) representation of an interval. */\ninterface Normalized<T extends Interval> {\n data: T;\n start: number;\n end: number;\n}\n\n/** Two half-open intervals overlap iff each starts before the other ends. */\nfunction overlaps(aStart: number, aEnd: number, bStart: number, bEnd: number): boolean {\n return aStart < bEnd && bStart < aEnd;\n}\n\n/**\n * Lay out timed events for a single day/column axis, resolving overlaps into\n * side-by-side columns.\n *\n * Algorithm (the same shape Google Calendar uses):\n * 1. **Normalize** — clamp every event to the day window, drop anything that\n * falls outside it, optionally enforce a minimum duration.\n * 2. **Sort** — by start ascending, then by end descending (longer events\n * first), then by id for a stable, deterministic order.\n * 3. **Cluster** — sweep the sorted list into maximal runs of transitively\n * overlapping events. Columns are counted per cluster, so a busy morning\n * never shrinks a quiet afternoon.\n * 4. **Pack** — within a cluster, greedily drop each event into the first\n * column whose previous event has already ended; open a new column only\n * when none is free.\n * 5. **Expand** — optionally widen each event rightward across columns that are\n * free for its entire duration.\n *\n * The engine is pure and deterministic: same input → same output, no clock, no\n * DOM, no framework.\n */\nexport function layoutDay<T extends Interval>(\n events: readonly T[],\n options: LayoutOptions = {},\n): PositionedInterval<T>[] {\n const dayStart = options.dayStart ?? 0;\n const dayEnd = options.dayEnd ?? 1440;\n const expand = options.expand ?? true;\n const minDuration = options.minDuration ?? 0;\n\n const range = dayEnd - dayStart;\n if (range <= 0) return [];\n\n // 1. Normalize: clamp to window, apply minDuration, drop out-of-window.\n const norm: Normalized<T>[] = [];\n for (const e of events) {\n let start = Math.max(e.start, dayStart);\n let end = Math.min(e.end, dayEnd);\n if (minDuration > 0 && end - start < minDuration) {\n end = Math.min(start + minDuration, dayEnd);\n // If clamping to the window pushed us against the wall, pull start back\n // so a min-duration event near dayEnd still renders its full min height.\n start = Math.max(dayStart, end - minDuration);\n }\n if (end <= start) continue;\n norm.push({ data: e, start, end });\n }\n if (norm.length === 0) return [];\n\n // 2. Sort: start asc, end desc, id asc.\n norm.sort(\n (a, b) =>\n a.start - b.start ||\n b.end - a.end ||\n String(a.data.id).localeCompare(String(b.data.id)),\n );\n\n const result: PositionedInterval<T>[] = [];\n\n // 3. Cluster + 4. Pack + 5. Expand, one maximal overlap-run at a time.\n let i = 0;\n while (i < norm.length) {\n let clusterEnd = norm[i]!.end;\n let j = i + 1;\n while (j < norm.length && norm[j]!.start < clusterEnd) {\n clusterEnd = Math.max(clusterEnd, norm[j]!.end);\n j++;\n }\n layoutCluster(norm.slice(i, j), { dayStart, range, expand }, result);\n i = j;\n }\n\n return result;\n}\n\ninterface ClusterContext {\n dayStart: number;\n range: number;\n expand: boolean;\n}\n\nfunction layoutCluster<T extends Interval>(\n cluster: readonly Normalized<T>[],\n ctx: ClusterContext,\n out: PositionedInterval<T>[],\n): void {\n // 4. Greedy column packing. `columns[c]` holds the events placed in column c,\n // in time order; the last one determines whether the column is free.\n const columns: Normalized<T>[][] = [];\n const columnOf = new Map<Normalized<T>, number>();\n\n for (const ev of cluster) {\n let placed = false;\n for (let c = 0; c < columns.length; c++) {\n const col = columns[c]!;\n const last = col[col.length - 1]!;\n if (last.end <= ev.start) {\n col.push(ev);\n columnOf.set(ev, c);\n placed = true;\n break;\n }\n }\n if (!placed) {\n columnOf.set(ev, columns.length);\n columns.push([ev]);\n }\n }\n\n const columnCount = columns.length;\n\n // 5. Expansion + geometry.\n for (const ev of cluster) {\n const columnIndex = columnOf.get(ev)!;\n const span = ctx.expand\n ? spanFor(ev, columnIndex, columnCount, columns)\n : 1;\n\n out.push({\n data: ev.data,\n columnIndex,\n columnCount,\n span,\n top: (ev.start - ctx.dayStart) / ctx.range,\n height: (ev.end - ev.start) / ctx.range,\n left: columnIndex / columnCount,\n width: span / columnCount,\n });\n }\n}\n\n/**\n * How many columns `ev` can occupy: start at its own column and extend right\n * while every event in the next column clears its time span.\n */\nfunction spanFor<T extends Interval>(\n ev: Normalized<T>,\n columnIndex: number,\n columnCount: number,\n columns: readonly Normalized<T>[][],\n): number {\n let span = 1;\n for (let c = columnIndex + 1; c < columnCount; c++) {\n const col = columns[c]!;\n const blocked = col.some((o) => overlaps(ev.start, ev.end, o.start, o.end));\n if (blocked) break;\n span++;\n }\n return span;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "calendar-ui-layout",
3
+ "version": "0.1.0",
4
+ "description": "Headless time-grid layout engine: overlap-column resolution for calendar/scheduler week & day views. UI-agnostic, zero runtime dependencies.",
5
+ "keywords": [
6
+ "calendar",
7
+ "scheduler",
8
+ "headless",
9
+ "layout",
10
+ "overlap",
11
+ "time-grid",
12
+ "react",
13
+ "typescript"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "jiku0730",
17
+ "homepage": "https://github.com/jiku0730/calendar-ui-layout#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/jiku0730/calendar-ui-layout.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/jiku0730/calendar-ui-layout/issues"
24
+ },
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.cjs"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "typecheck": "tsc --noEmit",
50
+ "demo": "tsx examples/ascii-preview.ts",
51
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "devDependencies": {
57
+ "tsup": "^8.3.5",
58
+ "tsx": "^4.23.1",
59
+ "typescript": "^5.7.3",
60
+ "vitest": "^3.0.5"
61
+ }
62
+ }