Landing Page Diagram
This example demonstrates the library’s capabilities and the possibility to integrate with external libraries, such as presenting data on charts inside nodes. This is a real use case scenario.
- configure-chart.ts
- diagram-data.config.ts
- diagram.component.ts
- graph-node.component.ts
- group-node.component.ts
- user-panel-node.component.ts
- workflow-node.component.ts
- graph-node.component.html
- group-node.component.html
- user-panel-node.component.html
- workflow-node.component.html
- _mixins.scss
- diagram.component.scss
- graph-node.component.scss
- group-node.component.scss
- tokens.scss
- user-panel-node.component.scss
- workflow-node.component.scss
import uPlot from 'uplot';import { NgDiagramViewportService } from 'ng-diagram';
const CHART_CONFIG = { WIDTH: 300, HEIGHT: 200, Y_RANGE_MAX: 1000, LINE_WIDTH: 2, POINT_SIZE: 6, HOVER_POINT_SIZE: 8,} as const;
const COLORS = { STROKE_PRIMARY: 'rgba(255, 255, 255, 0.3)', STROKE_SECONDARY: 'rgba(255, 255, 255, 0.2)', GRID: 'rgba(255, 255, 255, 0.1)', GRADIENT_START: 'rgb(139, 92, 246)', GRADIENT_MID: 'rgb(168, 85, 247)', GRADIENT_END: 'rgb(192, 132, 252)', FILL_START: 'rgba(147, 51, 234, 0.4)', FILL_END: 'rgba(147, 51, 234, 0)', POINT_STROKE: 'rgb(147, 51, 234)', POINT_FILL: 'rgb(30, 30, 35)',} as const;
const AXIS_CONFIG = { X: { SIZE: 35, GAP: 5, SPACE: 30, TICK_SIZE: 4, }, Y: { SIZE: 45, GAP: 5, TICK_SIZE: 4, INCREMENTS: [250, 500], },} as const;
const DAY_LABELS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] as const;
function createScales(): uPlot.Options['scales'] { return { x: { time: false }, y: { range: [0, CHART_CONFIG.Y_RANGE_MAX] }, };}
function createXAxis(): uPlot.Axis { return { show: true, stroke: COLORS.STROKE_PRIMARY, grid: { show: false }, ticks: { show: true, stroke: COLORS.STROKE_SECONDARY, size: AXIS_CONFIG.X.TICK_SIZE, }, gap: AXIS_CONFIG.X.GAP, size: AXIS_CONFIG.X.SIZE, space: AXIS_CONFIG.X.SPACE, incrs: [1], values: (_u, vals) => vals.map((v) => DAY_LABELS[v]), };}
function createYAxis(): uPlot.Axis { return { show: true, stroke: COLORS.STROKE_PRIMARY, grid: { show: true, stroke: COLORS.GRID, }, ticks: { show: true, stroke: COLORS.STROKE_SECONDARY, size: AXIS_CONFIG.Y.TICK_SIZE, }, size: AXIS_CONFIG.Y.SIZE, gap: AXIS_CONFIG.Y.GAP, incrs: [...AXIS_CONFIG.Y.INCREMENTS], values: (_u, vals) => vals.map((v) => Math.round(v).toString()), };}
function createAxes(): uPlot.Axis[] { return [createXAxis(), createYAxis()];}
function createLineGradient(u: uPlot): CanvasGradient { const gradient = u.ctx.createLinearGradient(0, 0, u.bbox.width, 0); gradient.addColorStop(0, COLORS.GRADIENT_START); gradient.addColorStop(0.5, COLORS.GRADIENT_MID); gradient.addColorStop(1, COLORS.GRADIENT_END); return gradient;}
function createFillGradient(u: uPlot): CanvasGradient { const gradient = u.ctx.createLinearGradient(0, 0, 0, u.bbox.height); gradient.addColorStop(0, COLORS.FILL_START); gradient.addColorStop(1, COLORS.FILL_END); return gradient;}
function createSeries(): uPlot.Series[] { return [ {}, // First series is for x-axis { label: 'New Users', stroke: (u) => createLineGradient(u), width: CHART_CONFIG.LINE_WIDTH, fill: (u) => createFillGradient(u), points: { show: true, size: CHART_CONFIG.POINT_SIZE, width: CHART_CONFIG.LINE_WIDTH, stroke: COLORS.POINT_STROKE, fill: COLORS.POINT_FILL, }, }, ];}
function handleCursorMove( self: uPlot, viewportService: NgDiagramViewportService) { return (e: MouseEvent) => { const scale = viewportService.scale(); const rect = self.over.getBoundingClientRect();
// Adjust mouse position to account for diagram zoom/scale const adjustedX = (e.clientX - rect.left) / scale; const adjustedY = (e.clientY - rect.top) / scale;
self.setCursor({ left: adjustedX, top: adjustedY });
return null; };}
function createCursorConfig( viewportService: NgDiagramViewportService): uPlot.Cursor { return { bind: { mousemove: (self) => handleCursorMove(self, viewportService), }, drag: { x: false, y: false, }, points: { show: true, size: () => CHART_CONFIG.HOVER_POINT_SIZE, stroke: () => COLORS.GRADIENT_MID, width: () => CHART_CONFIG.LINE_WIDTH, fill: () => COLORS.POINT_FILL, }, };}
/** * Creates the complete uPlot configuration */export function createChartOptions( viewportService: NgDiagramViewportService): uPlot.Options { return { width: CHART_CONFIG.WIDTH, height: CHART_CONFIG.HEIGHT, scales: createScales(), axes: createAxes(), series: createSeries(), cursor: createCursorConfig(viewportService), legend: { show: false }, };}import type { Edge, Node } from 'ng-diagram';
export const USERS = [ { id: 1, name: 'Lena Wu', avatar: '👩', color: '#349A97' }, { id: 2, name: 'Marcus Grant', avatar: '👨', color: '#292E78' }, { id: 3, name: 'Nina Patel', avatar: '👩', color: '#FBAA03' }, { id: 4, name: 'James Okoro', avatar: '👨', color: '#DD667C' }, { id: 5, name: 'Sofia Lemaire', avatar: '👩', color: '#E7D9CE' },] as const;
export const DIAGRAM_NODES: Node[] = [ { id: 'sub-process', type: 'group', position: { x: 50, y: 80 }, size: { width: 400, height: 400 }, autoSize: false, data: { title: 'Sub Process' }, isGroup: true, }, { id: 'start-flow', type: 'workflow', position: { x: 120, y: 160 }, groupId: 'sub-process', data: { title: 'Start Flow', subtitle: 'Trigger', icon: 'lightning', iconColor: '#4CAF50', }, }, { id: 'wait-interval', type: 'workflow', position: { x: 120, y: 260 }, groupId: 'sub-process', data: { title: 'Wait Interval', subtitle: 'Delay', icon: 'timer', iconColor: '#FFC107', }, }, { id: 'send-notification', type: 'workflow', position: { x: 120, y: 360 }, groupId: 'sub-process', data: { title: 'Send Notification', subtitle: 'Notification', icon: 'paper-plane-right', iconColor: '#2196F3', }, }, { id: 'proceed-if-true', type: 'workflow', position: { x: 560, y: 280 }, data: { title: 'Proceed if true', subtitle: 'Conditional', icon: 'list-checks', iconColor: '#4CAF50', }, }, { id: 'user-panel', type: 'userPanel', position: { x: 190, y: 600 }, data: { users: USERS }, }, { id: 'notify-users', type: 'graph', position: { x: 560, y: 475 }, data: { title: 'Notify Users', subtitle: 'Action Node', icon: 'play-circle', iconColor: '#4CAF50', }, },];
export const DIAGRAM_EDGES: Edge[] = [ // Sub Process internal connections { id: 'e1', source: 'start-flow', sourcePort: 'port-right', target: 'wait-interval', targetPort: 'port-left', data: {}, }, { id: 'e2', source: 'wait-interval', sourcePort: 'port-right', target: 'send-notification', targetPort: 'port-left', data: {}, },
// Sub Process to Conditional { id: 'e3', source: 'sub-process', sourcePort: 'port-right', target: 'proceed-if-true', targetPort: 'port-left', data: {}, },
// Notify Users connections { id: 'e4', source: 'notify-users', sourcePort: 'port-left', target: 'send-notification', targetPort: 'port-right', data: {}, }, { id: 'e5', source: 'notify-users', sourcePort: 'port-left', target: 'user-panel', targetPort: 'port-right', data: {}, },];import '@angular/compiler';
import { Component } from '@angular/core';import { initializeModel, NgDiagramBackgroundComponent, NgDiagramComponent, type NgDiagramConfig, NgDiagramNodeTemplateMap, provideNgDiagram,} from 'ng-diagram';import { GraphNodeComponent } from '../graph-node/graph-node.component';import { GroupNodeComponent } from '../group-node/group-node.component';import { UserPanelNodeComponent } from '../user-panel-node/user-panel-node.component';import { WorkflowNodeComponent } from '../workflow-node/workflow-node.component';import { DIAGRAM_EDGES, DIAGRAM_NODES } from './diagram-data.config';
@Component({ selector: 'diagram', imports: [NgDiagramComponent, NgDiagramBackgroundComponent], providers: [provideNgDiagram()], styleUrls: ['diagram.component.scss', 'tokens.scss'], template: ` <div class="not-content diagram"> <ng-diagram [model]="model" [config]="config" [nodeTemplateMap]="nodeTemplateMap" > <ng-diagram-background /> </ng-diagram> </div> `,})export class DiagramComponent { readonly nodeTemplateMap = new NgDiagramNodeTemplateMap([ ['workflow', WorkflowNodeComponent], ['group', GroupNodeComponent], ['userPanel', UserPanelNodeComponent], ['graph', GraphNodeComponent], ]);
readonly config: NgDiagramConfig = { edgeRouting: { defaultRouting: 'orthogonal', }, background: { dotSpacing: 20, }, resize: { allowResizeBelowChildrenBounds: false, }, selectionMoving: { edgePanningEnabled: false, }, linking: { edgePanningEnabled: false, }, zoom: { step: 0, //disable zoom zoomToFit: { onInit: true, }, }, viewportPanningEnabled: false, };
readonly model = initializeModel({ nodes: DIAGRAM_NODES, edges: DIAGRAM_EDGES, });}import { CommonModule } from '@angular/common';import { type AfterViewInit, ChangeDetectionStrategy, Component, computed, ElementRef, inject, input, type OnDestroy, viewChild,} from '@angular/core';import { type NgDiagramNodeTemplate, NgDiagramViewportService, type Node,} from 'ng-diagram';import uPlot from 'uplot';import { WorkflowNodeComponent, type WorkflowNodeData,} from '../workflow-node/workflow-node.component';import { createChartOptions } from './configure-chart';
@Component({ selector: 'app-graph-node', imports: [CommonModule, WorkflowNodeComponent], templateUrl: './graph-node.component.html', styleUrls: ['./graph-node.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class.ng-diagram-port-hoverable]': 'true', },})export class GraphNodeComponent implements NgDiagramNodeTemplate<WorkflowNodeData>, AfterViewInit, OnDestroy{ private readonly viewportService = inject(NgDiagramViewportService); private chart?: uPlot;
node = input.required<Node<WorkflowNodeData>>(); title = computed(() => this.node()?.data?.title || 'Graph');
chartContainer = viewChild<ElementRef<HTMLDivElement>>('chartContainer');
ngAfterViewInit(): void { const container = this.chartContainer()?.nativeElement; if (!container) { return; }
const data = this.getSampleData(); const options = createChartOptions(this.viewportService);
this.chart = new uPlot(options, data, container); }
ngOnDestroy(): void { this.chart?.destroy(); }
private getSampleData(): uPlot.AlignedData { const dayIndices = [0, 1, 2, 3, 4, 5, 6]; const newUsersPerDay = [450, 520, 480, 560, 620, 700, 850];
return [dayIndices, newUsersPerDay]; }}import { CommonModule } from '@angular/common';import { ChangeDetectionStrategy, Component, computed, input,} from '@angular/core';import { type GroupNode, NgDiagramGroupHighlightedDirective, type NgDiagramGroupNodeTemplate, NgDiagramNodeResizeAdornmentComponent, NgDiagramNodeSelectedDirective, NgDiagramPortComponent,} from 'ng-diagram';
export interface GroupNodeData { title: string;}
@Component({ selector: 'app-group-node', imports: [ NgDiagramPortComponent, NgDiagramNodeSelectedDirective, NgDiagramGroupHighlightedDirective, CommonModule, NgDiagramNodeResizeAdornmentComponent, ], templateUrl: './group-node.component.html', styleUrls: ['./group-node.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class.ng-diagram-port-hoverable]': 'true', },})export class GroupNodeComponent implements NgDiagramGroupNodeTemplate<GroupNodeData>{ node = input.required<GroupNode<GroupNodeData>>(); title = computed(() => this.node()?.data?.title || 'Group');}import { CommonModule } from '@angular/common';import { ChangeDetectionStrategy, Component, computed, input, signal,} from '@angular/core';import { NgDiagramNodeSelectedDirective, type NgDiagramNodeTemplate, NgDiagramPortComponent, type Node,} from 'ng-diagram';
export interface UserPanelData { users: { id: number; name: string; avatar: string; color: string }[];}
@Component({ selector: 'app-user-panel-node', imports: [ NgDiagramPortComponent, NgDiagramNodeSelectedDirective, CommonModule, ], templateUrl: './user-panel-node.component.html', styleUrls: ['./user-panel-node.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class.ng-diagram-port-hoverable-over-node]': 'true', },})export class UserPanelNodeComponent implements NgDiagramNodeTemplate<UserPanelData>{ selectedUser = signal(2);
node = input.required<Node<UserPanelData>>(); users = computed(() => this.node()?.data?.users || []);
selectUser(id: number) { this.selectedUser.set(id); }}import { CommonModule } from '@angular/common';import { ChangeDetectionStrategy, Component, computed, input,} from '@angular/core';import { NgDiagramNodeSelectedDirective, type NgDiagramNodeTemplate, NgDiagramPortComponent, type Node,} from 'ng-diagram';
export interface WorkflowNodeData { title: string; subtitle?: string; icon: string; iconColor?: string;}
@Component({ selector: 'app-workflow-node', imports: [ NgDiagramPortComponent, NgDiagramNodeSelectedDirective, CommonModule, ], templateUrl: './workflow-node.component.html', styleUrls: ['./workflow-node.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class.ng-diagram-port-hoverable-over-node]': 'true', },})export class WorkflowNodeComponent implements NgDiagramNodeTemplate<WorkflowNodeData>{ node = input.required<Node<WorkflowNodeData>>();
className = computed(() => `ph ph-${this.node().data.icon}`); title = computed(() => this.node()?.data?.title || ''); subtitle = computed(() => this.node()?.data?.subtitle || ''); icon = computed(() => this.node()?.data?.icon || ''); iconColor = computed(() => this.node()?.data?.iconColor || 'gray');}<app-workflow-node [node]="node()"> <div class="graph-node"> <div class="graph-node__header"> <span class="graph-node__title">New Users</span> </div>
<div class="graph-node__content"> <div #chartContainer class="graph-node__chart"></div> </div> </div></app-workflow-node><ng-diagram-node-resize-adornment> <div ngDiagramNodeSelected ngDiagramGroupHighlighted [node]="node()" class="group-node" > <ng-diagram-port id="port-right" side="right" type="both" style="top: 30px" />
<div class="group-node__header"> <span class="group-node__title">{{ title() }}</span> </div> <div class="group-node__content"></div> </div></ng-diagram-node-resize-adornment><div ngDiagramNodeSelected [node]="node()" class="user-panel"> <ng-diagram-port id="port-right" side="right" type="both" />
@for (user of users(); track user.id) { <div class="user-panel__item" [class.user-panel__item--selected]="user.id === selectedUser()" (click)="selectUser(user.id)" > <div class="user-panel__avatar" [style.background-color]="user.color"> {{ user.avatar }} </div> <span class="user-panel__name">{{ user.name }}</span> </div> }</div><div ngDiagramNodeSelected [node]="node()" class="workflow-node"> <ng-diagram-port id="port-left" side="left" type="both" /> <ng-diagram-port id="port-right" side="right" type="both" />
<div class="workflow-node__header"> <div class="workflow-node__icon"> <i [class]="className()"></i> </div> <div class="workflow-node__text"> <div class="workflow-node__title">{{ title() }}</div> @if (subtitle()) { <div class="workflow-node__subtitle">{{ subtitle() }}</div> } </div> </div> <ng-content /></div>@mixin glow-gradient { &::after { content: ''; position: absolute; bottom: -100px; left: 50%; transform: translateX(-50%); width: 120%; height: 120px; background: radial-gradient( ellipse at center top, var(--ngdlp-purple-500-30) 0%, var(--ngdlp-purple-900-15) 40%, transparent 70% ); pointer-events: none; z-index: -1; filter: blur(20px); }}@import url('https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css');
:host { --ngd-port-background-color: var(--ngdlp-port-bg); --ngd-default-edge-stroke-hover: var(--ngdlp-edge-hover); --ngd-diagram-background-color: var(--ngdlp-diagram-bg); --ngd-ui-bg-tertiary-default: var(--ngdlp-bg-dark-deepest);
.diagram { position: relative; display: flex; height: 40rem; border: var(--ng-diagram-border); margin-top: 0; }
::ng-deep { .diagram-background-container svg.dotted-background { width: var(--ngdlp-diagram-background-size); height: var(--ngdlp-diagram-background-size); border: 1px solid var(--ngdlp-border-diagram); border-radius: 20px; background-color: var(--ngd-ui-bg-tertiary-default); top: calc((100% - var(--ngdlp-diagram-background-size)) / 2); left: calc((100% - var(--ngdlp-diagram-background-size)) / 2); } }}:host { display: block;}
.graph-node { background: var(--ngdlp-component-bg); border: 1px solid var(--ngdlp-border-primary-80); border-radius: 0.75rem; padding: 1rem; box-shadow: 0 0.25rem 0.75rem var(--ngdlp-shadow-dark);
&__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
&__title { color: var(--ngdlp-text-primary); font-size: 0.9375rem; font-weight: 600; }
&__expand-btn { background: var(--ngdlp-transparent); border: 1px solid var(--ngdlp-border-secondary-60); color: var(--ngdlp-text-secondary); cursor: pointer; padding: 0.25rem 0.5rem; font-size: 0.875rem; border-radius: 0.25rem; transition: all 0.2s ease;
&:hover { background: var(--ngdlp-border-secondary-30); color: var(--ngdlp-white-90); } }
&__content { display: flex; width: 100%; }
&__chart { flex: 1; display: block;
.u-wrap { background: var(--ngdlp-transparent) !important; }
.u-over { background: var(--ngdlp-transparent) !important; }
.u-legend { display: none !important; }
.u-axis { color: var(--ngdlp-text-tertiary) !important; font-size: 11px !important; }
.u-value { color: var(--ngdlp-white-90) !important; background: var(--ngdlp-component-bg) !important; border: 1px solid var(--ngdlp-border-primary-80) !important; padding: 0.25rem 0.5rem !important; border-radius: 0.25rem !important; font-size: 0.75rem !important; } }}@use '../mixins' as *;
:host { display: block; width: 100%; height: 100%; position: relative;}
.group-node { backdrop-filter: blur(0.3125rem); background: var(--ngdlp-component-bg-glass); border-radius: 1rem; transition: all 0.2s ease; width: 100%; height: 100%; position: relative;
@include glow-gradient;
&::before { content: ''; position: absolute; inset: 0; border-radius: 1rem; padding: 0.125rem; background: linear-gradient( 135deg, var(--ngdlp-purple-400-100), var(--ngdlp-purple-100-80), var(--ngdlp-purple-500-80), var(--ngdlp-purple-700-90) ); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite: exclude; pointer-events: none; }
&__header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; position: relative;
&::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 1px; background: linear-gradient( 90deg, var(--ngdlp-purple-400-60), var(--ngdlp-purple-100-40), var(--ngdlp-purple-500-50), var(--ngdlp-purple-700-60) ); } }
&__title { color: var(--ngdlp-text-primary); font-size: 0.875rem; font-weight: 600; }
&__actions { display: flex; gap: 0.5rem; }
&__content { padding: 1.25rem; }}:host { --ngdlp-diagram-background-size: 400px;
/* ===== Base Colors ===== */ --ngdlp-white: #ffffff; --ngdlp-black: #000000; --ngdlp-transparent: transparent;
/* ===== Dark Background Colors ===== */ --ngdlp-bg-dark-primary: rgb(30, 30, 35); --ngdlp-bg-dark-primary-95: rgba(30, 30, 35, 0.95); --ngdlp-bg-dark-primary-60: rgba(30, 30, 35, 0.6);
--ngdlp-bg-dark-secondary: rgb(40, 40, 45); --ngdlp-bg-dark-secondary-95: rgba(40, 40, 45, 0.95);
--ngdlp-bg-dark-deep: rgb(21, 21, 22);
--ngdlp-bg-dark-deepest: rgb(9, 9, 16);
--ngdlp-bg-selected: #27282b;
/* ===== Border & Separator Colors ===== */ --ngdlp-border-primary: rgb(60, 60, 70); --ngdlp-border-primary-80: rgba(60, 60, 70, 0.8); --ngdlp-border-primary-50: rgba(60, 60, 70, 0.5);
--ngdlp-border-secondary: rgb(80, 80, 90); --ngdlp-border-secondary-80: rgba(80, 80, 90, 0.8); --ngdlp-border-secondary-60: rgba(80, 80, 90, 0.6); --ngdlp-border-secondary-30: rgba(80, 80, 90, 0.3);
--ngdlp-border-diagram: #262629;
/* ===== Shadow Colors ===== */ --ngdlp-shadow-dark: rgba(0, 0, 0, 0.4);
/* ===== Purple/Violet Color Palette ===== */ --ngdlp-purple-900: rgb(88, 28, 135); --ngdlp-purple-900-15: rgba(88, 28, 135, 0.15); --ngdlp-purple-700: rgb(124, 58, 237); --ngdlp-purple-700-90: rgba(124, 58, 237, 0.9); --ngdlp-purple-700-60: rgba(124, 58, 237, 0.6);
--ngdlp-purple-500: rgb(139, 92, 246); --ngdlp-purple-500-80: rgba(139, 92, 246, 0.8); --ngdlp-purple-500-50: rgba(139, 92, 246, 0.5); --ngdlp-purple-500-30: rgba(139, 92, 246, 0.3);
--ngdlp-purple-600: rgb(147, 51, 234); --ngdlp-purple-600-40: rgba(147, 51, 234, 0.4); --ngdlp-purple-600-0: rgba(147, 51, 234, 0);
--ngdlp-purple-400: rgb(168, 85, 247); --ngdlp-purple-400-100: rgba(168, 85, 247, 1); --ngdlp-purple-400-60: rgba(168, 85, 247, 0.6);
--ngdlp-purple-300: rgb(192, 132, 252);
--ngdlp-purple-100: rgb(233, 213, 255); --ngdlp-purple-100-80: rgba(233, 213, 255, 0.8); --ngdlp-purple-100-40: rgba(233, 213, 255, 0.4);
/* ===== White/Gray Variations ===== */ --ngdlp-white-90: rgba(255, 255, 255, 0.9); --ngdlp-white-60: rgba(255, 255, 255, 0.6); --ngdlp-white-50: rgba(255, 255, 255, 0.5); --ngdlp-white-30: rgba(255, 255, 255, 0.3); --ngdlp-white-20: rgba(255, 255, 255, 0.2); --ngdlp-white-10: rgba(255, 255, 255, 0.1);
--ngdlp-gray-light: #c2c0c0;
/* ===== Accent Colors ===== */ --ngdlp-green-bright: rgb(145, 255, 55); --ngdlp-green-500: #4caf50;
--ngdlp-blue-500: #2196f3;
--ngdlp-amber-500: #ffc107; --ngdlp-amber-600: #fbaa03;
/* ===== User Avatar Colors ===== */ --ngdlp-user-teal: #349a97; --ngdlp-user-navy: #292e78; --ngdlp-user-pink: #dd667c; --ngdlp-user-beige: #e7d9ce;
/* ===== Semantic Color Mappings ===== */ --ngdlp-text-primary: var(--ngdlp-white); --ngdlp-text-secondary: var(--ngdlp-white-60); --ngdlp-text-tertiary: var(--ngdlp-white-50);
--ngdlp-component-bg: var(--ngdlp-bg-dark-primary-95); --ngdlp-component-bg-alt: var(--ngdlp-bg-dark-secondary-95); --ngdlp-component-bg-glass: var(--ngdlp-bg-dark-primary-60);
--ngdlp-hover-bg: var(--ngdlp-border-primary-50); --ngdlp-active-bg: var(--ngdlp-bg-selected);
--ngdlp-port-bg: var(--ngdlp-black); --ngdlp-edge-hover: var(--ngdlp-gray-light); --ngdlp-diagram-bg: var(--ngdlp-transparent);
--ngdlp-status-success: var(--ngdlp-green-500); --ngdlp-status-warning: var(--ngdlp-amber-500); --ngdlp-status-info: var(--ngdlp-blue-500);}:host { display: block;}
.user-panel { background: var(--ngdlp-component-bg); border: 1px solid var(--ngdlp-border-primary-80); border-radius: 0.75rem; padding: 0.5rem; min-width: 180px; box-shadow: 0 0.25rem 0.75rem var(--ngdlp-shadow-dark);
&__item { display: flex; align-items: center; gap: 0.75rem; padding: 0.625rem; border-radius: 0.5rem; transition: background-color 0.2s ease; cursor: pointer;
&:hover { background-color: var(--ngdlp-hover-bg); }
&--selected { background-color: var(--ngdlp-active-bg); } }
&__avatar { width: 2.25rem; height: 2.25rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.125rem; flex-shrink: 0; border: 1px solid var(--ngdlp-white); }
&__name { color: var(--ngdlp-text-primary); font-size: 0.875rem; font-weight: 500; }}@use '../mixins' as *;
:host { display: block; position: relative;}
.workflow-node { background: var(--ngdlp-component-bg-alt); border: 1px solid var(--ngdlp-border-secondary-80); border-radius: 0.75rem; padding: 0.5rem; min-width: 250px; transition: all 0.2s ease; cursor: pointer; display: flex; flex-direction: column; gap: 8px; position: relative;
@include glow-gradient;
&__header { display: flex; align-items: center; gap: 0.75rem; }
&__content { display: flex; align-items: center; }
&__icon { width: 2.875rem; height: 2.875rem; border-radius: 0.5rem; display: flex; align-items: center; justify-content: center; background-color: var(--ngdlp-bg-dark-deep); color: var(--ngdlp-green-bright); font-size: 1.5rem; border: 1px solid var(--ngdlp-border-secondary-80); }
&__text { display: flex; flex-direction: column; gap: 0.125rem; }
&__title { color: var(--ngdlp-text-primary); font-size: 0.9375rem; font-weight: 600; line-height: 1.2; }
&__subtitle { color: var(--ngdlp-text-secondary); font-size: 0.75rem; line-height: 1.2; }}