Skip to content

feat: replace hard-to-grab resize tabs with card edge/corner resizing#70

Merged
Demonstrandum merged 4 commits into
masterfrom
cursor/card-resizing-usability-8e95
Apr 1, 2026
Merged

feat: replace hard-to-grab resize tabs with card edge/corner resizing#70
Demonstrandum merged 4 commits into
masterfrom
cursor/card-resizing-usability-8e95

Conversation

@Demonstrandum

@Demonstrandum Demonstrandum commented Mar 31, 2026

Copy link
Copy Markdown
Owner

Motivation for features / changes

The small resize tabs (browser-native CSS resize: vertical handles) in the bottom-right corner of chart containers are very hard to grab, especially on smaller cards. Users have to precisely target a tiny area to resize cards. This PR replaces that mechanism with full edge-based and corner-based resizing directly on the card wrapper, making it much easier to adjust card dimensions.

Technical description of changes

New: CardEdgeResizeDirective (edge_resize_directive.ts)

  • Detects mouse proximity to card edges within an 8px zone
  • Bottom edge: drag to resize card height smoothly in pixels (minimum 200px)
  • Right edge: drag to resize width smoothly in pixels during drag; on release, snaps to the nearest grid column span (span 1, 2, 3, ..., or full-width 1/-1)
  • Corner (bottom-right): both height and width simultaneously
  • On drag start for width: sets explicit pixel width, clears grid-column, raises z-index so the card overlaps neighbors during the smooth drag
  • On release: computes nearest column span from final pixel width, removes pixel overrides, applies grid-column: span N
  • Double-click bottom edge resets height; double-click right edge resets column span to 1
  • Both height and column span are persisted to localStorage (_tb_card_sizes.v1) and restored on init
  • Runs event listeners outside Angular zone for performance
  • Uses const enum ResizeEdge with bitwise operations

Visual indicators

  • Subtle ::after pseudo-elements on .card-space and .card-wrapper show:
    • A 3px highlight bar on the bottom or right edge when hovering near them
    • A 12px triangle indicator in the corner when near the corner
  • edge-resizing-width class adds a subtle shadow during width drag for visual feedback
  • Indicators fade in with CSS transitions

Removed: CSS resize: vertical and persistResize

  • Removed resize: vertical from .chart-container in both scalar and superimposed card SCSS
  • Removed now-unused [persistResize] from chart containers (data table containers retain theirs)
  • These were the hard-to-grab native browser resize tabs

Integration

  • [cardEdgeResize] directive applied to .card-space in card_grid_component.ng.html (regular cards)
  • [cardEdgeResize] directive applied to .card-wrapper in superimposed_cards_view_component.ts (superimposed cards)
  • EdgeResizeModule imported in MainViewModule
  • columnSpanChanged event handled in both CardGridComponent and SuperimposedCardsViewComponent
  • Bazel BUILD updated for both widgets and main_view targets

Screenshots of UI changes (or N/A)

N/A (cursor-based interaction — smooth pixel resize during drag, grid-snap on release, subtle edge highlights)

Detailed steps to verify changes work correctly (as executed by you)

  1. Open the time series dashboard with multiple scalar cards
  2. Hover near the bottom edge of a card — cursor changes to ns-resize, subtle bar appears
  3. Drag the bottom edge down/up — card height changes smoothly (pixel by pixel)
  4. Release — new height is persisted (survives page reload)
  5. Double-click the bottom edge — height resets to default
  6. Hover near the right edge — cursor changes to ew-resize
  7. Drag the right edge right — card width grows smoothly following the cursor, with a subtle shadow
  8. Release — card snaps to nearest grid column span (clean grid alignment)
  9. Drag right edge left — card shrinks smoothly, snaps to smaller span on release
  10. Double-click right edge — span resets to 1
  11. Hover near the bottom-right corner — cursor changes to nwse-resize
  12. Drag the corner — height and width adjust simultaneously; width snaps on release
  13. Repeat for superimposed cards — same behavior applies
  14. Full lint passes (yarn lint)

Alternate designs / implementations considered (or N/A)

  • Binary full-width toggle: Initial implementation just toggled full-width on/off. Too jarring — no intermediate sizes.
  • Integer column-span snapping during drag: Snapped to span N during drag. Still jumpy — columns are 335+ px wide, so changes are coarse.
  • Smooth pixel width during drag + grid-snap on release (chosen): Best of both worlds — smooth real-time feedback while dragging, clean grid-aligned result on release.
Open in Web Open in Cursor 

cursoragent and others added 2 commits March 31, 2026 15:57
Replace the tiny CSS resize: vertical browser-native grab handles on
chart containers with a proper edge/corner resize system at the card level.

- New CardEdgeResizeDirective handles mousedown/move/up on card edges
  (bottom: height, right: full-width toggle, corner: both)
- Visual edge indicators appear on hover showing resize affordances
- Double-click bottom edge resets height; double-click right toggles width
- Card height is persisted to localStorage (_tb_card_sizes.v1)
- Applied to both regular cards and superimposed cards
- Removed CSS resize: vertical from scalar and superimposed chart containers

Co-authored-by: Samuel <samuel@knutsen.co>
Since resize: vertical was removed from chart containers (now handled
by the card-level edge resize directive), the persistResize directives
on chart containers are no longer needed. Data table persistResize
directives are preserved as those still have resize: vertical.

Co-authored-by: Samuel <samuel@knutsen.co>
@github-actions

github-actions Bot commented Mar 31, 2026

Copy link
Copy Markdown

Preview Deployment

Status ⏳ Build Timeout
Live Preview https://Demonstrandum-tensorbored-pr-70.hf.space
Space https://huggingface.co/spaces/Demonstrandum/tensorbored-pr-70
Details
  • Wheel: tensorbored_nightly-2.21.0a20260331-py3-none-any.whl
  • Commit: 90a7ffc
  • Build status: timeout

cursoragent and others added 2 commits March 31, 2026 17:00
Replace the binary full-width toggle with smooth grid-column span
resizing. Dragging the right edge of a card now smoothly snaps between
grid column spans (span 1, span 2, span 3, ..., full-width).

- Directive reads grid column count, width, and gap from the parent
  grid element to compute the target span during drag
- Column span is applied via inline grid-column style (span N or 1/-1)
- Both height and column span are persisted to localStorage
- Double-click right edge resets span to 1
- Dragging to max columns auto-sets full-width (1/-1)
- Fix prettier formatting on all changed files

Co-authored-by: Samuel <samuel@knutsen.co>
During right-edge drag, the card now resizes smoothly in real pixels
(following the cursor exactly) instead of jumping between column spans.
On mouse-up, it snaps to the nearest grid column span for clean alignment.

- On drag start: set explicit pixel width, clear grid-column, raise
  z-index so the card overlaps neighbors during the drag
- During drag: update width in pixels for every mousemove
- On release: compute nearest column span from the final width, clear
  pixel overrides, apply grid-column span
- Added edge-resizing-width class with subtle shadow during drag
- Min width clamped to 200px, max width clamped to full grid width

Co-authored-by: Samuel <samuel@knutsen.co>
@Demonstrandum Demonstrandum marked this pull request as ready for review April 1, 2026 16:08
@Demonstrandum Demonstrandum merged commit c6a174b into master Apr 1, 2026
13 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Restore path bypasses column count clamping logic
    • Changed the ngOnInit restore path to call snapshotGrid() then applyColSpan(saved.colSpan) so the span is clamped to the current grid column count instead of being set directly.
  • ✅ Fixed: Inline gridColumn style overrides full-width CSS class permanently
    • Added ngDoCheck lifecycle hook that clears the inline gridColumn style whenever the element has the full-width CSS class and is not mid-drag, allowing the CSS class rule to take effect.
  • ✅ Fixed: Field startColSpan is assigned but never read
    • Removed the unused startColSpan field declaration and its sole assignment in handleDown.

Create PR

Or push these changes by commenting:

@cursor push bd234634ff
Preview (bd234634ff)
diff --git a/tensorbored/webapp/widgets/edge_resize_directive.ts b/tensorbored/webapp/widgets/edge_resize_directive.ts
--- a/tensorbored/webapp/widgets/edge_resize_directive.ts
+++ b/tensorbored/webapp/widgets/edge_resize_directive.ts
@@ -14,6 +14,7 @@
 ==============================================================================*/
 import {
   Directive,
+  DoCheck,
   ElementRef,
   EventEmitter,
   Input,
@@ -98,7 +99,7 @@
   standalone: false,
   selector: '[cardEdgeResize]',
 })
-export class CardEdgeResizeDirective implements OnInit, OnDestroy {
+export class CardEdgeResizeDirective implements OnInit, OnDestroy, DoCheck {
   @Input('cardEdgeResize') persistKey = '';
   @Output() columnSpanChanged = new EventEmitter<number>();
 
@@ -108,7 +109,6 @@
   private startY = 0;
   private startHeight = 0;
   private startWidth = 0;
-  private startColSpan = 1;
   private gridColWidth = 0;
   private gridGap = 0;
   private gridTotalCols = 1;
@@ -138,7 +138,8 @@
         this.el.style.height = `${saved.height}px`;
       }
       if (saved?.colSpan && saved.colSpan > 1) {
-        this.el.style.gridColumn = `span ${saved.colSpan}`;
+        this.snapshotGrid();
+        this.applyColSpan(saved.colSpan);
       }
     }
     this.zone.runOutsideAngular(() => {
@@ -149,6 +150,13 @@
     });
   }
 
+  ngDoCheck() {
+    if (this.dragging) return;
+    if (this.el.classList.contains('full-width') && this.el.style.gridColumn) {
+      this.el.style.gridColumn = '';
+    }
+  }
+
   ngOnDestroy() {
     this.el.removeEventListener('mousemove', this.onHover);
     this.el.removeEventListener('mousedown', this.onDown);
@@ -251,7 +259,6 @@
     const rect = this.el.getBoundingClientRect();
     this.startHeight = rect.height;
     this.startWidth = rect.width;
-    this.startColSpan = this.getCurrentColSpan();
     this.snapshotGrid();
 
     if (edge & ResizeEdge.RIGHT) {

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

}
if (saved?.colSpan && saved.colSpan > 1) {
this.el.style.gridColumn = `span ${saved.colSpan}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restore path bypasses column count clamping logic

Medium Severity

The ngOnInit restore path sets gridColumn to span ${saved.colSpan} directly, bypassing applyColSpan which clamps the span to the current grid column count (using '1 / -1' when span >= total columns). If a user saves a span of 4 on a wide screen then reopens on a narrower screen with 2 columns, span 4 creates implicit grid columns, breaking the layout with overflow or misaligned cards.

Additional Locations (1)
Fix in Cursor Fix in Web

} else {
this.el.style.gridColumn = `span ${span}`;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline gridColumn style overrides full-width CSS class permanently

Medium Severity

After an edge resize to span > 1, the directive leaves an inline gridColumn style on the element. This permanently overrides the .full-width CSS class (grid-column-start: 1; grid-column-end: -1), breaking the full-size toggle for histogram cards (via cardStateMap.fullWidth) and image cards (via cardsAtFullWidth). The toggle dispatches to the store and adds the CSS class, but the inline style silently wins.

Additional Locations (1)
Fix in Cursor Fix in Web

private startY = 0;
private startHeight = 0;
private startWidth = 0;
private startColSpan = 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field startColSpan is assigned but never read

Low Severity

startColSpan is declared at line 111 and assigned in handleDown at line 254 via getCurrentColSpan(), but is never read anywhere in the directive. This is dead code that adds confusion about whether span-relative logic was intended but forgotten.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants