Guide System V2 - Track-Based Layout Control

Technical specification for the improved divider guide system that controls only grid/flex track sizing without affecting section count

Guide System V2 - Track-Based Layout Control

Overview

The Guide System V2 is a modern, intuitive way to control canvas layout track sizing (grid rows/columns or flexbox proportions) without automatically adding or removing sections. This decouples layout structure from content organization, giving users precise control over both aspects independently.


Key Principles

1. Guides Control Track Sizing Only

2. Live, Real-Time Updates

3. Mode Parity


User Workflow

Entering Divide Mode

  1. Click "Divide" in the canvas toolbar
  2. The canvas saves a snapshot of the current layout state
  3. Existing sections remain visible and in place
  4. A divider toolbar appears at the top with controls

Adding Guides

  1. Click "Add Horizontal" to add a horizontal guide

    • Guide is placed at an optimal position (evenly distributed)
    • Immediately updates row heights or flex proportions
  2. Click "Add Vertical" (Grid mode only) to add a vertical guide

    • Guide is placed at an optimal position
    • Immediately updates column widths

Adjusting Guides

  1. Drag the guide handle to reposition

    • Handle is a modern, gradient-styled button with position label
    • Line thickness increases to 4px while dragging for clear feedback
    • Canvas tracks adjust in real-time (debounced at 60ms)
  2. Remove a guide by clicking the × button on the handle

    • Layout recalculates immediately to redistribute space

Exiting Divide Mode

  1. Done (Green button):

    • Keeps all guide changes
    • Persists layout to storage
    • Exits divide mode
  2. Restore & Exit (Red button):

    • Reverts to the snapshot taken when entering divide mode
    • Discards all guide adjustments
    • Exits divide mode

Technical Implementation

Architecture

GridCanvasComponent
  ├── dividerState: WritableSignal<DividerState>
  ├── divideModeSnapshot: LayoutConfiguration | null
  ├── applyDividerLayout(reason: string): void
  ├── applyGridTracksFromGuides(): void
  ├── applyFlexboxFromGuides(): void
  ├── getDividerStops(dividers, maxSize): number[]
  └── convertPixelListToFractions(pixelValues): string[]

DividerToolComponent
  ├── Visual rendering of guide line
  ├── Draggable handle with position display
  └── Real-time position feedback

Key Methods

applyDividerLayout(reason: string)

Main orchestrator:

applyGridTracksFromGuides(state, config, canvasSize)

Grid mode handler:

applyFlexboxFromGuides(state, config, canvasSize)

Flexbox mode handler:

getDividerStops(dividers, maxSize): number[]

Utility to get all stop positions:

convertPixelListToFractions(pixelValues): string[]

Converts absolute pixel sizes to proportional fractional units:


Visual Design

Guide Line Styling

.divider-line-visual {
  background: linear-gradient(90deg, 
    rgba(236, 72, 153, 0.8) 0%, 
    rgba(236, 72, 153, 1) 50%, 
    rgba(236, 72, 153, 0.8) 100%);
  box-shadow: 
    0 0 12px rgba(236, 72, 153, 0.6),
    inset 0 0 8px rgba(255, 255, 255, 0.3);
  
  // 3px width/height normally
  // 4px when dragging for emphasis
}

Guide Handle Styling

.divider-handle {
  background: linear-gradient(135deg, 
    rgba(236, 72, 153, 0.95) 0%, 
    rgba(236, 72, 153, 1) 100%);
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-radius: 10px;
  padding: 10px 16px;
  box-shadow: 
    0 4px 16px rgba(236, 72, 153, 0.4),
    inset 0 1px 0 rgba(255, 255, 255, 0.2);
  backdrop-filter: blur(8px);
  cursor: grab;
  
  &:active {
    cursor: grabbing;
    background: linear-gradient(135deg, 
      rgba(139, 92, 246, 1) 0%, 
      rgba(124, 58, 237, 1) 100%);
  }
}

Smooth Transitions

Both the canvas container and sections use cubic-bezier easing:

transition: 
  grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1),
  grid-template-columns 0.3s cubic-bezier(0.4, 0, 0.2, 1),
  width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
  height 0.3s cubic-bezier(0.4, 0, 0.2, 1);

Edge Cases & Considerations

1. No Guides

2. More Sections than Tracks

3. Fewer Sections than Tracks

4. Switching Modes with Guides Active

5. Minimum Edge Spacing


Comparison: V1 vs. V2

Aspect V1 (Old) V2 (New)
Guide Purpose Generate sections Control track sizing only
Section Count Auto-generated from guides User-controlled separately
Confirmation Required ("Confirm & Generate") Live updates, "Done" to keep
Restore "Cancel" exits without changes "Restore & Exit" reverts to snapshot
Visual Feedback Basic lines Modern gradients, shadows, glows
Handle Design Simple buttons Gradient badges with position labels
Mode Parity Flexbox only partially supported Full parity for Grid and Flexbox
Complexity High (too many generated sections) Low (intuitive track resizing)

Code Examples

Adding a Horizontal Guide

addHorizontalDivider(): void {
  if (this.canvasMode() !== 'divide') return;

  const canvasSize = this.getCanvasSize();
  const existing = this.dividerState().horizontal;
  const position = this.dividerManager.getOptimalPosition('horizontal', existing, canvasSize);

  const divider = this.dividerManager.addDivider('horizontal', position, canvasSize);

  this.dividerState.update(state => ({
    ...state,
    horizontal: [...state.horizontal, divider]
  }));

  this.scheduleDividerLayoutUpdate('add-horizontal');
}

Handling Guide Position Change

onDividerPositionChanged(event: DividerPositionChangeEvent): void {
  this.dividerState.update(state => {
    const horizontal = state.horizontal.map(d =>
      d.id === event.id
        ? { ...d, position: event.position, positionPercent: event.positionPercent }
        : d
    );

    const vertical = state.vertical.map(d =>
      d.id === event.id
        ? { ...d, position: event.position, positionPercent: event.positionPercent }
        : d
    );

    return { ...state, horizontal, vertical };
  });

  this.scheduleDividerLayoutUpdate('move-divider');
}

Applying Grid Tracks

private applyGridTracksFromGuides(
  state: DividerState,
  config: LayoutConfiguration,
  canvasSize: { width: number; height: number }
): void {
  const rowStops = this.getDividerStops(state.horizontal, canvasSize.height);
  const colStops = this.getDividerStops(state.vertical, canvasSize.width);

  const rows = rowStops.slice(1).map((stop, i) => `${stop - rowStops[i]}px`);
  const columns = colStops.slice(1).map((stop, i) => `${stop - colStops[i]}px`);

  config.grid = {
    ...(config.grid ?? {}),
    rows: this.convertPixelListToFractions(rows),
    columns: this.convertPixelListToFractions(columns),
    gap: config.grid?.gap ?? '16px'
  };

  // Ensure sections have grid placement
  config.sections.forEach((section, index) => {
    if (!section.grid) {
      const numCols = columns.length;
      section.grid = {
        row: Math.floor(index / numCols) + 1,
        col: (index % numCols) + 1,
        rowSpan: 1,
        colSpan: 1
      };
    }
    section.flexbox = undefined;
    section.updatedAt = new Date();
  });
}

Best Practices

For Users

  1. Start with sections first: Add the number of sections you want via "Add Section"
  2. Then adjust spacing: Enter Divide mode and add guides to control proportions
  3. Drag guides freely: Live feedback shows immediate results
  4. Remove guides easily: Click the × on any handle
  5. Cancel if needed: Use "Restore & Exit" to undo all guide changes

For Developers

  1. Debounce updates: Use scheduleDividerLayoutUpdate() to avoid excessive re-renders
  2. Always update sections immutably: Use structuredClone() for deep copies
  3. Call cdr.markForCheck(): Ensure Angular's OnPush strategy detects changes
  4. Preserve section count: Never add/remove sections in applyDividerLayout()
  5. Handle missing grid placement: Auto-assign sensible defaults if section.grid is undefined

Future Enhancements


Related Documentation


Changelog

V2 (November 20, 2025)

V1 (November 18, 2025)