Divider Span Constraints

Smart row/column constraints for dividers - enable advanced layouts like full-width headers with subdivided content

Divider Span Constraints

🎯 Overview

Span Constraints allow dividers to be scoped to specific rows or columns, enabling advanced layout patterns that were previously impossible. This is the intelligent enhancement that makes GoGrab Space's divider system truly revolutionary.

Problem Solved

Before: Dividers always spanned the entire canvas (all rows/columns).

After: Dividers can be constrained to specific rows or columns, enabling patterns like:


πŸ“ How It Works

1. Full-Canvas Dividers (Default)

When a divider has no span constraint (empty or undefined), it spans all rows/columns:

{
  id: 'divider-1',
  type: 'horizontal',
  position: 200,
  spanConstraint: undefined // βœ… Spans ALL columns
}

Visual Example:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Header         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ ← Horizontal divider (no constraint = all columns)
β”‚      Content        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Constrained Dividers (Smart Scoping)

When a divider has a span constraint, it only affects specific rows/columns:

{
  id: 'divider-2',
  type: 'vertical',
  position: 400,
  spanConstraint: [2] // βœ… Only spans row 2
}

Visual Example:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Header         β”‚ ← Row 1 (full-width, no vertical dividers here)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Content  β”‚ Sidebar  β”‚ ← Row 2 (subdivided by vertical divider constrained to row 2)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

🎨 Creating Advanced Layouts

Example 1: Full-Width Header + Subdivided Content

Goal: Header spans full width, content below is split into 2 columns.

Steps:

  1. Add horizontal divider at header height:

    • Position: 200px
    • Span Constraint: empty (spans all columns)
    • Result: Creates Row 1 (header) and Row 2 (content)
  2. Add vertical divider within content area only:

    • Position: 50% (center)
    • Span Constraint: 2 (only row 2)
    • Result: Splits Row 2 into 2 columns, Row 1 remains full-width

Resulting Grid:

grid-template-rows: 200px 1fr;
grid-template-columns: 1fr 1fr;
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Header (1,1:2)   β”‚  ← Full width (columns 1-2, row 1)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Content   β”‚  Sidebar  β”‚  ← Row 2 split (columns 1-2)
β”‚   (2,1)   β”‚   (2,2)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Example 2: 3-Column Layout with Unified Footer

Goal: Content area has 3 columns, footer spans all 3.

Steps:

  1. Add horizontal divider above footer:

    • Position: 80%
    • Span Constraint: empty
    • Result: Creates Row 1 (content) and Row 2 (footer)
  2. Add 2 vertical dividers in content area only:

    • Position 1: 33%, Span Constraint: 1
    • Position 2: 66%, Span Constraint: 1
    • Result: Splits Row 1 into 3 columns, Row 2 remains full-width

Resulting Grid:

β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”
β”‚  A  β”‚  B  β”‚  C  β”‚  ← Row 1 (3 columns)
β”œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€
β”‚     Footer      β”‚  ← Row 2 (full-width)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ–₯️ User Interface

Divider Handle UI

Each divider now shows a span constraint input in its handle:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 259px (28.1%)  [Cols: 2]  Γ— ⬍⬍⬍ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    ↑              ↑         ↑
  Position      Span     Remove
                Input

Input Format

The span input accepts flexible formats:

Input Format Meaning Example Result
(empty) All rows/columns Spans entire canvas
1 Single row/column Only affects row/column 1
1-3 Range Spans rows/columns 1, 2, and 3
1,3,5 Discrete Only rows/columns 1, 3, and 5
1-3,5,7-9 Mixed Rows/columns 1, 2, 3, 5, 7, 8, 9

Labels


πŸ’Ύ Data Model

Divider Interface

export interface Divider {
  id: string;
  type: 'horizontal' | 'vertical';
  position: number;
  positionPercent: number;
  isDragging: boolean;
  isTemporary: boolean;
  createdAt: Date;
  
  /**
   * SMART CONSTRAINT: For horizontal dividers, which row(s) to span across.
   * For vertical dividers, which column(s) to span across.
   * Examples:
   * - [1] = only in first row/column
   * - [2, 3] = spans rows/columns 2 and 3
   * - undefined = spans all rows/columns (default, full-canvas behavior)
   */
  spanConstraint?: number[];
}

Persistence

Span constraints are automatically persisted alongside divider positions:

// Saved to storage-system with flash persistence
const layoutKey = `modular-universe/layout/${universeId}/${moduleType}`;

// Data structure includes:
{
  dividers: {
    horizontal: [
      { 
        id: 'h1', 
        position: 200, 
        spanConstraint: undefined // Full-width
      }
    ],
    vertical: [
      { 
        id: 'v1', 
        position: 400, 
        spanConstraint: [2] // Only row 2
      }
    ]
  }
}

βš™οΈ Implementation Details

Component Structure

DividerToolComponent
β”œβ”€β”€ divider-handle
β”‚   β”œβ”€β”€ position-label (259px (28.1%))
β”‚   β”œβ”€β”€ span-constraint
β”‚   β”‚   β”œβ”€β”€ label ("Cols:" or "Rows:")
β”‚   β”‚   └── span-input (text field)
β”‚   β”œβ”€β”€ remove-btn (Γ—)
β”‚   └── drag-indicator (⬍⬍⬍)
└── divider-line-visual (pink line)

Event Flow

  1. User types in span input β†’ onSpanChange(value)
  2. Parse input β†’ Convert to number[] (e.g., "1-3" β†’ [1,2,3])
  3. Emit event β†’ spanChanged.emit({ id, spanConstraint })
  4. Parent updates state β†’ dividerState.update(...)
  5. Trigger layout recalculation β†’ scheduleDividerLayoutUpdate()
  6. Persist to storage β†’ saveLayoutConfiguration()

Parsing Logic

onSpanChange(value: string): void {
  const spans: number[] = [];
  const parts = value.split(',');
  
  for (const part of parts) {
    if (part.includes('-')) {
      // Range: "1-3" β†’ [1, 2, 3]
      const [start, end] = part.split('-').map(s => parseInt(s.trim(), 10));
      for (let i = start; i <= end; i++) {
        spans.push(i);
      }
    } else {
      // Single: "1" β†’ [1]
      const num = parseInt(part.trim(), 10);
      spans.push(num);
    }
  }
  
  this.spanChanged.emit({ 
    id: this.divider.id, 
    spanConstraint: spans.length > 0 ? spans : undefined 
  });
}

🎯 Use Cases

1. Dashboard with Full-Width Header

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         App Header          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Sidebar  β”‚  Main Dashboard  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Blog Layout with Sidebar

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        Blog Header          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Posts   β”‚     Sidebar      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚          Footer             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. Complex App Layout

β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”
β”‚ Nav β”‚   Top Banner   β”‚ User β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€
β”‚     β”‚ Editor β”‚ Props β”‚      β”‚
β”‚ Sidebar ┼────────┼──────── Toolsβ”‚
β”‚     β”‚ Output β”‚ Log   β”‚      β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜

πŸš€ Best Practices

1. Start with Full-Canvas Dividers

Always begin by dividing the entire canvas into major sections (rows/columns).

2. Then Add Constraints

Once major sections exist, constrain dividers to specific rows/columns for finer control.

3. Use Empty for Headers/Footers

Full-width headers and footers should use dividers with no span constraint.

4. Test Responsiveness

Span constraints work with percentage-based positioning for responsive layouts.

5. Document Complex Layouts

For intricate grids, document which dividers affect which sections.


πŸ› Troubleshooting

Issue: Divider Not Showing

Solution: Check z-index. Sections have z-index: 1, dividers have z-index: 100+.

Issue: Span Constraint Not Applying

Solution: Ensure the input format is valid (e.g., 1, 1-3, 1,3,5).

Issue: Layout Not Updating

Solution: Dividers trigger debounced updates (400ms). Wait for changes to apply.


πŸ“Š Performance


πŸ”— Related Documentation


Status: βœ… Production Ready

This feature enables GoGrab Space to create any layout imaginable - from simple full-width headers to complex nested grids. It's the secret sauce that makes our platform builder truly groundbreaking! πŸš€