summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspaces/logic/tile-layout.ts
AgeCommit message (Collapse)Author
2026-09-07feat(workspaces): bound the drag append area — 3 columns past the content edgeAdam Malczewski
Dragging a tile far right grew the wide canvas endlessly (the target cell resolved from the pointer with no upper bound), making precise placement impossible. Add a pure `dragAppendLimit(placements, geometry, extra = 3)`: the drag target may aim at most 3 columns (wide) / rows (tall) past the current span-aware content edge. The bound is measured on the COMMITTED layout — never the live preview — so it can't ratchet mid-drag; it steps forward only when a drop commits new content. cellAt applies it on the growth axis (wide: col, tall: row); the fixed cross-axis clamps are unchanged. Exported via the unit index. Verification: svelte-check 0 errors/0 warnings; vitest 1693/1693 (4 new dragAppendLimit tests); biome clean on touched files; build ok.
2026-09-07fix(workspaces): tile-grid drag bugs — stuck drop, grab-offset follow, ↵Adam Malczewski
empty-cell hover Three drag-interaction bugs on the Win8 tile home, one shared root cause (capture loss from DOM reordering) plus two wiring defects: - DROP NEVER COMMITS (stuck tile): displacePlace returned the displacement preview SORTED BY STREAM ORDER, so crossing stream positions moved the dragged tile's DOM node in the keyed {#each} and the browser dropped its pointer capture — pointerup never landed, phase stayed "dragging", the follow transform never cleared. displacePlace now preserves the INPUT render order (coordinates change only); placeAtCell re-sorts to stream order itself, so the commit's order-renumbering semantics are unchanged. - FOLLOW TRANSFORM OFFSET: the follow translate was raw pointer displacement, so a grab away from the tile's top-left made the tile jump (its ORIGIN rode the pointer). gridPointerDown records the grab offset (pointer minus the tile's rect) and followStyle cancels the tile's true grid anchor (targetCell origin), so the GRAB POINT tracks the pointer. - CAN'T HOVER EMPTY CELLS: pointer handling moved from per-tile elements to the GRID CONTAINER, which captures the pointer and is never re-parented, never pointer-events:none, never unmounted — move/up stream to it over empty cells and off-canvas, and capture can never be lost to a DOM move. Tests updated: the four order-sensitive displacePlace assertions now expect input render order (all coordinates unchanged — the plan's stream examples still hold); HomeTileGrid drag tests still pass via event bubbling. Verification: svelte-check 0 errors/0 warnings; vitest 1689/1689; biome clean on touched files; vite build succeeds. Pointer capture is browser-only — human check at the page: (1) drag a tile across columns, release — it must drop, not stay stuck; (2) grab a tile by its corner/center — the grabbed point must stay under the cursor; (3) hover/drag over empty cells past the last tile — the ghost + drop must keep tracking.
2026-09-07feat(workspaces): pinned tiles with displacement reflow + FLIP drag previewAdam Malczewski
Tiles can now be dropped onto specific cells; empty cells are real estate that persists (notes/pinned-tiles-plan.md). Data (v2): each tile carries an optional pinned anchor in a UNIVERSAL 4-row frame (tall folds its 2 columns onto frame column 1; a wide col>1 pin reads as unpinned in tall). v1 documents migrate in memory to all-unpinned (render-identical) and write back as v2 on the next change. sanitizeLayout drops garbage anchors. Packing: pinned-first (first-wins on overlap, one-ply), then unpinned tiles first-fit into the REMAINING cells — holes between pins count and never compact. displacePlace: the user-specified semantics — the dragged tile leaves a gap at its origin (nothing slides backward), anchors at the drop cell, and the contiguous filled run pushes FORWARD until the first empty cell absorbs it (X_X + Y@1 → YXX; _XZ_ + Y@2 → _YXZ). placeAtCell commits the displacement and pins every tile; pinAtFirstEmpty pins the create-flow tile at the first truly-empty slot; sortByRecentActivity now also clears pins (an explicit arrangement reset — the menu item says so). Drag UX: pointer → cell hit-math (empty cells are drop targets); a faint dot marks every cell center during a drag (CSS radial-gradient overlay, one extra pitch of end padding for the append slot); the dragged tile element itself follows the pointer (lifted via transform, pointer-events none); a dashed tone-colored ghost marks the landing footprint. The preview recomputes from the COMMITTED layout on every hover-cell change (as if the user moved straight from the original to there) while GSAP Flip animates from wherever the tiles currently are (re-entrant — mid-flight positions captured). Escape/pointercancel discards the preview and flies the tile home; a drop settles it onto its new cell. Tests: displacement (both user examples + internal moves + spans + tall stream), pinned pack (anchors, gaps kept, first-wins, universal-frame mapping, deleted-leaves-holes), v1→v2 migration + anchor sanitization, store placeAtCell/no-op write guard, Escape-cancels-drag component test. 1689 green.
2026-09-07feat(workspaces): rework tile-grid visuals — Start-screen polishAdam Malczewski
The home canvas now reads as a real Windows 8 Start screen instead of a cramped, edge-hugging list: - Geometry: the wide unit is DERIVED from the canvas height (4 rows fill it: unit=height/5 clamped to 112-184px, gap=unit/9, margin=unit/3 clamped 24-64px) — tiles are no longer tiny, and the tile field always carries a proportional outer margin (never compressed against the left edge). Tall mode keeps fluid columns with a taller 148px row + 12px gap. - Tiles take a per-workspace SATURATED tone (new pure tileTone(id): FNV-1a hash → one of the six theme-semantic colors) with tone-content text — the Win8 colored-tile identity. Starred is now a ★ badge (a tile keeps its own color), not a gray-tint override. - The tile face is the Win8 layout: small uppercase slug caption at the top, a bold span-aware title (lg → 3xl across 1×1 → 4×2), and a single-line stat row pinned to the bottom (count compacts to 'conv.' on 1×1 tiles, truncation-safe so narrow tiles never clip). - Selection is a high-contrast offset ring (visible on any tile color); tiles lift on hover; the pan arrow is a solid elevated circle. - Header gains a 'WORKSPACES' kicker; empty/loading states are centered and styled for the Start-screen canvas. Persisted data (order + spans only) and all interaction semantics (drag/FLIP, long-press, action bar, resize picker, create popover) are untouched. Tests: geometry derivation/clamping, tileTone determinism, wide-vs-compact count label — 1664 green.
2026-09-07feat(workspaces): Win8 Start-style tile-grid home pageAdam Malczewski
Rebuild the workspace home as a Windows 8 Start-screen-style tile grid, replacing the max-w-3xl vertical card list. Pure core (zero DOM/Svelte/fetch, fully unit-tested): - logic/tile-layout.ts — geometryFor(width, height) resolves WIDE (4 fixed ~120px rows, column-wise flow, 4x2 span cap) or TALL (2 fixed columns, row-wise flow, 2x2 cap); pack() is the ONLY order->coordinates transform (greedy first-fit in fill order, hole-free, render-clamped per geometry); firstEmptySlot/insertAtFirstEmptySlot/mergeLayout/resizeTile/moveTile/ sortByRecentActivity/sanitizeLayout. Persisted data is orientation-independent {version:1, tiles:[{id,colSpan,rowSpan,order}]} — order + spans only, never positions; the same data renders in either geometry (spans clamp for the render, persisted values untouched). - logic/drag-controller.ts — pure pointer/press state machine: mouse threshold-drag + tap-open; touch long-press lifts + selects, pre-lift movement cancels so vertical scrolling never fights the gesture. Shell: - tile-layout-store.svelte.ts — thin reactive wrapper persisting through the injected createLocalStore adapter (dispatch.workspaces.tiles), ops: sync/resize/move/placeFirstEmpty/sortByRecentActivity. - ui/HomeTileGrid.svelte — CSS-grid tile canvas (wide: horizontal scroll + snap + wheel pan + pan arrow + scroll indicator; tall: native vertical scroll), FLIP-animated reflow, drag ghost, transient resize preview. - ui/HomeHeader.svelte — fixed chrome: Dispatch display title, New workspace popover (same slug validation; creates via create-on-miss and does NOT navigate — the new tile lands in the first empty slot in fill order and enters rename mode), global Plugins button, sort-by-recent overflow; compact icon buttons + bottom sheet on tall screens. - ui/WorkspaceTile.svelte — tile face (title, /slug, conversation count, relative last-activity, live active dots, starred tint); inline rename with the old card's semantics. - ui/TileActionBar.svelte — slides up on select (right-click / long-press): Open, Rename, Resize, Star/Unstar, Details, Plugins, Delete (old confirm + store call preserved). - ui/ResizePicker.svelte — Word-style insert-table hover grid (wide 4x2 / tall 2x2) with arrow/Enter/Escape support and ghost preview on commit. - ui/WorkspaceDetails.svelte — flyout holding the old card's default-cwd input + default-computer select (same store calls + error states). - WorkspacesHome.svelte orchestrates; App.svelte untouched (same props). - WorkspaceCard.svelte is removed (replaced by the tile system). Manual tile order rules; starred workspaces get a tint (star is no longer a sort key); workspaces unknown to the layout append at the end. vitest: jsdom gains PointerEvent (MouseEvent-based) + Element.animate stubs — the pointer logic is the outermost edge for the drag tests. Verification: svelte-check 0/0, vitest 1655 passed (102 files, twice), biome clean, vite build OK.