@ogidor/dashboard
v1.0.16
Published
A content-agnostic drag-and-drop dashboard library for Angular 15. Multi-workspace, persistent layout, pop-out windows, live cross-tab sync.
Maintainers
Readme
@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 persistent layout, pop-out windows, and live cross-tab sync — out of the box.
Table of Contents
- Features
- Installation
- Quick Start
- API Reference
- Models
- Theming
- Pop-out Windows
- Persisting Layouts
- Full Integration Example
- Development
- 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. - Persistent Layout — state is saved to
localStorageon every change and restored on reload. - 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
DashboardThemeinput or CSS custom properties.
Installation
npm install @ogidor/dashboard line-awesomeAdd Line Awesome to your global styles (styles.scss or angular.json):
@import "line-awesome/dist/line-awesome/css/line-awesome.css";Quick Start
1. Import DashboardModule
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.
<!-- 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>/* your global stylesheet */
.dashboard-container {
width: 100%;
height: calc(100vh - 60px); /* e.g. subtract a top nav */
}<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
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. |
| 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)
@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.
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'|'x'|'y'|'cols'|'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.
<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
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 { Widget, Page, DashboardConfig, DashboardTheme } from '@ogidor/dashboard';Models
Widget
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
export interface Page {
id: string;
name: string;
widgets: Widget[];
}DashboardConfig
export interface DashboardConfig {
pages: Page[];
activePageId: string;
}DashboardTheme
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
cardColoron anyWidgetto overridewidgetCardColorfor just that one card.
Theming
Theme Input
Pass a partial DashboardTheme to [theme]. Only provided fields are overridden.
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',
};<app-dashboard [theme]="theme"></app-dashboard>CSS Custom Properties
Every token is also available as a CSS variable on the app-dashboard host element:
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) |
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:
this.state.popOutPage(pageId);Persisting Layouts
State is saved to localStorage automatically on every change.
| Key | Contents |
|---|---|
| ogidor_shared | Page list and widget metadata. Shared across all windows. |
| ogidor_positions_main | Grid positions for the main window. |
| ogidor_positions_<pageId> | Grid positions for each pop-out window. |
To save and restore via a backend:
// Save
const json = this.dash.serializeLayout();
await api.save(json);
// Restore
const json = await api.load();
this.dash.stateService.loadLayout(JSON.parse(json));Full Integration Example
import { Component, ViewChild } from '@angular/core';
import {
DashboardComponent,
DashboardStateService,
DashboardTheme,
Widget,
} from '@ogidor/dashboard';
@Component({
selector: 'app-root',
template: `
<app-dashboard
[theme]="theme"
(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',
};
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
# Install dependencies
npm install
# Build the library
npm run build:lib
# Run the local demo app
npm startLicense
MIT
