diff --git a/apps/web/components/kanban/adaptive-desktop-kanban.test.tsx b/apps/web/components/kanban/adaptive-desktop-kanban.test.tsx index 716740e850..0dae952274 100644 --- a/apps/web/components/kanban/adaptive-desktop-kanban.test.tsx +++ b/apps/web/components/kanban/adaptive-desktop-kanban.test.tsx @@ -5,10 +5,11 @@ import { AdaptiveDesktopKanban } from "./adaptive-desktop-kanban"; const blankColumnSpaceTestId = "blank-column-space"; const grabbingCursorClass = "cursor-grabbing"; -function renderBoard() { +function renderBoard(isDragging = false) { render( (
@@ -39,6 +40,19 @@ function startPan(window: HTMLElement, clientX = 200) { } afterEach(cleanup); + +describe("AdaptiveDesktopKanban drag end reserve", () => { + it("@covers AC-UI-ADAPTIVE-KANBAN-001.9 keeps the reserve outside grid sizing", () => { + renderBoard(true); + + const grid = screen.getByTestId("desktop-kanban-lane-grid"); + expect(grid.style.paddingInlineEnd).toBe(""); + expect(screen.getByTestId("desktop-kanban-drag-end-reserve").getAttribute("aria-hidden")).toBe( + "true", + ); + }); +}); + describe("AdaptiveDesktopKanban mouse panning", () => { it("shows a grabbing cursor only while holding an eligible board background", () => { const window = renderBoard(); diff --git a/apps/web/components/kanban/adaptive-desktop-kanban.tsx b/apps/web/components/kanban/adaptive-desktop-kanban.tsx index 2549f08a8b..454c64677f 100644 --- a/apps/web/components/kanban/adaptive-desktop-kanban.tsx +++ b/apps/web/components/kanban/adaptive-desktop-kanban.tsx @@ -10,7 +10,7 @@ type AdaptiveDesktopKanbanProps = { renderColumn: (step: WorkflowStep) => ReactNode; }; -export const KANBAN_DRAG_END_PADDING = `max(0px, calc(100% - ${KANBAN_COLUMN_MIN_PX}px))`; +const KANBAN_DRAG_END_RESERVE = `max(0px, calc(100cqw - ${KANBAN_COLUMN_MIN_PX}px))`; const PAN_ACTIVATION_DISTANCE_PX = 4; const INTERACTIVE_TARGET_SELECTOR = [ @@ -87,26 +87,50 @@ export function AdaptiveDesktopKanban({ data-testid="desktop-kanban-scroll-window" className={`h-full min-h-0 min-w-0 overflow-x-auto snap-x snap-mandatory ${ isPanCandidate ? "cursor-grabbing" : "" - } ${isPanning ? "select-none" : ""}`} - style={{ scrollSnapType: isPanning ? "none" : undefined }} + } ${isPanning ? "select-none" : ""} ${isDragging ? "scrollbar-hide" : ""}`} + style={{ + containerType: "inline-size", + scrollSnapType: isPanning ? "none" : undefined, + }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={cancelPan} onMouseLeave={cancelPan} >
- {steps.map((step) => ( -
- {renderColumn(step)} -
- ))} +
+ {steps.map((step) => ( +
+ {renderColumn(step)} +
+ ))} +
+ {isDragging && ( +
diff --git a/apps/web/components/kanban/kanban-grid-template.test.ts b/apps/web/components/kanban/kanban-grid-template.test.ts index 69547e5691..75444bbefc 100644 --- a/apps/web/components/kanban/kanban-grid-template.test.ts +++ b/apps/web/components/kanban/kanban-grid-template.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; import { getKanbanColumnGridTemplate } from "./kanban-grid-template"; -import { KANBAN_DRAG_END_PADDING } from "./adaptive-desktop-kanban"; describe("getKanbanColumnGridTemplate", () => { it("keeps every desktop column readable", () => { @@ -15,9 +14,3 @@ describe("getKanbanColumnGridTemplate", () => { expect(getKanbanColumnGridTemplate(0)).toBe("repeat(0, minmax(280px, 1fr))"); }); }); - -describe("KANBAN_DRAG_END_PADDING", () => { - it("reserves one viewport minus one readable column for drag anchoring", () => { - expect(KANBAN_DRAG_END_PADDING).toBe("max(0px, calc(100% - 280px))"); - }); -}); diff --git a/apps/web/e2e/tests/kanban/auto-hide-empty-columns.spec.ts b/apps/web/e2e/tests/kanban/auto-hide-empty-columns.spec.ts index 3f063be8eb..301e4fd468 100644 --- a/apps/web/e2e/tests/kanban/auto-hide-empty-columns.spec.ts +++ b/apps/web/e2e/tests/kanban/auto-hide-empty-columns.spec.ts @@ -28,6 +28,12 @@ async function beginPointerDrag(page: Page, card: Locator) { await page.mouse.move(x + 20, y, { steps: 4 }); } +async function renderedColumnWidths(page: Page) { + return page + .locator("[data-kanban-step-id]") + .evaluateAll((columns) => columns.map((column) => column.getBoundingClientRect().width)); +} + test("auto-hides empty columns without changing drag destinations", async ({ testPage, apiClient, @@ -59,6 +65,32 @@ test("auto-hides empty columns without changing drag destinations", async ({ await expect(kanban.columnByStepId(autoHiddenStep.id)).toBeVisible(); await expect(kanban.columnByStepId(manuallyHiddenStep.id)).toBeVisible(); + // @covers AC-UI-ADAPTIVE-KANBAN-001.9 + const widthsBeforeDrag = await renderedColumnWidths(testPage); + expect(widthsBeforeDrag.length).toBe(3); + await beginPointerDrag(testPage, kanban.taskCard(task.id)); + await expect(testPage.getByTestId("desktop-kanban-drag-end-reserve")).toHaveCount(1); + const widthsDuringDrag = await renderedColumnWidths(testPage); + expect(widthsDuringDrag).toHaveLength(widthsBeforeDrag.length); + widthsDuringDrag.forEach((width, index) => { + expect(Math.abs(width - widthsBeforeDrag[index])).toBeLessThanOrEqual(1); + }); + const dragOverflow = await testPage.evaluate(() => { + const scrollWindow = document.querySelector( + '[data-testid="desktop-kanban-scroll-window"]', + ); + if (!scrollWindow) throw new Error("desktop Kanban scroll window is missing"); + return { + documentClientWidth: document.documentElement.clientWidth, + documentScrollWidth: document.documentElement.scrollWidth, + scrollbarWidth: getComputedStyle(scrollWindow).scrollbarWidth, + }; + }); + expect(dragOverflow.documentScrollWidth).toBe(dragOverflow.documentClientWidth); + expect(dragOverflow.scrollbarWidth).toBe("none"); + await testPage.keyboard.press("Escape"); + await testPage.mouse.up(); + await openColumnsMenu(testPage, workflow.id); const autoHideToggle = testPage.getByTestId(`columns-menu-auto-hide-empty-${workflow.id}`); await expect(autoHideToggle).toHaveAttribute("aria-checked", "false"); @@ -120,3 +152,42 @@ test("auto-hides empty columns without changing drag destinations", async ({ expect(settledDestinationBox.x + settledDestinationBox.width).toBeGreaterThan(0); await expect(kanban.columnByStepId(manuallyHiddenStep.id)).toHaveCount(0); }); + +test("keeps the drag reserve after overflowing minimum-width tracks", async ({ + testPage, + apiClient, + seedData, +}) => { + await testPage.setViewportSize({ width: 1440, height: 900 }); + const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Overflow reserve E2E"); + const steps = await Promise.all( + ["Source", "Second", "Third", "Fourth", "Fifth", "Sixth"].map((title, index) => + apiClient.createWorkflowStep(workflow.id, title, index, { is_start_step: index === 0 }), + ), + ); + const task = await apiClient.createTask(seedData.workspaceId, "Overflow reserve sample", { + workflow_id: workflow.id, + workflow_step_id: steps[0].id, + }); + await apiClient.saveUserSettings({ + workspace_id: seedData.workspaceId, + workflow_filter_id: workflow.id, + kanban_hidden_step_ids: {}, + workflow_ids_with_auto_hide_empty_steps: [], + }); + + const kanban = new KanbanPage(testPage); + await kanban.goto(); + await expect(kanban.columnByStepId(steps[5].id)).toBeVisible(); + + const scrollWindow = testPage.getByTestId("desktop-kanban-scroll-window"); + const scrollWidthBeforeDrag = await scrollWindow.evaluate((element) => element.scrollWidth); + const clientWidth = await scrollWindow.evaluate((element) => element.clientWidth); + await beginPointerDrag(testPage, kanban.taskCard(task.id)); + const scrollWidthDuringDrag = await scrollWindow.evaluate((element) => element.scrollWidth); + expect(scrollWidthDuringDrag - scrollWidthBeforeDrag).toBeGreaterThanOrEqual( + clientWidth - 280 - 1, + ); + await testPage.keyboard.press("Escape"); + await testPage.mouse.up(); +}); diff --git a/docs/plans/kanban-drag-column-width-stability/plan.md b/docs/plans/kanban-drag-column-width-stability/plan.md new file mode 100644 index 0000000000..23c05c0419 --- /dev/null +++ b/docs/plans/kanban-drag-column-width-stability/plan.md @@ -0,0 +1,94 @@ +--- +created: 2026-09-05 +status: complete +requirements: + - REQ-UI-ADAPTIVE-KANBAN-001 +system_design: + - ../../specs/ui/system-design/adaptive-kanban.md +legacy_specs: [] +--- + +# Implementation Plan: Kanban Drag Column Width Stability + +## Overview + +Correct GitHub issue #3345 without removing drag scroll anchoring. Move the drag end reserve outside the grid sizing box and add regression evidence. + +## Confirmed root cause + +`AdaptiveDesktopKanban` applies `padding-inline-end: calc(100% - 280px)` while a drag is active. This padding reduces the grid content box. + +The grid uses `repeat(N, minmax(280px, 1fr))`. The reduced content box removes free space from the `1fr` tracks and compresses each column to 280px. + +A focused Chromium reproduction measured a source column at 362.66px before drag and 280px during drag. + +## Scope + +### In scope + +- Keep the width of existing desktop columns stable when drag state does not change the rendered steps. +- Preserve the end scroll range that supports drag scroll anchoring. +- Keep drag-only scroll overflow contained and hide its transient native scrollbar. +- Preserve auto-hidden drag destinations, cancellation, and successful drops. +- Add component and Chromium E2E regression coverage. + +### Out of scope + +- Changes to the 280px readable minimum. +- Changes to the drag-and-drop model or task move API. +- Changes to auto-hide or manual-hide rules. +- Changes to tablet or phone composition. +- Public documentation changes. + +## Technical approach + +### Grid end reserve + +Update `apps/web/components/kanban/adaptive-desktop-kanban.tsx`. Replace the drag end padding with a trailing spacer after the lane grid. + +The spacer must remain conditional on `isDragging`. The lane grid must size to the viewport or its minimum track width before the spacer is appended. + +Keep `useKanbanDragScrollAnchor` unchanged. Its source-position capture and `scrollLeft` restoration still own drag anchoring. + +### Regression tests + +Update the focused component tests to assert the trailing spacer style and the absence of end padding. + +Extend `apps/web/e2e/tests/kanban/auto-hide-empty-columns.spec.ts`. Measure a visible column before drag and after drag activation. + +The E2E test must fail on the current padding behavior. It must continue through auto-hide enablement, cancellation, and a successful drop. + +## Tests + +| Acceptance criterion | Evidence | +| --- | --- | +| `AC-UI-ADAPTIVE-KANBAN-001.2` | `kanban-grid-template.test.ts` keeps the distributed `minmax(280px, 1fr)` template. | +| `AC-UI-ADAPTIVE-KANBAN-001.3` | The existing Chromium scenario proves contained horizontal lane scrolling. | +| `AC-UI-ADAPTIVE-KANBAN-001.4` | The existing Chromium scenario completes drag cancellation and a successful task move. | +| `AC-UI-ADAPTIVE-KANBAN-001.9` | The Chromium scenario compares every rendered desktop column before and during drag and verifies reserve after overflowing tracks. | +| `AC-UI-ADAPTIVE-KANBAN-001.10` | The Chromium scenario verifies fixed document width and a hidden native scrollbar during drag. | + +## E2E tests + +Use the Chromium project for `apps/web/e2e/tests/kanban/auto-hide-empty-columns.spec.ts`. + +The scenario uses a 1440px desktop viewport and three visible columns. It compares every rendered column width before and during drag and verifies contained, hidden drag-only overflow. + +No new mobile test is required because this correction changes only `AdaptiveDesktopKanban`. The existing mobile auto-hide scenario remains the parity evidence. + +## Work orders + +- [x] [Task 01: Stabilize desktop drag columns](task-01-stabilize-desktop-drag-columns.md) + +## Verification results + +- The focused component suite passed with 10 tests. +- The Chromium production-build scenario passed with two tests, including overflow-track scroll range. +- The drag-only scrollbar regression failed with `scrollbar-width: thin` and passed with `scrollbar-width: none` after the correction. +- Specification tests and lint passed, and `git diff --check` passed. +- Scoped ESLint, web type checking, and the i18n ratchet passed. + +## Risks + +- Removing the reserve instead of relocating it can break source-column anchoring when auto-hidden destinations appear. +- A style-only component test cannot prove browser geometry. The Chromium assertion supplies that evidence. diff --git a/docs/plans/kanban-drag-column-width-stability/task-01-stabilize-desktop-drag-columns.md b/docs/plans/kanban-drag-column-width-stability/task-01-stabilize-desktop-drag-columns.md new file mode 100644 index 0000000000..43d03a2261 --- /dev/null +++ b/docs/plans/kanban-drag-column-width-stability/task-01-stabilize-desktop-drag-columns.md @@ -0,0 +1,93 @@ +--- +id: "01-stabilize-desktop-drag-columns" +title: "Stabilize desktop drag columns" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +requirements: + - REQ-UI-ADAPTIVE-KANBAN-001 +acceptance_criteria: + - AC-UI-ADAPTIVE-KANBAN-001.2 + - AC-UI-ADAPTIVE-KANBAN-001.3 + - AC-UI-ADAPTIVE-KANBAN-001.4 + - AC-UI-ADAPTIVE-KANBAN-001.9 + - AC-UI-ADAPTIVE-KANBAN-001.10 +system_design: + - ../../specs/ui/system-design/adaptive-kanban.md +--- + +# Task 01: Stabilize Desktop Drag Columns + +## Summary + +Move the desktop drag end reserve after the overflowing grid tracks. Add browser geometry evidence and preserve the current drag-anchor behavior. + +## In scope + +- Replace drag end padding with a trailing spacer after the lane grid in `AdaptiveDesktopKanban`. +- Update the focused component tests for the drag-only style. +- Add a Chromium E2E assertion for stable column width during drag. +- Keep the existing auto-hidden destination, cancellation, and drop assertions green. + +## Out of scope + +- Changes to column minimum width or responsive breakpoints. +- Changes to `useKanbanDragScrollAnchor` behavior. +- Changes to phone or tablet layouts. +- Changes to backend or persistence contracts. + +## Acceptance + +- The regression test fails because the current drag padding changes a column from 362.66px to 280px. +- A drag with an unchanged rendered step set keeps every rendered desktop column within one CSS pixel. +- A board wider than the viewport retains the full drag reserve after its overflowing tracks. +- Drag-only reserve does not widen the document or show a transient native horizontal scrollbar. +- The focused scenario still restores auto-hidden destinations and moves a task into one destination. + +## Verification + +```bash +(cd apps/web && pnpm test -- --run components/kanban/adaptive-desktop-kanban.test.tsx components/kanban/kanban-grid-template.test.ts) +(cd apps/web && pnpm exec eslint components/kanban/adaptive-desktop-kanban.tsx components/kanban/adaptive-desktop-kanban.test.tsx components/kanban/kanban-grid-template.test.ts e2e/tests/kanban/auto-hide-empty-columns.spec.ts) +(cd apps/web && pnpm run typecheck && pnpm run i18n:ratchet) +(cd apps/web && pnpm e2e:run --project chromium tests/kanban/auto-hide-empty-columns.spec.ts) +``` + +## Files likely touched + +- `apps/web/components/kanban/adaptive-desktop-kanban.tsx` +- `apps/web/components/kanban/adaptive-desktop-kanban.test.tsx` +- `apps/web/components/kanban/kanban-grid-template.test.ts` +- `apps/web/e2e/tests/kanban/auto-hide-empty-columns.spec.ts` + +## Dependencies + +None. + +## Risks + +- The end reserve must remain in the scroll overflow area after it leaves the grid content box. +- The E2E assertion must allow subpixel layout values without hiding a real width change. + +## Parallelism + +`sequential` + +## Inputs + +- `docs/specs/ui/requirements/adaptive-kanban.md` +- `docs/specs/ui/system-design/adaptive-kanban.md` +- `apps/web/components/kanban/adaptive-desktop-kanban.tsx` +- `apps/web/hooks/domains/kanban/use-kanban-drag-scroll-anchor.ts` +- `apps/web/e2e/tests/kanban/auto-hide-empty-columns.spec.ts` + +## Results + +- Replaced the drag-only grid end padding with a trailing spacer after the lane grid. +- Added component coverage for the spacer and the absence of padding. +- Added Chromium geometry and overflow-track scroll-range assertions. +- Focused component tests passed with 10 tests. +- Production-build Chromium tests passed with two tests, including overflow-track scroll range and all-column width stability. +- The document remained contained while the internal drag scroll range grew, and the native scrollbar is hidden during drag. +- Scoped ESLint, web type checking, i18n ratchet, specification tests, specification lint, and diff checks passed. diff --git a/docs/specs/ui/README.md b/docs/specs/ui/README.md index 2768563c0b..4fcb72ce3e 100644 --- a/docs/specs/ui/README.md +++ b/docs/specs/ui/README.md @@ -136,6 +136,7 @@ UI owns responsive behavior; other systems own behavior/state. - [Active workspace first in settings](requirements/workspace-active-first-order.md) - [WebSocket Connectivity Warning](requirements/ws-connectivity-warning.md) ### Design +- [Adaptive Kanban](system-design/adaptive-kanban.md) - [Clarification submit feedback](system-design/clarification-submit-feedback.md) - [Dialog containment](system-design/dialog-content-containment.md) - [Descriptive select options](system-design/descriptive-select-options.md) diff --git a/docs/specs/ui/requirements/adaptive-kanban.md b/docs/specs/ui/requirements/adaptive-kanban.md index 3665c18471..aeaca96356 100644 --- a/docs/specs/ui/requirements/adaptive-kanban.md +++ b/docs/specs/ui/requirements/adaptive-kanban.md @@ -2,6 +2,7 @@ status: active system: ui created: 2026-07-27 +updated: 2026-09-05 owners: - kandev --- @@ -27,6 +28,8 @@ People use Kandev in portrait desktop windows, beside a task preview, and with a - **AC-UI-ADAPTIVE-KANBAN-001.6:** Users can reach every task by scrolling or searching. Vertical windowing does not change the column count, task order, WIP queue boundary, selection order, or card actions. - **AC-UI-ADAPTIVE-KANBAN-001.7:** A column with 440 tasks mounts fewer than 100 card bodies during initial display on desktop, tablet, and phone surfaces. - **AC-UI-ADAPTIVE-KANBAN-001.8:** Opening or resizing the task preview may change the effective desktop composition without changing the user's saved Kanban, Pipeline, workflow, or preview preferences. +- **AC-UI-ADAPTIVE-KANBAN-001.9:** When a pointer drag starts or ends without a change to the rendered workflow steps, each desktop column SHALL retain its computed width. +- **AC-UI-ADAPTIVE-KANBAN-001.10:** A desktop pointer drag MAY extend the workflow's internal scroll range for drag anchoring, but it SHALL NOT widen the document or show a transient horizontal scrollbar solely for that reserve. ## Migrated source detail @@ -100,6 +103,11 @@ desktop. snap-scrolling layout remains active and no desktop stage selector is mounted. - **GIVEN** a desktop column with 440 tasks, **WHEN** Kanban renders, **THEN** fewer than 100 card bodies are mounted and the visible cards respond to user input. +- **GIVEN** a desktop board whose rendered workflow steps do not change, **WHEN** pointer drag starts + or ends, **THEN** each column keeps its computed width. +- **GIVEN** a desktop board whose columns fit before a pointer drag, **WHEN** drag anchoring adds + internal end space, **THEN** the document width stays fixed and no transient horizontal scrollbar + appears. - **GIVEN** a phone column with 440 tasks, **WHEN** Kanban renders, **THEN** fewer than 100 card bodies are mounted inside the existing focused-column scroll area. - **GIVEN** a windowed column, **WHEN** the user scrolls from the first task to the last task, diff --git a/docs/specs/ui/system-design/adaptive-kanban.md b/docs/specs/ui/system-design/adaptive-kanban.md new file mode 100644 index 0000000000..8666af41d7 --- /dev/null +++ b/docs/specs/ui/system-design/adaptive-kanban.md @@ -0,0 +1,77 @@ +--- +status: current +system: ui +requirements: + - REQ-UI-ADAPTIVE-KANBAN-001 +created: 2026-09-05 +owners: + - kandev +--- +# Adaptive Kanban System Design + +## Purpose and boundaries + +The UI system owns the desktop Kanban grid, its contained horizontal overflow, and its drag-time geometry. + +This design covers desktop column sizing and drag scroll anchoring. It does not change workflow data, move permissions, or task persistence. + +## Requirement mapping + +| Requirement | Design sections | +| --- | --- | +| `REQ-UI-ADAPTIVE-KANBAN-001` | [Components and responsibilities](#components-and-responsibilities), [Drag control flow](#drag-control-flow), [Responsive boundaries](#responsive-boundaries) | + +## Components and responsibilities + +- `getKanbanColumnGridTemplate` defines each desktop column as `minmax(280px, 1fr)`. +- `AdaptiveDesktopKanban` owns the horizontal scroll window and the lane grid. +- `AdaptiveDesktopKanban` adds drag-only end space after the grid's overflowing tracks. +- `useKanbanDragScrollAnchor` records the source column position before drag state changes the rendered steps. +- `SwimlaneKanbanContent` derives normal steps, move-target steps, temporary steps, and the active drag state. + +The lane grid keeps `min-width: 100%`. Columns share available width until their 280px minimum requires contained horizontal overflow. + +## Drag end-space contract + +A drag can reveal auto-hidden destinations before or after the source column. The scroll window needs end space to restore the source position. + +The drag reserve uses a trailing spacer after a width-constrained lane grid. The spacer extends the scrollable area after overflowing tracks without reducing space for grid tracks. + +The lane grid must not use end padding or margin for this reserve. Padding reduces the grid content box, and margin starts at the grid border edge instead of after overflowing tracks. + +The reserve exists only while a task drag is active. The normal board has no additional end space. + +The desktop scroll window hides its native horizontal scrollbar while drag state is active. The window keeps its internal scroll range so pointer-driven and automatic drag scrolling continue to work. Document width remains unchanged. + +## Drag control flow + +1. The drag-start handler records the source step and its viewport position. +2. The handler starts the existing drag state. +3. `SwimlaneKanbanContent` adds temporary auto-hidden destinations when required. +4. `AdaptiveDesktopKanban` sizes the lane grid to the viewport or its minimum track width, then adds the end spacer after it. +5. `useKanbanDragScrollAnchor` adjusts `scrollLeft` after the rendered step key changes. +6. A drop or cancellation clears the drag state and removes the end spacer. +7. The anchor hook restores the final scroll position and then clears its saved anchor. + +If the source step no longer exists, the hook keeps the current scroll position. The authoritative task update controls the final rendered steps. + +## Responsive boundaries + +The desktop layout uses this design. The tablet layout keeps its two-column snap-scrolling surface. + +The phone layout keeps one focused column and separate touch drop targets. This correction does not change mobile composition or touch behavior. + +The existing mobile auto-hide E2E scenario covers the nearest mobile surface. It proves touch destinations and document-width containment. + +## Test strategy + +- Component tests assert that drag state uses a trailing spacer and does not use end padding. +- The Chromium Kanban E2E scenario compares every rendered desktop column before and during drag. +- The same Chromium scenario covers a board whose minimum track width exceeds the viewport and verifies the added scroll range. +- The Chromium scenario verifies document containment and the absence of a transient drag-only scrollbar. +- The same E2E scenario proves temporary destinations, cancellation, and a successful drop. +- The existing `mobile-chrome` scenario continues to prove mobile drag destinations and document-width containment. + +## Related decisions + +No architecture decision record applies. This design keeps the existing grid and drag-anchor boundaries.