npm package: @ogidor/dashboard
Description: A content-agnostic drag-and-drop dashboard library for Angular 15. Multi-workspace, persistent layout, pop-out windows, live cross-tab sync.
Latest version: 1.0.19

# @ogidor/dashboard

A lightweight, content-agnostic drag-and-drop dashboard library for Angular 15.
Drop it into any project, supply your own card content, and get a fully featured
multi-workspace grid with pluggable persistence, pop-out windows, and live cross-tab
sync — out of the box.

---

## Table of Contents

1. [Features](#features)
2. [Installation](#installation)
3. [Quick Start](#quick-start)
4. [API Reference](#api-reference)
5. [Models](#models)
6. [Theming](#theming)
7. [Labels & i18n](#labels--i18n)
8. [Pop-out Windows](#pop-out-windows)
9. [Persisting Layouts](#persisting-layouts)
10. [Full Integration Example](#full-integration-example)
11. [Development](#development)
12. [License](#license)

---

## Features

- **Drag & Drop Grid** — smooth cursor-following drag with collision resolution and auto-compaction.
- **Live Resize** — bottom-right resize handle; surrounding cards shift out of the way in real time.
- **Multi-Workspace** — unlimited tabbed workspaces, each with its own independent layout.
- **Independent Pop-out Layouts** — pop a workspace into its own window; dragging there never affects the main window.
- **Structural Sync** — adding, removing, or renaming a widget in any window instantly appears in all open windows via `BroadcastChannel`.
- **Pluggable Persistence** — localStorage by default, or inject your own DB/API adapter.
- **Content-Agnostic Cards** — you own the card body; stamp any Angular component, chart, or markup inside via `ng-template`.
- **Fully Themeable** — 36 colour tokens plus font family, controllable via a typed `DashboardTheme` input or CSS custom properties.
- **Localizable / Customizable Text** — every UI string (button labels, tooltips, prompts, dialog messages) is overridable via a typed `DashboardLabels` input.

---

## Installation

```bash
npm install @ogidor/dashboard line-awesome
```

Add Line Awesome to your global styles (`styles.scss` or `angular.json`):

```scss
@import "line-awesome/dist/line-awesome/css/line-awesome.css";
```

---

## Quick Start

### 1. Import `DashboardModule`

```typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { DashboardModule } from '@ogidor/dashboard';

@NgModule({
  imports: [BrowserModule, DashboardModule],
  bootstrap: [AppComponent],
})
export class AppModule {}
```

### 2. Add the component to your template

`app-dashboard` fills whatever container you place it in — **you control the size**. Give its parent a defined height and the dashboard will fill it completely.

```html
<!-- Full-page example -->
<div style="width: 100vw; height: 100vh;">
  <app-dashboard ...></app-dashboard>
</div>

<!-- Embedded in a layout example -->
<div class="dashboard-container">
  <app-dashboard ...></app-dashboard>
</div>
```

```css
/* your global stylesheet */
.dashboard-container {
  width: 100%;
  height: calc(100vh - 60px); /* e.g. subtract a top nav */
}
```

```html
<app-dashboard
  (addWidgetRequested)="onAddWidget()"
  (editWidgetRequested)="onEditWidget($event)"
>
  <!-- Stamp your own content inside every card -->
  <ng-template gridCell let-widget="widget">
    <div style="padding: 8px; color: white;">
      {{ widget.data | json }}
    </div>
  </ng-template>
</app-dashboard>
```

### 3. Add and edit widgets programmatically

```typescript
import { DashboardStateService, Widget } from '@ogidor/dashboard';

export class AppComponent {
  constructor(private dashboard: DashboardStateService) {}

  onAddWidget() {
    this.dashboard.addWidget({
      title: 'Revenue',
      cols: 6,
      rows: 4,
      cardColor: '#1a1a2e', // optional — overrides the global card color
      data: { metric: 'revenue' },
    });
  }

  onEditWidget(widget: Widget) {
    // open your own dialog, then patch:
    this.dashboard.updateWidget(widget.id, {
      title: 'Updated Title',
      data: { metric: 'users' },
    });
  }
}
```

---

## API Reference

### `app-dashboard`

The main component. Handles tabs, the Add Widget button, and the grid.

#### Inputs

| Input | Type | Description |
|---|---|---|
| `theme` | `DashboardTheme` | Override any colour token or font. All fields optional. |
| `labels` | `DashboardLabels` | Override any UI string (button labels, tooltips, prompts). All fields optional. |
| `initialLayout` | `string` | JSON from a previous `serializeLayout()` call. Restores the full layout on load. |

#### Outputs

| Output | Payload | Description |
|---|---|---|
| `addWidgetRequested` | `void` | Fires when the user clicks **Add Widget**. Open your own dialog here. |
| `editWidgetRequested` | `Widget` | Fires when the user clicks the edit icon on a card. |

#### Content children

| Selector | Description |
|---|---|
| `ng-template[gridCell]` | Rendered inside every card body. Template context exposes `widget: Widget`. |

#### Methods (via `@ViewChild`)

```typescript
@ViewChild(DashboardComponent) dash!: DashboardComponent;

// Returns the current layout as a JSON string
const json = this.dash.serializeLayout();
```

---

### `DashboardStateService`

Provided at root. Inject it anywhere for full programmatic control.

```typescript
constructor(private state: DashboardStateService) {}
```

#### Methods

| Method | Signature | Description |
|---|---|---|
| `addWidget` | `(widget: Partial<Widget> & { title: string }) => Widget` | Add a widget to the active workspace. Returns the new widget with its generated `id`. |
| `updateWidget` | `(id: string, patch: Partial<Omit<Widget, 'id'&#124;'x'&#124;'y'&#124;'cols'&#124;'rows'>>) => void` | Update title, cardColor, or data. Use `updateWidgetPosition` for positional fields. |
| `removeWidget` | `(id: string) => void` | Remove a widget from the active workspace. |
| `updateWidgetPosition` | `(pageId, widgetId, x, y, cols, rows) => void` | Manually reposition or resize a widget. |
| `getActivePage` | `() => Page \| undefined` | Returns the currently active `Page` object. |
| `addPage` | `(name: string) => void` | Add a new workspace tab. |
| `removePage` | `(id: string) => void` | Remove a workspace tab (at least one is always kept). |
| `setActivePage` | `(id: string) => void` | Switch the active workspace by id. |
| `popOutPage` | `(id: string) => void` | Open a workspace in a separate browser window. |
| `loadLayout` | `(config: DashboardConfig) => void` | Load a full layout object (parsed from `serializeLayout`). |
| `serializeLayout` | `() => string` | Serialize the full layout to a JSON string. |

#### Observables

| Observable | Type | Description |
|---|---|---|
| `pages$` | `Observable<Page[]>` | Emits whenever the page list changes. |
| `activePageId$` | `Observable<string>` | Emits whenever the active workspace changes. |

---

### `app-grid` (Advanced)

The raw grid engine, exported for advanced use cases. `app-dashboard` uses this internally.

```html
<app-grid
  [widgets]="myWidgets"
  [columns]="12"
  [gap]="16"
  [rowHeight]="80"
  (itemChanged)="onItemChanged($event)"
  (layoutChanged)="onLayoutChanged($event)"
>
  <ng-template gridCell let-widget="widget">
    <!-- your card here -->
  </ng-template>
</app-grid>
```

#### Inputs

| Input | Type | Default | Description |
|---|---|---|---|
| `widgets` | `Widget[]` | `[]` | The widgets to render. |
| `columns` | `number` | `12` | Number of grid columns. |
| `gap` | `number` | `16` | Gap between cells in pixels. |
| `rowHeight` | `number` | `80` | Height of one grid row in pixels. |
| `minItemCols` | `number` | `1` | Minimum column span during resize. |
| `minItemRows` | `number` | `1` | Minimum row span during resize. |

#### Outputs

| Output | Payload | Description |
|---|---|---|
| `itemChanged` | `Widget` | Fires after a single widget is moved or resized. |
| `layoutChanged` | `Widget[]` | Fires after any move or resize with the full updated list. |

---

### Public Exports

```typescript
export { DashboardModule }          from '@ogidor/dashboard';
export { DashboardComponent }       from '@ogidor/dashboard';
export { WidgetRendererComponent }  from '@ogidor/dashboard';
export { CustomGridComponent }      from '@ogidor/dashboard';
export { GridCellDirective }        from '@ogidor/dashboard';
export { DashboardStateService }    from '@ogidor/dashboard';
export {
  DashboardPersistence,
  LocalStorageDashboardPersistence,
  DASHBOARD_PERSISTENCE,
  DASHBOARD_PERSISTENCE_CONTEXT,
  PositionMap,
} from '@ogidor/dashboard';
export { Widget, Page, DashboardConfig, DashboardTheme, DashboardLabels } from '@ogidor/dashboard';
```

---

## Models

### `Widget`

```typescript
export interface Widget {
  id: string;          // auto-generated — do not set manually
  x: number;           // grid column, 0-based
  y: number;           // grid row, 0-based
  cols: number;        // column span (default: 4)
  rows: number;        // row span (default: 3)
  title: string;       // shown in the card header
  cardColor?: string;  // per-card background — overrides the global widgetCardColor
  data?: any;          // custom payload — stored and synced, never inspected
}
```

### `Page`

```typescript
export interface Page {
  id: string;
  name: string;
  widgets: Widget[];
}
```

### `DashboardConfig`

```typescript
export interface DashboardConfig {
  pages: Page[];
  activePageId: string;
}
```

### `DashboardTheme`

```typescript
export interface DashboardTheme {
  // ── Layout ──
  backgroundColor?:          string;  // outer wrapper              default: #000000
  panelColor?:               string;  // main panel bg              default: #1c1c1e
  panelBorderColor?:         string;  // panel & pop-out border     default: rgba(255,255,255,0.05)
  widgetCardColor?:          string;  // card background            default: #2c2c2e
  foreColor?:                string;  // text / labels              default: #8e8e93
  accentColor?:              string;  // active states, buttons     default: #0a84ff
  accentTintColor?:          string;  // light accent hover bg      default: rgba(10,132,255,0.15)
  accentTintStrongColor?:    string;  // strong accent active bg    default: rgba(10,132,255,0.25)
  dangerColor?:              string;  // destructive actions        default: #ff453a
  dangerTintColor?:          string;  // light danger hover bg      default: rgba(255,69,58,0.22)
  fontFamily?:               string;  // global font stack          default: system-ui

  // ── Header & Tabs ──
  tabsContainerColor?:       string;  // pill container bg          default: rgba(44,44,46,0.6)
  tabActiveColor?:           string;  // active tab bg              default: #3a3a3c
  tabActiveTextColor?:       string;  // active tab text            default: #ffffff
  tabHoverTextColor?:        string;  // hovered tab text           default: #e5e5ea
  tabHoverBgColor?:          string;  // hovered tab bg             default: rgba(255,255,255,0.05)
  addWidgetButtonTextColor?: string;  // Add Widget label           default: #ffffff
  addWidgetButtonIconColor?: string;  // Add Widget icon colour      default: #ffffff
  addWidgetButtonBgColor?:   string;  // Add Widget button bg        default: #0a84ff
  tabActiveIconColor?:       string;  // active tab action icons     default: #ffffff
  tabInactiveIconColor?:     string;  // inactive tab action icons   default: #8e8e93
  overflowBtnBgColor?:       string;  // "+N more" button bg        default: rgba(255,255,255,0.08)
  overflowBtnBorderColor?:   string;  // "+N more" button border    default: rgba(255,255,255,0.12)

  // ── Tab-switcher overlay ──
  overlayBgColor?:           string;  // dimmed backdrop            default: rgba(0,0,0,0.55)
  sheetBorderColor?:         string;  // sheet border & dividers    default: rgba(255,255,255,0.08)
  scrollbarColor?:           string;  // scrollbar thumb            default: rgba(255,255,255,0.15)
  switcherTitleColor?:       string;  // dialog "N Workspaces" text default: #ffffff
  switcherCardTextColor?:    string;  // dialog card name text      default: #ffffff

  // ── Pop-out Window ──
  popoutTitleColor?:         string;  // pop-out title text         default: #ffffff

  // ── Widget Card ──
  widgetTitleColor?:         string;  // card title                 default: #ffffff
  dragHandleColor?:          string;  // drag dots                  default: rgba(255,255,255,0.2)
  widgetBorderColor?:        string;  // card border                default: rgba(255,255,255,0.07)
  widgetButtonBgColor?:      string;  // icon button bg             default: rgba(255,255,255,0.07)
  widgetButtonColor?:        string;  // icon button colour         default: rgba(255,255,255,0.5)

  // ── Grid ──
  placeholderColor?:         string;  // drag ghost overlay         default: #0a84ff
  resizeHandleColor?:        string;  // resize icon                default: rgba(255,255,255,0.25)
}
```

> **Per-card colour:** set `cardColor` on any `Widget` to override `widgetCardColor` for just that one card.

### `DashboardLabels`

```typescript
export interface DashboardLabels {
  addWidgetButton?: string;          // "Add Widget"        — visible button text
  addWidgetButtonTitle?: string;     // "Add widget"        — button tooltip
  addPageButtonTitle?: string;       // "New workspace"     — + tab button tooltip
  overflowButtonTitle?: string;      // "Show all workspaces" — overflow +N tooltip
  popoutButtonTitle?: string;        // "Open in new window" — pop-out icon tooltip
  closeTabButtonTitle?: string;      // "Close"             — × tab button tooltip
  workspaceSingular?: string;        // "Workspace"         — tab-switcher title (1 item)
  workspacePlural?: string;          // "Workspaces"        — tab-switcher title (N items)
  newWorkspaceButton?: string;       // "New Workspace"     — switcher footer button text
  addWorkspacePromptLabel?: string;  // "Workspace name:"   — browser prompt label
  defaultWorkspaceName?: string;     // "Workspace"         — prompt placeholder prefix
  removeWorkspaceConfirm?: string;   // "Remove this workspace?" — browser confirm dialog
  dragToMoveTitle?: string;          // "Drag to move"      — drag handle tooltip
  editButtonTitle?: string;          // "Edit"              — widget edit icon tooltip
  removeButtonTitle?: string;        // "Remove"            — widget remove icon tooltip
}
```

---

## Theming

### Theme Input

Pass a partial `DashboardTheme` to `[theme]`. Only provided fields are overridden.

```typescript
theme: DashboardTheme = {
  accentColor:              '#6c63ff',
  accentTintColor:          'rgba(108,99,255,0.15)',
  accentTintStrongColor:    'rgba(108,99,255,0.25)',
  tabActiveColor:           '#2a2a38',
  tabActiveTextColor:       '#ffffff',
  tabActiveIconColor:       '#ffffff',   // icons on the active tab
  tabInactiveIconColor:     '#6e6e73',   // icons on inactive tabs
  addWidgetButtonBgColor:   '#6c63ff',   // button background
  addWidgetButtonIconColor: '#ffffff',   // + icon inside button
  widgetTitleColor:         '#f0f0f0',
  placeholderColor:         '#6c63ff',
};
```

```html
<app-dashboard [theme]="theme"></app-dashboard>
```

### CSS Custom Properties

Every token is also available as a CSS variable on the `app-dashboard` host element:

```css
app-dashboard {
  /* ── Layout ── */
  --dash-bg:                   #0d0d0d;
  --dash-panel-bg:             #161616;
  --dash-panel-border:         rgba(255,255,255,0.06);
  --dash-card-bg:              #1f1f1f;
  --dash-fore-color:           #a0a0a0;
  --dash-accent-color:         #6c63ff;
  --dash-accent-tint:          rgba(108,99,255,0.15);
  --dash-accent-tint-strong:   rgba(108,99,255,0.25);
  --dash-danger-color:         #e84545;
  --dash-danger-tint:          rgba(232,69,69,0.22);
  --dash-font-family:          'Inter', sans-serif;

  /* ── Header / Tabs ── */
  --dash-tabs-container-color: rgba(30,30,32,0.7);
  --dash-tab-active-color:     #2a2a2e;
  --dash-tab-active-text:      #ffffff;
  --dash-tab-hover-text:       #d0d0d5;
  --dash-tab-hover-bg:         rgba(255,255,255,0.05);
  --dash-add-widget-text:      #ffffff;
  --dash-add-widget-icon:      #ffffff;
  --dash-add-widget-bg:        #0a84ff;
  --dash-tab-active-icon:      #ffffff;
  --dash-tab-inactive-icon:    #a0a0a0;
  --dash-overflow-btn-bg:      rgba(255,255,255,0.08);
  --dash-overflow-btn-border:  rgba(255,255,255,0.12);

  /* ── Tab-switcher overlay ── */
  --dash-overlay-bg:           rgba(0,0,0,0.6);
  --dash-sheet-border:         rgba(255,255,255,0.08);
  --dash-scrollbar-color:      rgba(255,255,255,0.15);
  --dash-switcher-title:       #ffffff;
  --dash-switcher-card-text:   #ffffff;

  /* ── Pop-out ── */
  --dash-popout-title-color:   #ffffff;

  /* ── Widget card ── */
  --dash-widget-title-color:   #ffffff;
  --dash-drag-handle-color:    rgba(255,255,255,0.2);
  --dash-widget-border-color:  rgba(255,255,255,0.07);
  --dash-widget-btn-bg:        rgba(255,255,255,0.07);
  --dash-widget-btn-color:     rgba(255,255,255,0.5);

  /* ── Grid ── */
  --dash-placeholder-color:    #6c63ff;
  --dash-resize-handle-color:  rgba(255,255,255,0.25);
}
```

### CSS Variable Reference

| CSS Variable | `DashboardTheme` field | Default |
|---|---|---|
| `--dash-bg` | `backgroundColor` | `#000000` |
| `--dash-panel-bg` | `panelColor` | `#1c1c1e` |
| `--dash-panel-border` | `panelBorderColor` | `rgba(255,255,255,0.05)` |
| `--dash-card-bg` | `widgetCardColor` | `#2c2c2e` |
| `--dash-fore-color` | `foreColor` | `#8e8e93` |
| `--dash-accent-color` | `accentColor` | `#0a84ff` |
| `--dash-accent-tint` | `accentTintColor` | `rgba(10,132,255,0.15)` |
| `--dash-accent-tint-strong` | `accentTintStrongColor` | `rgba(10,132,255,0.25)` |
| `--dash-danger-color` | `dangerColor` | `#ff453a` |
| `--dash-danger-tint` | `dangerTintColor` | `rgba(255,69,58,0.22)` |
| `--dash-font-family` | `fontFamily` | system-ui stack |
| `--dash-tabs-container-color` | `tabsContainerColor` | `rgba(44,44,46,0.6)` |
| `--dash-tab-active-color` | `tabActiveColor` | `#3a3a3c` |
| `--dash-tab-active-text` | `tabActiveTextColor` | `#ffffff` |
| `--dash-tab-hover-text` | `tabHoverTextColor` | `#e5e5ea` |
| `--dash-tab-hover-bg` | `tabHoverBgColor` | `rgba(255,255,255,0.05)` |
| `--dash-add-widget-text` | `addWidgetButtonTextColor` | `#ffffff` |
| `--dash-add-widget-icon` | `addWidgetButtonIconColor` | `#ffffff` |
| `--dash-add-widget-bg` | `addWidgetButtonBgColor` | `#0a84ff` |
| `--dash-tab-active-icon` | `tabActiveIconColor` | `#ffffff` |
| `--dash-tab-inactive-icon` | `tabInactiveIconColor` | `#8e8e93` |
| `--dash-overflow-btn-bg` | `overflowBtnBgColor` | `rgba(255,255,255,0.08)` |
| `--dash-overflow-btn-border` | `overflowBtnBorderColor` | `rgba(255,255,255,0.12)` |
| `--dash-overlay-bg` | `overlayBgColor` | `rgba(0,0,0,0.55)` |
| `--dash-sheet-border` | `sheetBorderColor` | `rgba(255,255,255,0.08)` |
| `--dash-scrollbar-color` | `scrollbarColor` | `rgba(255,255,255,0.15)` |
| `--dash-switcher-title` | `switcherTitleColor` | `#ffffff` |
| `--dash-switcher-card-text` | `switcherCardTextColor` | `#ffffff` |
| `--dash-popout-title-color` | `popoutTitleColor` | `#ffffff` |
| `--dash-widget-title-color` | `widgetTitleColor` | `#ffffff` |
| `--dash-drag-handle-color` | `dragHandleColor` | `rgba(255,255,255,0.2)` |
| `--dash-widget-border-color` | `widgetBorderColor` | `rgba(255,255,255,0.07)` |
| `--dash-widget-btn-bg` | `widgetButtonBgColor` | `rgba(255,255,255,0.07)` |
| `--dash-widget-btn-color` | `widgetButtonColor` | `rgba(255,255,255,0.5)` |
| `--dash-placeholder-color` | `placeholderColor` | `#0a84ff` |
| `--dash-resize-handle-color` | `resizeHandleColor` | `rgba(255,255,255,0.25)` |

---

## Labels & i18n

Every visible string in the dashboard UI can be replaced via the `[labels]` input. All properties are optional — unset properties fall back to their English defaults.

### Basic usage

```html
<app-dashboard [labels]="myLabels"></app-dashboard>
```

```typescript
import { DashboardLabels } from '@ogidor/dashboard';

myLabels: DashboardLabels = {
  addWidgetButton:    'New Widget',
  workspaceSingular:  'Page',
  workspacePlural:    'Pages',
  defaultWorkspaceName: 'Page',
  newWorkspaceButton: 'New Page',
  removeWorkspaceConfirm: 'Delete this page?',
};
```

### Full reference

| Property | Default | Where it appears |
|---|---|---|
| `addWidgetButton` | `'Add Widget'` | Visible text on the top-right button |
| `addWidgetButtonTitle` | `'Add widget'` | Tooltip on that button |
| `addPageButtonTitle` | `'New workspace'` | Tooltip on the `+` icon in the tab bar |
| `overflowButtonTitle` | `'Show all workspaces'` | Tooltip on the overflow `+N` button |
| `popoutButtonTitle` | `'Open in new window'` | Tooltip on the pop-out arrow icon |
| `closeTabButtonTitle` | `'Close'` | Tooltip on the `×` close icon |
| `workspaceSingular` | `'Workspace'` | Tab-switcher dialog title when there is 1 workspace |
| `workspacePlural` | `'Workspaces'` | Tab-switcher dialog title when there are N workspaces |
| `newWorkspaceButton` | `'New Workspace'` | Button text in the tab-switcher footer |
| `addWorkspacePromptLabel` | `'Workspace name:'` | Label in the browser `prompt()` when adding a workspace |
| `defaultWorkspaceName` | `'Workspace'` | Prefix for the default name in that prompt (`Workspace 2`, etc.) |
| `removeWorkspaceConfirm` | `'Remove this workspace?'` | Browser `confirm()` message before deleting a workspace |
| `dragToMoveTitle` | `'Drag to move'` | Tooltip on the widget drag handle |
| `editButtonTitle` | `'Edit'` | Tooltip on the widget edit icon |
| `removeButtonTitle` | `'Remove'` | Tooltip on the widget remove icon |

---

## Pop-out Windows

Every workspace tab shows a pop-out button (arrow icon, visible on hover). Clicking it opens that workspace in a new browser window.

The pop-out window:

- Shows only that workspace — no tabs, no Add Widget button, just the grid.
- Has its **own independent layout** — moving cards there never affects the main window.
- **Receives** structural changes (add/remove widget, metadata edits) from all other windows in real time.

### Sync behaviour

| State | Synced across windows? | Stored in |
|---|---|---|
| Page list (add / remove / rename) | Yes | `ogidor_shared` |
| Widget list (add / remove) | Yes | `ogidor_shared` |
| Widget metadata (title, cardColor, data) | Yes | `ogidor_shared` |
| Card positions and sizes | No — per window | `ogidor_positions_<windowId>` |

To pop out programmatically:

```typescript
this.state.popOutPage(pageId);
```

---

## Persisting Layouts

By default, state is saved to `localStorage` automatically on every change.

| Key | Contents |
|---|---|
| `ogidor_shared` | Page list and widget metadata for the default dashboard context. |
| `ogidor_positions_main` | Grid positions for the main window (default context). |
| `ogidor_positions_<pageId>` | Grid positions for each pop-out window (default context). |

For multi-tenant scenarios, provide `DASHBOARD_PERSISTENCE_CONTEXT` with a custom `dashboardId`; keys are namespaced automatically in the default localStorage adapter.

### Use a database/API instead of localStorage

Provide your own implementation of `DashboardPersistence` and override the `DASHBOARD_PERSISTENCE` token.

```typescript
import { Injectable, NgModule } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
  DashboardConfig,
  DashboardModule,
  DashboardPersistence,
  DASHBOARD_PERSISTENCE,
  DASHBOARD_PERSISTENCE_CONTEXT,
  PositionMap,
} from '@ogidor/dashboard';

@Injectable()
export class HttpDashboardPersistence extends DashboardPersistence {
  constructor(private http: HttpClient) { super(); }

  loadShared(dashboardId: string) {
    return this.http
      .get<DashboardConfig | null>(`/api/dashboards/${encodeURIComponent(dashboardId)}/shared`)
      .toPromise();
  }

  saveShared(dashboardId: string, shared: DashboardConfig) {
    return this.http
      .put<void>(`/api/dashboards/${encodeURIComponent(dashboardId)}/shared`, shared)
      .toPromise()
      .then(() => undefined);
  }

  loadPositions(dashboardId: string, windowId: string) {
    return this.http
      .get<PositionMap | null>(`/api/dashboards/${encodeURIComponent(dashboardId)}/positions/${encodeURIComponent(windowId)}`)
      .toPromise();
  }

  savePositions(dashboardId: string, windowId: string, positions: PositionMap) {
    return this.http
      .put<void>(`/api/dashboards/${encodeURIComponent(dashboardId)}/positions/${encodeURIComponent(windowId)}`, positions)
      .toPromise()
      .then(() => undefined);
  }
}

@NgModule({
  imports: [DashboardModule],
  providers: [
    { provide: DASHBOARD_PERSISTENCE, useClass: HttpDashboardPersistence },
    { provide: DASHBOARD_PERSISTENCE_CONTEXT, useValue: { dashboardId: 'customer-123' } },
  ],
})
export class AppModule {}
```

Recommended backend endpoints:

- `GET /api/dashboards/:dashboardId/shared`
- `PUT /api/dashboards/:dashboardId/shared`
- `GET /api/dashboards/:dashboardId/positions/:windowId`
- `PUT /api/dashboards/:dashboardId/positions/:windowId`

### Migration tip

If users already have data in localStorage, run a one-time migration in your app:

1. Read `ogidor_shared` and `ogidor_positions_*` from localStorage.
2. Upload them to your backend using the new API.
3. Set a migration flag (for example `ogidor_migrated=true`).
4. Use your HTTP persistence adapter as the source of truth from then on.

---

## Full Integration Example

```typescript
import { Component, ViewChild } from '@angular/core';
import {
  DashboardComponent,
  DashboardStateService,
  DashboardTheme,
  DashboardLabels,
  Widget,
} from '@ogidor/dashboard';

@Component({
  selector: 'app-root',
  template: `
    <app-dashboard
      [theme]="theme"
      [labels]="labels"
      (addWidgetRequested)="openAddDialog()"
      (editWidgetRequested)="openEditDialog($event)"
    >
      <ng-template gridCell let-widget="widget">
        <my-chart [config]="widget.data"></my-chart>
      </ng-template>
    </app-dashboard>

    <my-add-dialog
      *ngIf="addOpen"
      (confirmed)="onAddConfirmed($event)"
      (cancelled)="addOpen = false"
    ></my-add-dialog>

    <my-edit-dialog
      *ngIf="editTarget"
      [widget]="editTarget"
      (confirmed)="onEditConfirmed($event)"
      (cancelled)="editTarget = null"
    ></my-edit-dialog>
  `,
})
export class AppComponent {
  @ViewChild(DashboardComponent) dash!: DashboardComponent;

  theme: DashboardTheme = {
    accentColor:     '#6c63ff',
    tabActiveColor:  '#2a2a38',
    widgetTitleColor: '#f0f0f0',
  };

  labels: DashboardLabels = {
    addWidgetButton:      'New Widget',
    workspaceSingular:    'Page',
    workspacePlural:      'Pages',
    defaultWorkspaceName: 'Page',
  };

  addOpen = false;
  editTarget: Widget | null = null;

  constructor(private state: DashboardStateService) {}

  openAddDialog()           { this.addOpen = true; }
  openEditDialog(w: Widget) { this.editTarget = w; }

  onAddConfirmed(payload: Partial<Widget> & { title: string }) {
    this.state.addWidget(payload);
    this.addOpen = false;
  }

  onEditConfirmed(patch: Partial<Widget>) {
    this.state.updateWidget(this.editTarget!.id, patch);
    this.editTarget = null;
  }

  saveLayout() {
    return this.dash.serializeLayout();
  }
}
```

---

## Development

```bash
# Install dependencies
npm install

# Build the library
npm run build:lib

# Run the local demo app
npm start
```

---

## License

MIT