Skip to content

Commit 2e01df0

Browse files
authored
Merge pull request #39 from ZDOSS/codex/fix-roadmap-interactions
fix: repair roadmap drag, resize, and date geometry
2 parents 2fb186b + 1f8a5bb commit 2e01df0

6 files changed

Lines changed: 410 additions & 81 deletions

File tree

AI.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,10 @@ The core domain in `packages/core/src/domain/` covers the entities the plan call
263263
- that editor reset now keys only on `doc.id`, so switching documents still refreshes the draft while external bundle updates to the same document do not silently clobber unsaved local typing
264264
- deleting the currently open doc now routes to the nearest active remaining document, or `/docs` when none remain, so the route does not stay pinned to a deleted or archived document ID while other docs still exist
265265
- `DocEditor` now keeps that selection-sync effect above its null guard so hook ordering stays valid even if future refactors ever allow the component to see a transient `bundle === null`
266-
- roadmap lanes now show milestone target dates, completed/total/percent progress, dependency indicators from `relationshipsForItem()`, explicit date inputs, milestone reassignment controls, and inline invalid-range feedback while preserving date-only semantics through `item.update`; clearing Start or Due clears only that side of the range
266+
- roadmap lanes show milestone target dates, completed/total/percent progress, dependency indicators from `relationshipsForItem()`, explicit date inputs, milestone reassignment controls, and inline invalid-range feedback while preserving date-only semantics through `item.update`; clearing Start or Due clears only that side of the range
267+
- roadmap geometry has one shared day scale: month header columns use each month's actual day count, bars render as inclusive start/due ranges expressed as percentages of the visible timeline, and the same visible `totalDays / measuredRowWidth` ratio converts pointer movement back into date-only day shifts. Do not reintroduce fixed virtual-pixel coordinates or a fixed pixels-per-day drag divisor.
268+
- the timeline bar and its edit controls are deliberately separate siblings inside `.roadmap-item`. The bar's rendered width represents only its scheduled duration; title/date/milestone controls live in the detail row below and therefore cannot impose a fake minimum duration. The item title link is also outside the drag surface, preventing a completed gesture from navigating to item detail.
269+
- roadmap pointer sessions capture the pointer on the actual bar, retain tracking beyond the bar/resize handle, preview one local day shift during the gesture, and dispatch a single validated `item.update` when the pointer is released. Move gestures preserve a missing Start or Due side, resize gestures materialize a range when needed, backward resizing clamps at Start, pointer cancellation discards the preview, and the resize grip retains one-day Arrow / one-week Shift+Arrow keyboard behavior.
267270
- calendar now keeps the month grid and adds a derived agenda for upcoming item start/due dates plus active reminders; reminder agenda rows use the reminder's IANA timezone to decide the visible date, boundary filtering keeps reminders visible when their local display date and UTC date straddle the agenda start, agenda links use existing work-item routes, and the feature does not introduce a calendar-specific storage model
268271
- calendar scheduled-work creation intentionally creates ordinary work items through the shared `CreateItemDialog` and `item.create` command, not a separate calendar-event entity; day-cell add buttons prefill the explicit cell `dueDate`, the toolbar opens the same dialog without carrying the hidden month `anchor` date, and the dialog exposes editable Start/Due date fields plus the existing type/status/priority/assignee controls
269272
- bug triage now exposes a visible `New bug` action in the intake column, maps software-project `inbox` bugs into Intake, opens the shared create dialog with the correct intake status preselected, and limits Intake's planned-status fallback so custom workflows can still populate Ready; `buildBugTriageColumns()` is shared with overview so accepted Ready bugs do not reappear as intake pressure
@@ -359,7 +362,7 @@ The core domain in `packages/core/src/domain/` covers the entities the plan call
359362
- table priority and updated-date sort direction behavior
360363
- table bulk status/priority/assignee editing across selected rows, including selections hidden by a local filter
361364
- overview route summaries, default root-to-overview routing for open projects, project create/open/demo/import landing on overview, future-only reminders, and triage-lane bug intake matching the bug board
362-
- roadmap milestone progress, dependency indicators, date inputs, independent date clearing, and milestone reassignment controls
365+
- roadmap milestone progress, dependency indicators, date inputs, independent date clearing, milestone reassignment, inclusive bar-width geometry, measured-scale pointer move/resize, and pointer capture/release behavior
363366
- calendar agenda rendering from item dates and timezone-local reminder dates, plus dated work-item creation from calendar day cells
364367
- trash restore and confirmed permanent deletion for work items, documents, and attachments
365368
- document soft-delete cleanup for document-scoped reminders and attachments, including reminder restoration from the document trash payload
@@ -734,7 +737,7 @@ The core domain in `packages/core/src/domain/` covers the entities the plan call
734737
## Open follow-on planning
735738

736739
- UI/UX overhaul planning lives in `docs/superpowers/specs/2026-07-02-ui-ux-overhaul-design.md`, `docs/superpowers/plans/2026-07-02-ui-ux-overhaul-implementation-plan.md`, and the new `docs/plans/July 2026 plan.md`; the first implementation pass is now in code, and the July plan is the active priority order for deeper product-depth, workflow, and surface-by-surface improvements
737-
- a deeper roadmap interaction plan (multi-day bars, dependencies, swimlanes)
740+
- roadmap follow-on depth is now limited to dependency-line rendering, milestone-lane drag reassignment, and optional swimlanes; multi-day geometry plus pointer/keyboard move and resize are implemented and browser-tested
738741
- a security-first plugin runtime plan before any third-party plugin execution
739742
- a public-internet hosting plan (currently out of MVP scope; GitHub Pages is a static hosted demo, and real work remains local/self-hosted until a richer trusted hosting story exists)
740743
- a sync-backend plan if richer collaboration is pursued

Readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Grillo is past the skeleton stage. The current app includes:
5959
- JSON, Markdown, and CSV import/export, plus clean print preview.
6060
- A full Appearance studio with light/dark/system modes, Grillo Adaptive, Graphite, Warm Sand, and High Contrast presets, seed-based custom theme creation, every authored UI color exposed as a semantic role, live preview, contrast checks, safe JSON import/export, per-project personal overrides, and a shared project accent.
6161
- A remembered desktop sidebar that toggles between full navigation and an icon rail, context-aware no-project navigation, responsive mobile navigation and project tabs, offline status, and local/self-hosted PWA install support when the browser exposes it.
62-
- Planning-surface UX polish including aligned Backlog rows, progressive Table controls, a keyboard-operable Roadmap resize grip, a complete seven-day Calendar beside its agenda, and item-title link names for assistive technology.
62+
- Planning-surface UX polish including aligned Backlog rows, progressive Table controls, and a day-accurate Roadmap whose bars resize with their dates and support pointer-captured move/resize plus keyboard adjustment; Calendar keeps all seven days beside its agenda, and item links retain useful assistive names.
6363
- A realistic, non-persistent demo workspace with active and completed work, bug intake, comments, members, dates, milestones, and linked docs.
6464

6565
## Appearance and themes
@@ -141,7 +141,7 @@ The test suite covers:
141141
- `packages/ui`: AppShell, project router, shared primitives, work item modal, planning views, docs, settings, launcher, command palette, trash, automation settings.
142142
- `apps/web`: browser/PWA storage adapter and distribution-mode behavior.
143143
- `apps/desktop`: desktop storage adapter command wiring.
144-
- `tests/e2e`: explicit launcher-to-project setup, hybrid parity, project workflow, theme, command palette, export, search, calendar creation, docs edit-mode behavior, and mobile navigation.
144+
- `tests/e2e`: explicit launcher-to-project setup, hybrid parity, project workflow, theme, command palette, export, search, calendar creation, roadmap date/drag/resize geometry, docs edit-mode behavior, and mobile navigation.
145145

146146
Run unit/component coverage with `npm test`, browser coverage with `npm run test:e2e`, and TypeScript coverage with `npm run typecheck`.
147147

packages/ui/src/theme/global.css

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ a.btn {
831831
background: var(--color-bg-surface);
832832
border: 1px solid var(--color-border-subtle);
833833
border-radius: var(--radius-md);
834-
min-width: 800px;
834+
min-width: 960px;
835835
}
836836
.roadmap-header {
837837
display: contents;
@@ -875,63 +875,118 @@ a.btn {
875875
position: relative;
876876
height: 56px;
877877
}
878+
.roadmap-item {
879+
position: absolute;
880+
left: 0;
881+
right: 0;
882+
height: 88px;
883+
pointer-events: none;
884+
}
878885
.roadmap-bar {
879886
position: absolute;
880-
top: 8px;
881-
min-height: 72px;
882-
min-width: 360px;
887+
top: 0;
888+
height: 32px;
889+
min-width: 0;
883890
border-radius: var(--radius-sm);
884891
background: var(--color-accent-soft);
885892
border: 1px solid var(--color-accent);
886-
padding: var(--space-2);
893+
padding: 5px 14px 5px var(--space-2);
887894
display: flex;
888-
flex-direction: column;
889-
align-items: stretch;
895+
align-items: center;
890896
gap: var(--space-1);
891897
font-size: var(--font-size-xs);
892898
color: var(--color-text-primary);
893899
cursor: grab;
900+
pointer-events: auto;
901+
touch-action: none;
894902
user-select: none;
895903
overflow: visible;
896-
white-space: normal;
904+
white-space: nowrap;
905+
}
906+
.roadmap-bar[data-dragging] {
907+
cursor: grabbing;
908+
z-index: 2;
909+
}
910+
.roadmap-bar-label {
911+
display: block;
912+
min-width: 0;
913+
overflow: hidden;
914+
text-overflow: ellipsis;
915+
pointer-events: none;
897916
}
898-
.roadmap-bar-main,
899917
.roadmap-bar-controls {
918+
position: absolute;
919+
top: 39px;
920+
left: var(--space-2);
921+
right: var(--space-2);
900922
display: flex;
901-
align-items: center;
923+
align-items: flex-end;
902924
gap: var(--space-2);
903925
min-width: 0;
926+
pointer-events: auto;
904927
}
905-
.roadmap-bar-controls label {
928+
.roadmap-item-link {
929+
flex: 1 1 120px;
930+
min-width: 96px;
931+
max-width: 220px;
932+
padding-bottom: 7px;
933+
color: var(--color-text-primary);
934+
font-size: var(--font-size-xs);
935+
font-weight: 600;
936+
overflow: hidden;
937+
text-decoration: none;
938+
text-overflow: ellipsis;
939+
white-space: nowrap;
940+
}
941+
.roadmap-item-link:hover {
942+
color: var(--color-accent);
943+
text-decoration: underline;
944+
}
945+
.roadmap-bar-badges {
906946
display: flex;
907947
align-items: center;
908-
gap: 4px;
948+
gap: var(--space-1);
949+
padding-bottom: 5px;
950+
}
951+
.roadmap-bar-badges:empty {
952+
display: none;
953+
}
954+
.roadmap-bar-controls label {
955+
display: flex;
956+
flex-direction: column;
957+
align-items: stretch;
958+
gap: 2px;
909959
}
910960
.roadmap-resize-handle {
911-
align-self: stretch;
961+
position: absolute;
962+
top: 50%;
963+
right: -11px;
964+
transform: translateY(-50%);
912965
display: inline-flex;
913966
align-items: center;
914967
justify-content: center;
915968
width: 22px;
916969
min-width: 22px;
970+
height: 26px;
917971
padding: 0;
918-
border: 0;
919-
border-left: 1px solid color-mix(in srgb, currentColor 24%, transparent);
920-
background: transparent;
972+
border: 1px solid currentColor;
973+
border-radius: var(--radius-sm);
974+
background: var(--color-bg-surface);
921975
color: currentColor;
922976
cursor: ew-resize;
977+
touch-action: none;
923978
}
924979
.roadmap-resize-handle:focus-visible {
925980
outline: 2px solid var(--color-accent);
926981
outline-offset: 1px;
927982
}
928983
.roadmap-date-input {
929-
width: 132px;
984+
width: 112px;
930985
min-height: 28px;
931986
font-size: var(--font-size-2xs);
932987
}
933988
.roadmap-milestone-select {
934-
width: 150px;
989+
width: 132px;
935990
min-height: 28px;
936991
font-size: var(--font-size-2xs);
937992
}

packages/ui/src/views/roadmap/RoadmapView.test.tsx

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
22
import userEvent from "@testing-library/user-event";
3-
import { beforeEach, describe, expect, it } from "vitest";
3+
import { beforeEach, describe, expect, it, vi } from "vitest";
44
import { MemoryRouter } from "react-router-dom";
55
import { buildProjectFromTemplate } from "@gph/core";
66
import { closeCreateItem } from "../../commands/palette-bus";
77
import { useProjectStore } from "../../store/project-store";
88
import { CreateItemDialog } from "../../work-item";
99
import { RoadmapView } from "./RoadmapView";
1010

11+
class MockPointerEvent extends MouseEvent {
12+
readonly pointerId: number;
13+
readonly isPrimary: boolean;
14+
15+
constructor(type: string, init: PointerEventInit = {}) {
16+
super(type, init);
17+
this.pointerId = init.pointerId ?? 0;
18+
this.isPrimary = init.isPrimary ?? true;
19+
}
20+
}
21+
22+
vi.stubGlobal("PointerEvent", MockPointerEvent);
23+
1124
describe("RoadmapView", () => {
1225
beforeEach(() => {
1326
cleanup();
@@ -116,6 +129,98 @@ describe("RoadmapView", () => {
116129
expect(updated.milestoneId).toBe(secondMilestone.id);
117130
});
118131

132+
it("keeps the rendered bar width synchronized with the inclusive date range", () => {
133+
const bundle = buildProjectFromTemplate("software-project", "Roadmap geometry");
134+
useProjectStore.setState({ bundle });
135+
useProjectStore.getState().applyCommand({
136+
type: "item.create",
137+
projectId: bundle.project.id,
138+
typeId: "task",
139+
title: "Resizable roadmap item",
140+
statusId: "ready",
141+
startDate: "2026-07-01",
142+
dueDate: "2026-07-10"
143+
});
144+
145+
render(<MemoryRouter><RoadmapView /></MemoryRouter>);
146+
fireEvent.change(screen.getByLabelText("Anchor"), { target: { value: "2026-07" } });
147+
148+
const bar = screen.getByRole("group", { name: /Resizable roadmap item timeline/ });
149+
const grid = bar.closest<HTMLElement>(".roadmap-grid")!;
150+
expect(grid.style.gridTemplateColumns).toBe("220px 31fr 31fr 30fr 31fr 30fr 31fr");
151+
expect(parseFloat(bar.style.width)).toBeCloseTo((10 / 184) * 100);
152+
153+
fireEvent.change(screen.getByLabelText("Due date for Resizable roadmap item"), {
154+
target: { value: "2026-07-20" }
155+
});
156+
157+
expect(parseFloat(bar.style.width)).toBeCloseTo((20 / 184) * 100);
158+
expect(bar).toHaveAttribute("data-due-date", "2026-07-20");
159+
});
160+
161+
it("moves and resizes with the measured timeline scale and pointer capture", () => {
162+
const bundle = buildProjectFromTemplate("software-project", "Roadmap pointers");
163+
useProjectStore.setState({ bundle });
164+
useProjectStore.getState().applyCommand({
165+
type: "item.create",
166+
projectId: bundle.project.id,
167+
typeId: "task",
168+
title: "Pointer roadmap item",
169+
statusId: "ready",
170+
startDate: "2026-07-01",
171+
dueDate: "2026-07-10"
172+
});
173+
174+
render(<MemoryRouter><RoadmapView /></MemoryRouter>);
175+
fireEvent.change(screen.getByLabelText("Anchor"), { target: { value: "2026-07" } });
176+
177+
const bar = screen.getByRole("group", { name: /Pointer roadmap item timeline/ });
178+
const timeline = bar.parentElement!;
179+
vi.spyOn(timeline, "getBoundingClientRect").mockReturnValue({
180+
bottom: 88,
181+
height: 88,
182+
left: 0,
183+
right: 1840,
184+
top: 0,
185+
width: 1840,
186+
x: 0,
187+
y: 0,
188+
toJSON: () => ({})
189+
});
190+
const setPointerCapture = vi.fn();
191+
const releasePointerCapture = vi.fn();
192+
Object.assign(bar, {
193+
hasPointerCapture: vi.fn(() => true),
194+
releasePointerCapture,
195+
setPointerCapture
196+
});
197+
198+
fireEvent.pointerDown(bar, { button: 0, clientX: 100, isPrimary: true, pointerId: 7 });
199+
fireEvent.pointerMove(bar, { clientX: 130, isPrimary: true, pointerId: 7 });
200+
fireEvent.pointerUp(bar, { clientX: 130, isPrimary: true, pointerId: 7 });
201+
202+
let updated = useProjectStore.getState().bundle!.core.items.find((item) =>
203+
item.title === "Pointer roadmap item"
204+
)!;
205+
expect(updated).toMatchObject({ startDate: "2026-07-04", dueDate: "2026-07-13" });
206+
expect(setPointerCapture).toHaveBeenCalledWith(7);
207+
expect(releasePointerCapture).toHaveBeenCalledWith(7);
208+
209+
const resizeHandle = screen.getByRole("button", { name: "Adjust due date for Pointer roadmap item" });
210+
fireEvent.pointerDown(resizeHandle, { button: 0, clientX: 200, isPrimary: true, pointerId: 8 });
211+
fireEvent.pointerMove(bar, { clientX: 270, isPrimary: true, pointerId: 8 });
212+
fireEvent.pointerUp(bar, { clientX: 270, isPrimary: true, pointerId: 8 });
213+
214+
updated = useProjectStore.getState().bundle!.core.items.find((item) =>
215+
item.title === "Pointer roadmap item"
216+
)!;
217+
expect(updated).toMatchObject({ startDate: "2026-07-04", dueDate: "2026-07-20" });
218+
expect(screen.getByRole("group", { name: /Pointer roadmap item timeline/ })).toHaveAttribute(
219+
"data-due-date",
220+
"2026-07-20"
221+
);
222+
});
223+
119224
it("clears only the edited side of a roadmap date range", () => {
120225
const bundle = buildProjectFromTemplate("software-project", "Roadmap");
121226
useProjectStore.setState({ bundle });

0 commit comments

Comments
 (0)