npm package: @ngx-km/grid
Description: Interactive infinite canvas grid component for Angular 19+.
Latest version: 0.0.3

# @ngx-km/grid

Interactive infinite canvas grid component for Angular 19+.

## Features

- **Infinite canvas** with smooth pan and zoom
- **Render any Angular component** at specific coordinates
- **Automatic dimension tracking** with ResizeObserver
- **Element dragging** with optional grid snapping
- **Configurable backgrounds** (lines, dots, or none)
- **Floating toolbar** with built-in zoom controls

## Installation

```bash
npm install @ngx-km/grid
```

## Quick Start

```typescript
import { Component } from '@angular/core';
import { GridComponent, GridConfig } from '@ngx-km/grid';

@Component({
  selector: 'app-example',
  standalone: true,
  imports: [GridComponent],
  template: `
    <div style="width: 100%; height: 500px;">
      <ngx-grid [config]="gridConfig" />
    </div>
  `
})
export class ExampleComponent {
  gridConfig: GridConfig = {
    dimensionMode: 'full',
    backgroundMode: 'lines',
    cellSize: 20,
  };
}
```

## API Reference

### GridComponent

**Selector:** `ngx-grid`

#### Inputs

| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `config` | `GridConfig` | See defaults | Grid configuration |
| `elements` | `GridElement[]` | `[]` | Elements to render on the grid |

#### Outputs

| Output | Type | Description |
|--------|------|-------------|
| `viewportChange` | `ViewportState` | Emitted when pan/zoom changes |
| `elementsRendered` | `RenderedElement[]` | Emitted when element dimensions are measured |
| `elementPositionChange` | `ElementPositionChange` | Emitted when an element is dragged |

#### Public Methods

| Method | Description |
|--------|-------------|
| `resetViewport()` | Reset pan and zoom to origin |
| `zoomIn(factor?)` | Zoom in by factor (default 1.25x) |
| `zoomOut(factor?)` | Zoom out by factor (default 1.25x) |

### GridConfig

```typescript
interface GridConfig {
  // Dimension settings
  dimensionMode: 'fixed' | 'full';
  width?: number;           // For fixed mode
  height?: number;          // For fixed mode
  minWidth?: number;
  minHeight?: number;
  maxWidth?: number;
  maxHeight?: number;

  // Background settings
  backgroundMode?: 'lines' | 'dots' | 'none';
  cellSize?: number;        // Default: 20
  backgroundPatternColor?: string;
  backgroundPatternOpacity?: number;
  backgroundColor?: string;

  // Pan settings
  panEnabled?: boolean;     // Default: true
  panMinX?: number;         // Infinite if not set
  panMaxX?: number;
  panMinY?: number;
  panMaxY?: number;

  // Zoom settings
  zoomEnabled?: boolean;    // Default: true
  zoomMin?: number;         // Default: 0.1
  zoomMax?: number;         // Default: 5
  zoomSpeed?: number;       // Default: 0.001

  // Drag settings
  dragEnabled?: boolean;    // Default: true
  snapEnabled?: boolean;    // Default: false
  snapGridSize?: number;    // Default: uses cellSize

  // Toolbar settings
  toolbar?: GridToolbarConfig;
}
```

### GridElement

```typescript
interface GridElement<T = unknown> {
  id: string;                    // Unique identifier
  component: Type<unknown>;      // Angular component to render
  data?: T;                      // Data passed to component
  x: number;                     // X position in world space
  y: number;                     // Y position in world space
}
```

## Usage Examples

### Rendering Custom Components

Create a node component that receives data via the `GRID_ELEMENT_DATA` token:

```typescript
import { Component, inject } from '@angular/core';
import { GRID_ELEMENT_DATA } from '@ngx-km/grid';

interface NodeData {
  title: string;
  color: string;
}

@Component({
  selector: 'app-node',
  standalone: true,
  template: `
    <div class="node" [style.background]="data.color">
      {{ data.title }}
    </div>
  `,
  styles: [`
    .node {
      padding: 16px 24px;
      border-radius: 8px;
      color: white;
      font-weight: bold;
    }
  `]
})
export class NodeComponent {
  data = inject<NodeData>(GRID_ELEMENT_DATA);
}
```

Use it in your grid:

```typescript
import { Component, signal } from '@angular/core';
import { GridComponent, GridConfig, GridElement } from '@ngx-km/grid';
import { NodeComponent, NodeData } from './node.component';

@Component({
  selector: 'app-graph',
  standalone: true,
  imports: [GridComponent],
  template: `
    <ngx-grid
      [config]="config"
      [elements]="elements()"
      (elementPositionChange)="onDrag($event)" />
  `
})
export class GraphComponent {
  config: GridConfig = {
    dimensionMode: 'full',
    backgroundMode: 'dots',
    dragEnabled: true,
    snapEnabled: true,
    snapGridSize: 20,
  };

  elements = signal<GridElement<NodeData>[]>([
    {
      id: 'node-1',
      component: NodeComponent,
      data: { title: 'Start', color: '#22c55e' },
      x: 100,
      y: 100,
    },
    {
      id: 'node-2',
      component: NodeComponent,
      data: { title: 'End', color: '#ef4444' },
      x: 300,
      y: 200,
    },
  ]);

  onDrag(event: ElementPositionChange) {
    console.log(`Element ${event.id} moved to (${event.x}, ${event.y})`);
  }
}
```

### Adding a Toolbar

```typescript
const config: GridConfig = {
  dimensionMode: 'full',
  toolbar: {
    position: 'top-right',
    showZoomControls: true,
    showResetView: true,
    customButtons: [
      {
        id: 'add-node',
        label: 'Add Node',
        icon: '<svg>...</svg>',
        onClick: () => this.addNode(),
      },
      {
        id: 'toggle-snap',
        label: 'Toggle Snap',
        active: this.snapEnabled(),
        onClick: () => this.toggleSnap(),
      },
    ],
  },
};
```

### Tracking Element Dimensions

```typescript
@Component({
  template: `
    <ngx-grid
      [config]="config"
      [elements]="elements()"
      (elementsRendered)="onElementsRendered($event)" />
  `
})
export class MyComponent {
  onElementsRendered(elements: RenderedElement[]) {
    elements.forEach(el => {
      console.log(`${el.id}: ${el.width}x${el.height} at (${el.x}, ${el.y})`);
    });
  }
}
```

### Controlling Viewport Programmatically

```typescript
@Component({
  template: `
    <ngx-grid #grid [config]="config" />
    <button (click)="grid.resetViewport()">Reset</button>
    <button (click)="grid.zoomIn()">Zoom In</button>
    <button (click)="grid.zoomOut()">Zoom Out</button>
  `
})
export class MyComponent {}
```

## Exports

### Components
- `GridComponent` - Main grid component
- `ToolbarComponent` - Standalone toolbar (used internally)

### Types
- `GridConfig` - Grid configuration interface
- `GridToolbarConfig` - Toolbar configuration
- `GridElement<T>` - Element definition
- `RenderedElement` - Element with tracked dimensions
- `ElementPositionChange` - Drag event data
- `ViewportState` - Pan/zoom state
- `DimensionMode` - 'fixed' | 'full'
- `BackgroundMode` - 'lines' | 'dots' | 'none'
- `ToolbarPosition` - Toolbar position options
- `ToolbarButton` - Button configuration
- `ToolbarConfig` - Full toolbar config

### Constants
- `DEFAULT_GRID_CONFIG` - Default configuration
- `DEFAULT_VIEWPORT_STATE` - Default viewport
- `GRID_ELEMENT_DATA` - Injection token for element data
- `DEFAULT_CELL_SIZE`, `DEFAULT_ZOOM_MIN`, etc.

## Running Tests

```bash
nx test ngx-grid
```

## License

MIT