diff --git a/.claude/skills/igniteui-angular-components/references/layout-manager.md b/.claude/skills/igniteui-angular-components/references/layout-manager.md index 891f1b6b5..3be472300 100644 --- a/.claude/skills/igniteui-angular-components/references/layout-manager.md +++ b/.claude/skills/igniteui-angular-components/references/layout-manager.md @@ -124,10 +124,10 @@ Because Dock Manager is a Web Component, it requires two one-time setup steps: **1. Register custom elements — `main.ts`:** ```typescript -import { defineCustomElements } from 'igniteui-dockmanager/loader'; +import { defineComponents, IgcDockManagerComponent } from 'igniteui-dockmanager'; // Must be called before bootstrapApplication -defineCustomElements(); +defineComponents(IgcDockManagerComponent); ``` **2. Add `CUSTOM_ELEMENTS_SCHEMA` to every standalone component that uses ``:** @@ -400,7 +400,7 @@ export class DockManagerComponent { ### Key Rules for Dock Manager 1. **Separate package** — `igniteui-dockmanager` is installed independently of `igniteui-angular` -2. **Call `defineCustomElements()` in `main.ts`** before `bootstrapApplication` — without this the `` element renders as an unknown element +2. **Call `defineComponents(IgcDockManagerComponent)` in `main.ts`** before `bootstrapApplication` — without this the `` element renders as an unknown element 3. **Add `CUSTOM_ELEMENTS_SCHEMA`** to every standalone component or NgModule that uses `` 4. **Slot names = `contentId` values** — the `slot="..."` attribute on child elements must exactly match the `contentId` string in the layout 5. **Premium component** — requires a licensed Ignite UI subscription; verify availability before recommending to users diff --git a/live-editing/configs/DockManagerConfigGenerator.ts b/live-editing/configs/DockManagerConfigGenerator.ts index 5974f99fc..01bfa46f6 100644 --- a/live-editing/configs/DockManagerConfigGenerator.ts +++ b/live-editing/configs/DockManagerConfigGenerator.ts @@ -6,14 +6,14 @@ export class DockManagerConfigGenerator implements IConfigGenerator { public generateConfigs(): Config[] { const configs = new Array(); - const dockManagerImport = "import { defineCustomElements } from 'igniteui-dockmanager/loader';"; - const defineCustomElements = "defineCustomElements();"; + const dockManagerImport = "import { defineComponents, IgcDockManagerComponent } from 'igniteui-dockmanager';"; + const defineDockManagerComponents = 'defineComponents(IgcDockManagerComponent);'; configs.push(new Config({ component: 'DockManagerComponent', additionalDependencies: ["igniteui-dockmanager"], appConfig: { ...BaseAppConfig, - additionalAdjustments: [dockManagerImport, defineCustomElements] + additionalAdjustments: [dockManagerImport, defineDockManagerComponents] }, shortenComponentPathBy: "/layouts/dock-manager/" })); diff --git a/live-editing/configs/app-dv-configs/DVGridConfig.ts b/live-editing/configs/app-dv-configs/DVGridConfig.ts index 881e6f231..2ae2b7f04 100644 --- a/live-editing/configs/app-dv-configs/DVGridConfig.ts +++ b/live-editing/configs/app-dv-configs/DVGridConfig.ts @@ -37,15 +37,14 @@ export class DVGridConfigGenerator implements IConfigGenerator { GridFinJSComponent: '../../../projects/app-lob/src/app/grid-finjs/grid-finjs.component', SignalRService: '../../../projects/app-lob/src/app/services/signal-r.service', FloatingPanesService: '../../../projects/app-lob/src/app/services/floating-panes.service', - DockSlotComponent: '../../../projects/app-lob/src/app/grid-finjs-dock-manager/dock-slot.component', - GridHostDirective: '../../../projects/app-lob/src/app/grid-finjs-dock-manager/dock-slot.component', + DockSlotComponent: '../../../projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/dock-slot/dock-slot.component', FinancialDataService: '../../../projects/app-lob/src/app/services/financial.service' }; public generateConfigs(): Config[] { const configs = new Array(); - const dockManagerImport = 'import { defineCustomElements } from \'igniteui-dockmanager/loader\';'; - const defineCustomElements = 'defineCustomElements();'; + const dockManagerImport = 'import { defineComponents, IgcDockManagerComponent } from \'igniteui-dockmanager\';'; + const defineDockManagerComponents = 'defineComponents(IgcDockManagerComponent);'; configs.push(new Config({ component: 'GridComponent', @@ -88,11 +87,11 @@ export class DVGridConfigGenerator implements IConfigGenerator { '/projects/app-lob/src/app/services/signal-r.service.ts', '/projects/app-lob/src/app/data/financialData.ts', '/projects/app-lob/src/app/services/floating-panes.service.ts', - '/projects/app-lob/src/app/grid-finjs-dock-manager/dock-slot.component.ts', + '/projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/dock-slot/dock-slot.component.ts', '/projects/app-lob/src/_app-layout.scss', '/projects/app-lob/src/_variables.scss' ], appConfig: { ...BaseAppConfig, - additionalAdjustments: [dockManagerImport, defineCustomElements] + additionalAdjustments: [dockManagerImport, defineDockManagerComponents] } })); diff --git a/package-lock.json b/package-lock.json index 49ccff720..bec746420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "igniteui-angular-core": "^22.0.0", "igniteui-angular-extras": "22.1.0-beta.1", "igniteui-angular-i18n": "22.1.0-beta.1", - "igniteui-dockmanager": "^1.17.0", + "igniteui-dockmanager": "^2.1.3", "igniteui-grid-lite": "0.9.0", "igniteui-i18n-resources": "^1.0.4", "igniteui-live-editing": "^3.5.1", @@ -10580,20 +10580,27 @@ } }, "node_modules/igniteui-dockmanager": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/igniteui-dockmanager/-/igniteui-dockmanager-1.19.0.tgz", - "integrity": "sha512-MQNRgqSNItDt/k6f85Z8XRBl5OEJxIpEkic9m4QWKqdu4wFhDm9si+KkbpnvMgnyGMmNTL/pWUQIwAIPikIS/g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/igniteui-dockmanager/-/igniteui-dockmanager-2.1.3.tgz", + "integrity": "sha512-s0PdbdArUrppIBm7LvXaDXN2UOkkF+ldwNlWAHeOT+5LCxh1K1dfKBWnqbuPNNvYHleeNSCM9rMAXGVrx9xPTA==", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "igniteui-trial-watermark": "^1.0.3" + "igniteui-i18n-core": "^1.0.1", + "igniteui-trial-watermark": "^3.1.0", + "lit": "^3.3.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "igniteui-i18n-resources": "^1.0.1" + }, + "peerDependenciesMeta": { + "igniteui-i18n-resources": { + "optional": true + } } }, - "node_modules/igniteui-dockmanager/node_modules/igniteui-trial-watermark": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/igniteui-trial-watermark/-/igniteui-trial-watermark-1.0.3.tgz", - "integrity": "sha512-q6thtu+7R6MOB+i9GorFPCcWeOImW43BzCAtKnDAYWwaoueb8Lg1EhBkIhAyfEIH+yZ/9c5lnZdU61/GRPoP+g==", - "license": "Apache-2.0" - }, "node_modules/igniteui-grid-lite": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/igniteui-grid-lite/-/igniteui-grid-lite-0.9.0.tgz", diff --git a/package.json b/package.json index 0133ff2e8..2ff97ebd0 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "igniteui-angular-core": "^22.0.0", "igniteui-angular-extras": "22.1.0-beta.1", "igniteui-angular-i18n": "22.1.0-beta.1", - "igniteui-dockmanager": "^1.17.0", + "igniteui-dockmanager": "^2.1.3", "igniteui-grid-lite": "0.9.0", "igniteui-i18n-resources": "^1.0.4", "igniteui-live-editing": "^3.5.1", @@ -147,7 +147,6 @@ "event-stream": "^4.0.1", "fs-extra": "^8.1.0", "gulp": "^5.0.1", - "slash": "^3.0.0", "istanbul-lib-instrument": "^6.0.3", "jasmine-core": "~5.1.1", "karma": "^6.4.2", @@ -155,6 +154,7 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.0.0", "postcss": "^8.5.3", + "slash": "^3.0.0", "ts-node": "^10.9.1", "typescript": "6.0.3", "typescript-eslint": "^8.61.0", diff --git a/projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/data-analysis-dock-manager.component.scss b/projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/data-analysis-dock-manager.component.scss index 8e1de676e..4a69eb6b7 100644 --- a/projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/data-analysis-dock-manager.component.scss +++ b/projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/data-analysis-dock-manager.component.scss @@ -1,5 +1,5 @@ @use '../../../variables' as *; -@import 'igniteui-dockmanager/dist/collection/styles/igc.themes.css'; +@import 'igniteui-dockmanager/themes/igc.themes.css'; :host { width: 100%; diff --git a/projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/data-analysis-dock-manager.component.ts b/projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/data-analysis-dock-manager.component.ts index 0ac9fbc73..2d08ea833 100644 --- a/projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/data-analysis-dock-manager.component.ts +++ b/projects/app-lob/src/app/grid-dynamic-chart-data/data-analysis-dock-manager/data-analysis-dock-manager.component.ts @@ -1,7 +1,7 @@ /* eslint-disable max-len */ import { AfterViewInit, ChangeDetectorRef, ViewContainerRef, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, OnInit, Pipe, PipeTransform, QueryList, ViewChild, ViewChildren, TemplateRef, inject } from '@angular/core'; import { IgxChartIntegrationDirective, IgxConditionalFormattingDirective, IgxContextMenuDirective, OPTIONS_TYPE, CHART_TYPE, IDeterminedChartTypesArgs } from 'igniteui-angular-extras'; -import { IgcDockManagerLayout, IgcDockManagerPaneType, IgcSplitPane, IgcSplitPaneOrientation } from 'igniteui-dockmanager'; +import { IgcDockManagerComponent, IgcDockManagerLayout, IgcDockManagerPaneType, IgcSplitPane, IgcSplitPaneOrientation } from 'igniteui-dockmanager'; import { FinancialData } from '../../data/financialData'; import { FloatingPanesService } from '../../services/floating-panes.service'; import { DockSlotComponent } from './dock-slot/dock-slot.component'; @@ -68,7 +68,7 @@ export class DataAnalysisDockManagerComponent implements OnInit, AfterViewInit { public grid: IgxGridComponent; @ViewChild('dock', { read: ElementRef }) - public dockManager: ElementRef; + public dockManager: ElementRef; @ViewChild(IgxChartIntegrationDirective, { read: IgxChartIntegrationDirective, static: true }) public chartIntegration: IgxChartIntegrationDirective; diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/app-data-store.ts b/projects/app-lob/src/app/grid-finjs-dock-manager/app-data-store.ts new file mode 100644 index 000000000..0562a2ae4 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/app-data-store.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; + +export interface NavItem { + label: string; + icon: string; +} + +export interface MarketIdx { + name: string; + value: string; + change: string; + positive: boolean; +} + +@Injectable({ providedIn: 'root' }) +export class AppDataStore { + public readonly navItems: NavItem[] = [ + { label: 'Watchlist', icon: 'format_list_bulleted' }, + { label: 'Dashboard', icon: 'dashboard' }, + { label: 'Charts', icon: 'bar_chart' }, + { label: 'Goal Simulator', icon: 'track_changes' }, + { label: 'Dividends', icon: 'payments' }, + { label: 'Alerts', icon: 'notifications' } + ]; + + public readonly indices: MarketIdx[] = [ + { name: 'S&P 500', value: '5,634.61', change: '+0.42%', positive: true }, + { name: 'NASDAQ', value: '18,234.55', change: '+0.65%', positive: true }, + { name: 'DOW', value: '39,872.10', change: '-0.12%', positive: false }, + { name: 'VIX', value: '14.22', change: '-2.10%', positive: false } + ]; +} diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/chart-panel/chart-panel.component.html b/projects/app-lob/src/app/grid-finjs-dock-manager/chart-panel/chart-panel.component.html new file mode 100644 index 000000000..b281dc20a --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/chart-panel/chart-panel.component.html @@ -0,0 +1,51 @@ +
+
+
+ {{ currentPrice() | number:'1.2-2' }} + + {{ priceChange().positive ? '+' : '-' }}{{ priceChange().value }} + ({{ priceChange().positive ? '+' : '-' }}{{ priceChange().pct }}%) + +
+
+ O {{ todayStats().open }} + H {{ todayStats().high }} + L {{ todayStats().low }} + Vol {{ todayStats().vol }} +
+
+ @for (r of ranges; track r) { + + } +
+
+ +
+ +
+
diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/chart-panel/chart-panel.component.scss b/projects/app-lob/src/app/grid-finjs-dock-manager/chart-panel/chart-panel.component.scss new file mode 100644 index 000000000..dbcd4dc72 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/chart-panel/chart-panel.component.scss @@ -0,0 +1,88 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + background: var(--sd-bg); +} + +.chart-panel { + display: flex; + flex-direction: column; + height: 100%; +} + +.chart-header { + display: flex; + align-items: center; + gap: 16px; + padding: 10px 16px; + border-bottom: 1px solid var(--sd-border); + flex-shrink: 0; + flex-wrap: wrap; +} + +.price-block { + display: flex; + align-items: baseline; + gap: 10px; +} + +.current-price { + font-size: 26px; + font-weight: 700; + color: var(--sd-text); + font-variant-numeric: tabular-nums; + font-family: 'DM Mono', monospace; + letter-spacing: -0.5px; +} + +.price-change { + font-size: 13px; + font-weight: 500; +} + +.ohlc-stats { + display: flex; + gap: 14px; + flex: 1; +} + +.stat-item { + font-size: 12px; + color: var(--sd-muted); + white-space: nowrap; +} + +.stat-item b { + color: var(--sd-text); + font-weight: 500; +} + +.range-buttons { + display: flex; + gap: 2px; +} + +.range-btn { + font-size: 11px; + padding: 3px 9px; + color: var(--sd-muted); + min-width: unset; + border-radius: 4px; + transition: background 0.12s, color 0.12s; +} + +.range-btn.active-range { + color: var(--sd-cyan); + background: var(--sd-cyan-dim); +} + +.chart-area { + flex: 1; + min-height: 0; + padding: 4px 0 0; +} + +.pos { color: var(--sd-green); } +.neg { color: var(--sd-red); } diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/chart-panel/chart-panel.component.ts b/projects/app-lob/src/app/grid-finjs-dock-manager/chart-panel/chart-panel.component.ts new file mode 100644 index 000000000..5b0aaba73 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/chart-panel/chart-panel.component.ts @@ -0,0 +1,157 @@ +import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core'; +import { DecimalPipe } from '@angular/common'; +import { IgxFinancialChartModule } from 'igniteui-angular-charts'; +import { IgxButtonDirective } from 'igniteui-angular/directives'; + +interface CandlePoint { + Date: Date; + Open: number; + High: number; + Low: number; + Close: number; + Volume: number; +} + +type Range = '1W' | '1M' | '3M' | '6M' | '1Y' | '3Y' | '5Y'; +const RANGE_DAYS: Record = { + '1W': 7, + '1M': 30, + '3M': 63, + '6M': 126, + '1Y': 252, + '3Y': 756, + '5Y': 1260 +}; + +@Component({ + selector: 'app-chart-panel', + standalone: true, + imports: [IgxFinancialChartModule, IgxButtonDirective, DecimalPipe], + templateUrl: './chart-panel.component.html', + styleUrls: ['./chart-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ChartPanelComponent { + public readonly symbol = input('AAPL'); + public readonly basePrice = input(200); + + public readonly selectedRange = signal('1Y'); + public readonly ranges: Range[] = ['1W', '1M', '3M', '6M', '1Y', '3Y', '5Y']; + + private readonly allData = computed(() => this.generateData(1260)); + + public readonly chartData = computed(() => { + const days = RANGE_DAYS[this.selectedRange()]; + const all = this.allData(); + return all.slice(Math.max(0, all.length - days)); + }); + + public readonly currentPrice = computed(() => { + const data = this.chartData(); + if (data.length === 0) { + return this.basePrice(); + } + return data[data.length - 1].Close; + }); + + public readonly priceChange = computed(() => { + const data = this.chartData(); + if (data.length < 2) return { value: '0.00', pct: '0.00', positive: true }; + const first = data[0].Close; + const last = data[data.length - 1].Close; + const diff = last - first; + return { + value: Math.abs(diff).toFixed(2), + pct: Math.abs((diff / first) * 100).toFixed(2), + positive: diff >= 0 + }; + }); + + public readonly todayStats = computed(() => { + const data = this.chartData(); + if (data.length === 0) { + const p = this.basePrice(); + return { + open: p.toFixed(2), + high: p.toFixed(2), + low: p.toFixed(2), + vol: '0' + }; + } + + const latest = data[data.length - 1]; + return { + open: latest.Open.toFixed(2), + high: latest.High.toFixed(2), + low: latest.Low.toFixed(2), + vol: this.formatVolume(latest.Volume) + }; + }); + + private readonly symbolSeed = computed(() => this.symbol().split('').reduce((acc, c, i) => acc + c.charCodeAt(0) * (i + 1), 0)); + + private pseudoRandom(n: number): number { + const x = Math.sin(n) * 10000; + return x - Math.floor(x); + } + + private generateData(days: number): CandlePoint[] { + const seed = this.symbolSeed(); + const candles: CandlePoint[] = []; + let closePrice = this.basePrice() * 0.9; + + for (let i = days; i >= 0; i--) { + const d = new Date('2026-07-14'); + d.setDate(d.getDate() - i); + if (d.getDay() === 0 || d.getDay() === 6) continue; + + const k = seed * 13 + i * 37; + const drift = 0.00025; + const noise = (this.pseudoRandom(k * 5.7) - 0.5) * 0.03; + + const open = closePrice; + closePrice = open * (1 + drift + noise); + + const highSpread = 0.002 + this.pseudoRandom(k * 1.9) * 0.018; + const lowSpread = 0.002 + this.pseudoRandom(k * 2.3 + 9) * 0.018; + const high = Math.max(open, closePrice) * (1 + highSpread); + const low = Math.min(open, closePrice) * (1 - lowSpread); + const volume = 1200000 + Math.round(this.pseudoRandom(k * 9.4 + 3.1) * 8800000); + + candles.push({ + Date: new Date(d), + Open: Math.round(open * 100) / 100, + High: Math.round(high * 100) / 100, + Low: Math.round(low * 100) / 100, + Close: Math.round(closePrice * 100) / 100, + Volume: volume + }); + } + + if (candles.length === 0) return []; + + const scale = this.basePrice() / candles[candles.length - 1].Close; + + return candles.map(c => ({ + Date: c.Date, + Open: Math.round(c.Open * scale * 100) / 100, + High: Math.round(c.High * scale * 100) / 100, + Low: Math.round(c.Low * scale * 100) / 100, + Close: Math.round(c.Close * scale * 100) / 100, + Volume: c.Volume + })); + } + + private formatVolume(value: number): string { + if (value >= 1_000_000_000) { + return `${(value / 1_000_000_000).toFixed(2)}B`; + } + if (value >= 1_000_000) { + return `${(value / 1_000_000).toFixed(2)}M`; + } + if (value >= 1_000) { + return `${(value / 1_000).toFixed(1)}K`; + } + return value.toString(); + } +} diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/dock-slot.component.ts b/projects/app-lob/src/app/grid-finjs-dock-manager/dock-slot.component.ts deleted file mode 100644 index ddeba6a90..000000000 --- a/projects/app-lob/src/app/grid-finjs-dock-manager/dock-slot.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -// eslint-disable-next-line max-len -import { AfterViewInit, Component, Directive, EventEmitter, HostBinding, Input, OnInit, OnDestroy, Output, ViewChild, ViewContainerRef, inject } from '@angular/core'; -import { Subject } from 'rxjs'; - -@Directive({ - // eslint-disable-next-line @angular-eslint/directive-selector - selector: '[gridHost]' -}) -export class GridHostDirective { - viewContainerRef = inject(ViewContainerRef); - -} - -@Component({ - selector: 'app-dock-slot', - template: ` -
- -
`, - imports: [GridHostDirective] -}) -export class DockSlotComponent implements OnInit, AfterViewInit, OnDestroy { - @Output() - public viewInit = new EventEmitter(); - - @ViewChild(GridHostDirective) - public gridHost: GridHostDirective; - - @Input() - public id; - - @HostBinding('slot') - public slot; - - public destroy$ = new Subject(); - - constructor() { } - - public ngOnInit(): void { - this.slot = this.id; - } - - public ngAfterViewInit() { - this.viewInit.emit(); - } - public ngOnDestroy() { - this.destroy$.next(true); - this.destroy$.complete(); - } -} diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.html b/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.html index 6c192d2e3..701c99dad 100644 --- a/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.html +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.html @@ -1,197 +1,116 @@ - -
-
- Change theme:
Dark Mode -
-
- Start/Stop live data: {{ liveData === - true ? 'Streaming' : 'Not Streaming' }} -
-
- - - - - view_list - - @for (item of dataVolumeItems; track item) { - - {{item}} - - } - -
-
- - - - - cell_wifi - - @for (item of frequencyItems; track item) { - - {{item}} - +
+
-
Add Floating Pane
-
-
-
- - - - - - - - - -
- {{cell.value | currency:'USD':'symbol':'1.4-4'}} - @if (trends.positive(cell.row.data)) { - trending_up - } - @if (trends.negative(cell.row.data)) { - trending_down - } + + + + + +
+ + + + + + +
+ +
+
+
+
+
+
+
+
+
+
- - - -
diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.scss b/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.scss index dd4842ab3..68292719b 100644 --- a/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.scss +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.scss @@ -1,190 +1,525 @@ -@use 'igniteui-dockmanager/dist/collection/styles/igc.themes'; -@use '../../variables' as *; +:host { + display: block; + height: 100vh; + overflow: hidden; +} + +.app-shell { + --shell-rail-bg-start: #0f1724; + --shell-rail-bg-end: #0c1320; + --shell-command-bg-start: #0f1724; + --shell-command-bg-end: #0d1421; + --shell-strip-bg: #0d1420; + --shell-chip-bg: #1b2737; + --shell-chip-border: rgba(153, 178, 210, 0.35); + --shell-chip-text: #d7e6fa; + --shell-toggle-bg: #101b2a; + --shell-toggle-border: rgba(133, 165, 206, 0.35); + --shell-toggle-text: #8aa6c7; + --shell-toggle-hover-bg: rgba(138, 183, 236, 0.12); + --shell-toggle-hover-text: #d3e7ff; + --shell-toggle-hover-border: rgba(160, 202, 255, 0.5); + --shell-rail-btn-color: #b0bdd2; + --shell-rail-btn-hover: #b8cde6; + --shell-rail-active-color: #8ec7ff; + --shell-rail-active-bg: rgba(83, 158, 255, 0.15); + --shell-rail-active-border: #4aa2ff; + --shell-search-border: rgba(124, 155, 197, 0.22); + --shell-search-bg: rgba(4, 10, 18, 0.8); + --shell-search-icon: var(--sd-muted); + display: flex; + height: 100%; + background: + radial-gradient(circle at 35% 20%, rgba(44, 132, 255, 0.13), transparent 40%), + radial-gradient(circle at 80% 80%, rgba(44, 255, 196, 0.08), transparent 45%), + var(--sd-bg); + overflow: hidden; +} -.actionItem { - margin-block-end: rem(20px); +.app-shell.light-theme { + --shell-rail-bg-start: #eef3f9; + --shell-rail-bg-end: #e8eef6; + --shell-command-bg-start: #f8fbff; + --shell-command-bg-end: #f1f6fc; + --shell-strip-bg: #edf3fa; + --shell-chip-bg: #ffffff; + --shell-chip-border: #cbd5e1; + --shell-chip-text: #0f172a; + --shell-toggle-bg: #ffffff; + --shell-toggle-border: #cbd5e1; + --shell-toggle-text: #1e3a5f; + --shell-toggle-hover-bg: #eef5fd; + --shell-toggle-hover-text: #0f172a; + --shell-toggle-hover-border: #93c5fd; + --shell-rail-btn-color: #475569; + --shell-rail-btn-hover: #1e293b; + --shell-rail-active-color: #0f172a; + --shell-rail-active-bg: rgba(56, 138, 255, 0.14); + --shell-rail-active-border: #3b82f6; + --shell-search-border: #cbd5e1; + --shell-search-bg: #ffffff; + --shell-search-icon: #64748b; } -.finjs-icons { +.platform-rail { + width: 58px; + flex-shrink: 0; + background: linear-gradient(180deg, var(--shell-rail-bg-start) 0%, var(--shell-rail-bg-end) 100%); + border-right: 1px solid var(--sd-border); display: flex; + flex-direction: column; align-items: center; + padding: 10px 0; + gap: 10px; +} - igx-icon { - font-size: rem(16px); - width: rem(16px); - height: rem(16px); - margin-inline-start: rem(4px); - } +.rail-compose { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border: 1px solid rgba(255, 93, 93, 0.4); + border-radius: 8px; + background: rgba(255, 93, 93, 0.22); + color: #ff6a6a; + cursor: pointer; } -.changePos, -.changeNeg, -.strongPositive, -.strongNegative { - color: contrast-color(null, 'gray', 500) !important; +.rail-compose:disabled { + opacity: 0.45; + cursor: not-allowed; +} - .igx-grid__td-text { - padding: rem(2px) rem(5px); - } +.rail-nav { + list-style: none; + padding: 0; + margin: 0; + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; + align-items: center; } -.positive { - color: color(null, 'success', 500) !important; +.rail-nav-item { + width: 100%; + display: flex; + justify-content: center; } -.positive.strongPositive { - .igx-grid__td-text { - color: color(null, 'success', 500, .8) !important; - } +.rail-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + border: none; + background: transparent; + color: var(--shell-rail-btn-color); + gap: 2px; + font-size: 10px; + line-height: 1; + cursor: pointer; + padding: 7px 0; + border-left: 2px solid transparent; } -.negative { - color: color(null, 'error', 500) !important; +.rail-btn igx-icon { + font-size: 15px; } -.negative.strongNegative { - .igx-grid__td-text { - color: color(null, 'success', 500, .8) !important; - } +.rail-btn:hover { + color: var(--shell-rail-btn-hover); + background: rgba(129, 186, 255, 0.07); } -// NORMAL -// positive -.changePos { - .igx-grid__td-text { - background: color(null, 'success', 500, .5); - } +.rail-btn:disabled, +.rail-disabled { + opacity: 0.45; + cursor: not-allowed; } -.changePos1 { - background: color(null, 'success', 500, .5); - color: contrast-color(null, 'gray', 900); +.rail-btn:disabled:hover, +.rail-disabled:hover { + color: #b0bdd2; + background: transparent; } -.changePos2 { - .igx-grid__td-text { - border-inline-end: rem(4px) solid color(null, 'success', 500, .5); - padding-inline-end: rem(15px); - } +.rail-active { + color: var(--shell-rail-active-color); + background: var(--shell-rail-active-bg); + border-left-color: var(--shell-rail-active-border); } -// negative -.changeNeg { - .igx-grid__td-text { - background: color(null, 'error', 500, .5); - } +.rail-footer { + width: 100%; + border-top: 1px solid rgba(142, 164, 196, 0.15); + padding-top: 8px; } -.changeNeg1 { - background: color(null, 'error', 500, .5); - color: contrast-color(null, 'gray', 900); +.workspace { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; } -.changeNeg2 { - .igx-grid__td-text { - border-inline-end: rem(4px) solid color(null, 'error', 500, .5); - padding-inline-end: rem(9px); - } +.command-bar { + display: flex; + align-items: center; + gap: 14px; + height: 46px; + padding: 0 12px; + background: linear-gradient(180deg, var(--shell-command-bg-start) 0%, var(--shell-command-bg-end) 100%); + border-bottom: 1px solid var(--sd-border); + flex-shrink: 0; } -// STRONG -// positive -.strongPositive { - .igx-grid__td-text { - background: color(null, 'success', 500); - } +.brand { + font-size: 18px; + font-weight: 700; + color: var(--sd-text); + letter-spacing: -0.3px; + white-space: nowrap; } -.strongPositive1 { - background: color(null, 'success', 500); - color: contrast-color(null, 'gray', 900); +.search-wrap { + display: flex; + align-items: center; + gap: 8px; + width: 290px; + height: 32px; + border: 1px solid var(--shell-search-border); + border-radius: 6px; + padding: 0 10px; + color: var(--shell-search-icon); + background: var(--shell-search-bg); } -.strongPositive2 { - .igx-grid__td-text { - border-inline-end: rem(4px) solid color(null, 'success', 500); - padding-inline-end: rem(15px); - } +.search-wrap input { + width: 100%; + border: none; + outline: none; + background: transparent; + color: var(--sd-text); + font-size: 12px; } -// negative -.strongNegative { - .igx-grid__td-text { - background: color(null, 'error', 500); - color: contrast-color(null, 'gray', 900); - } +.search-wrap input::placeholder { + color: var(--sd-muted); + opacity: 1; } -.strongNegative1 { - background: color(null, 'error', 500); - color: contrast-color(null, 'gray', 900); +.indices-ribbon { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + overflow: hidden; } -.strongNegative2 { - .igx-grid__td-text { - border-inline-end: rem(4px) solid color(null, 'error', 500); - padding-inline-end: rem(9px); - } +.idx-item { + display: flex; + align-items: center; + gap: 5px; + white-space: nowrap; + font-size: 11px; } -igx-grid { - --ig-size: var(--ig-size-small); +.idx-name { color: var(--sd-muted); } +.idx-value { color: var(--sd-text); font-weight: 600; font-variant-numeric: tabular-nums; } +.pos { color: var(--sd-green); } +.neg { color: var(--sd-red); } - .grid-area { - margin-block-start: 1rem; - overflow-y: hidden; - overflow-x: hidden; - width: 100%; - } +.account-strip { + display: flex; + align-items: center; + gap: 14px; + flex-shrink: 0; +} - // selected - .igx-grid__td--column-selected.changePos1, - .igx-grid__td--column-selected.changePos2, - .igx-grid__td--column-selected.changePos { - background-color: color(null, 'success', 500) !important; +.acct-stat { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; +} - .finjs-icons, - .igx-grid__td-text { - color: contrast-color(null, 'gray', 900);; - } - } +.acct-stat span { + color: var(--sd-muted); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.4px; +} - .igx-grid__td--column-selected.changeNeg1, - .igx-grid__td--column-selected.changeNeg2, - .igx-grid__td--column-selected.changeNeg { - background-color: color(null, 'error', 500) !important; +.acct-stat strong { + color: var(--sd-text); + font-size: 18px; + line-height: 1; + font-variant-numeric: tabular-nums; + font-family: 'DM Mono', monospace; +} - .finjs-icons, - .igx-grid__td-text { - color: contrast-color(null, 'gray', 900); - } - } +.user-chip { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--shell-chip-bg); + border: 1px solid var(--shell-chip-border); + color: var(--shell-chip-text); + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 700; + cursor: pointer; + flex-shrink: 0; + outline: none; +} - // selected - .igx-grid__td--column-selected.strongPositive1, - .igx-grid__td--column-selected.strongPositive2, - .igx-grid__td--column-selected.strongPositive { - background-color: color(null, 'success', 500) !important; +.user-chip:focus-visible { + outline: 2px solid var(--sd-cyan); + outline-offset: 2px; +} - .finjs-icons, - .igx-grid__td-text { - color: contrast-color(null, 'gray', 900); - } - } +.main-area { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.dock-area { + flex: 1; + display: block; + min-height: 0; + overflow: hidden; +} - .igx-grid__td--column-selected.strongNegative1, - .igx-grid__td--column-selected.strongNegative2, - .igx-grid__td--column-selected.strongNegative { - background-color: color(null, 'error', 500) !important; +.pane-host { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + background: var(--sd-surface); +} - .finjs-icons, - .igx-grid__td-text { - color: contrast-color(null, 'gray', 900); - } - } +.theme-toggle-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: 1px solid var(--shell-toggle-border); + border-radius: 6px; + background: var(--shell-toggle-bg); + color: var(--shell-toggle-text); + cursor: pointer; + padding: 0; + transition: background 0.15s, color 0.15s, border-color 0.15s; + outline: none; + flex-shrink: 0; } -igx-select { - --ig-size: var(--ig-size-small); +.theme-toggle-btn:hover { + background: var(--shell-toggle-hover-bg); + color: var(--shell-toggle-hover-text); + border-color: var(--shell-toggle-hover-border); +} + +.theme-toggle-btn:focus-visible { + outline: 2px solid var(--sd-cyan); + outline-offset: 2px; +} + +.theme-toggle-btn igx-icon { + font-size: 14px; + line-height: 1; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.sr-only-panels { + display: none; +} + +:host, +.app-shell { + --sd-bg: #12121a; + --sd-surface: #12121a; + --sd-panel: #12121a; + --sd-border: #272735; + --sd-text: #f1f5ff; + --sd-muted: #b0bdd2; + --sd-green: #34d399; + --sd-green-dim: rgba(52, 211, 153, 0.1); + --sd-red: #ff4d6a; + --sd-red-dim: rgba(255, 77, 106, 0.1); + --sd-cyan: #a78bfa; + --sd-cyan-dim: rgba(167, 139, 250, 0.09); + --sd-amber: #fbbf24; + + --ig-surface-500: #0f1520; + --ig-gray-50: #0a0d14; + --ig-gray-100: #141e2e; + --ig-gray-200: #1e2d44; + --ig-gray-300: #273d58; + --ig-gray-700: #7a93b5; + --ig-gray-800: #a3bcd4; + --ig-gray-900: #e2e8f0; + + --ig-grid-header-background: #141e2e; + --ig-grid-header-text-color: #5e7798; + --ig-grid-content-background: #0f1520; + --ig-grid-row-odd-background: #0f1520; + --ig-grid-row-even-background: #141e2e; + --ig-grid-row-hover-background: rgba(255, 255, 255, 0.04); + --ig-grid-row-border-color: #1e2d44; +} + +igc-dockmanager { + color-scheme: dark; + --igc-background-color: var(--sd-bg); + --igc-accent-color: var(--sd-cyan); + --igc-text-color: var(--sd-text); + --igc-border-color: var(--sd-border); + --igc-pane-content-background: var(--sd-surface); + --igc-tab-background-active: var(--sd-panel); + --igc-splitter-background: var(--sd-border); + --igc-splitter-thickness: 4px; + --igc-floating-pane-border-color: var(--sd-border); + --igc-joystick-border-color: var(--sd-border); + --igc-joystick-icon-color-active: var(--sd-cyan); + --igc-context-menu-color-active: var(--sd-cyan); + --igc-flyout-shadow-color: rgba(0, 0, 0, 0.7); + --igc-sb-track-bg-color: var(--sd-panel); + --igc-sb-track-bg-color-hover: var(--sd-panel); + --igc-sb-thumb-bg-color: var(--sd-border); + --igc-sb-thumb-bg-color-hover: var(--sd-muted); + --igc-sb-corner-bg: var(--sd-panel); +} + +igc-dockmanager::part(pane-header) { + background: var(--sd-panel); + color: var(--sd-text); +} + +igc-dockmanager::part(pane-header-content) { + color: var(--sd-text); +} + +igc-dockmanager::part(pane-header-close-button), +igc-dockmanager::part(pane-header-maximize-button), +igc-dockmanager::part(pane-header-minimize-button), +igc-dockmanager::part(pane-header-pin-button), +igc-dockmanager::part(pane-header-unpin-button), +igc-dockmanager::part(tab-header-close-button), +igc-dockmanager::part(tab-header-more-options) { + color: var(--sd-text); + opacity: 0.9; + border: none; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +igc-dockmanager::part(pane-header-close-button):hover, +igc-dockmanager::part(pane-header-maximize-button):hover, +igc-dockmanager::part(pane-header-minimize-button):hover, +igc-dockmanager::part(pane-header-pin-button):hover, +igc-dockmanager::part(pane-header-unpin-button):hover, +igc-dockmanager::part(tab-header-close-button):hover, +igc-dockmanager::part(tab-header-more-options):hover { + opacity: 1; + background: transparent; +} + +.light-theme { + color-scheme: light; + --sd-bg: #f0f4f8; + --sd-surface: #ffffff; + --sd-panel: #f8fafc; + --sd-border: #e2e8f0; + --sd-text: #0f172a; + --sd-muted: #334155; + --sd-green: #16a34a; + --sd-green-dim: rgba(22, 163, 74, 0.1); + --sd-red: #dc2626; + --sd-red-dim: rgba(220, 38, 38, 0.1); + --sd-cyan: #0284c7; + --sd-cyan-dim: rgba(2, 132, 199, 0.08); + + --ig-surface-500: #ffffff; + --ig-gray-50: #fafafa; + --ig-gray-100: #f1f5f9; + --ig-gray-200: #e2e8f0; + --ig-gray-300: #cbd5e1; + --ig-gray-700: #475569; + --ig-gray-800: #334155; + --ig-gray-900: #0f172a; + + --ig-grid-header-background: #f1f5f9; + --ig-grid-header-text-color: #475569; + --ig-grid-content-background: #ffffff; + --ig-grid-row-odd-background: #ffffff; + --ig-grid-row-even-background: #f8fafc; + --ig-grid-row-hover-background: rgba(0, 0, 0, 0.04); + --ig-grid-row-border-color: #e2e8f0; +} + +.light-theme igc-dockmanager { + color-scheme: light; + --igc-pane-content-background: #ffffff; + --igc-tab-background-active: #f8fafc; + --igc-border-color: #e2e8f0; +} + +.light-theme igc-dockmanager::part(pane-header-close-button), +.light-theme igc-dockmanager::part(pane-header-maximize-button), +.light-theme igc-dockmanager::part(pane-header-minimize-button), +.light-theme igc-dockmanager::part(pane-header-pin-button), +.light-theme igc-dockmanager::part(pane-header-unpin-button), +.light-theme igc-dockmanager::part(tab-header-close-button), +.light-theme igc-dockmanager::part(tab-header-more-options) { + color: #0f172a; + opacity: 1; + border: none; + background: transparent; + filter: brightness(0) saturate(100%); +} + +.light-theme igc-dockmanager::part(pane-header-close-button):hover, +.light-theme igc-dockmanager::part(pane-header-maximize-button):hover, +.light-theme igc-dockmanager::part(pane-header-minimize-button):hover, +.light-theme igc-dockmanager::part(pane-header-pin-button):hover, +.light-theme igc-dockmanager::part(pane-header-unpin-button):hover, +.light-theme igc-dockmanager::part(tab-header-close-button):hover, +.light-theme igc-dockmanager::part(tab-header-more-options):hover { + background: transparent; + filter: brightness(0) saturate(100%); +} + +@media (max-width: 1240px) { + .account-strip { + display: none; + } + + .search-wrap { + width: 210px; + } } diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.ts b/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.ts index 35951d546..844b8a6e1 100644 --- a/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.ts +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/grid-finjs-dock-manager.component.ts @@ -1,328 +1,214 @@ -/* eslint-disable max-len */ -import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Renderer2, OnDestroy, OnInit, DoCheck, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation, CUSTOM_ELEMENTS_SCHEMA, inject } from '@angular/core'; -import { AbsoluteScrollStrategy, ConnectedPositioningStrategy, DefaultSortingStrategy, GridColumnDataType, IgxOverlayOutletDirective, OverlaySettings, SortingDirection } from 'igniteui-angular/core'; -import { IgxCellTemplateDirective, IgxColumnComponent } from 'igniteui-angular/grids/core'; -import { IgxGridComponent } from 'igniteui-angular/grids/grid'; -import { IgxSelectComponent, IgxSelectItemComponent } from 'igniteui-angular/select'; -import { IgxSwitchComponent } from 'igniteui-angular/switch'; -import { IgxLabelDirective, IgxPrefixDirective } from 'igniteui-angular/input-group'; +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation, inject, signal } from '@angular/core'; +import { IgcDockManagerLayout, IgcDockManagerPaneType, IgcSplitPaneOrientation } from 'igniteui-dockmanager'; import { IgxIconComponent } from 'igniteui-angular/icon'; -import { IgxButtonDirective } from 'igniteui-angular/directives'; -import { IgxPaginatorComponent } from 'igniteui-angular/paginator'; -import { IgcDockManagerLayout, IgcDockManagerPaneType, IgcSplitPane, IgcSplitPaneOrientation } from 'igniteui-dockmanager'; -import { Subject } from 'rxjs'; -import { first, takeUntil } from 'rxjs/operators'; -import { FloatingPanesService } from '../services/floating-panes.service'; -import { SignalRService } from '../services/signal-r.service'; -import { DockSlotComponent, GridHostDirective } from './dock-slot.component'; -import { FormsModule } from '@angular/forms'; -import { AsyncPipe, CurrencyPipe } from '@angular/common'; -@Component({ - encapsulation: ViewEncapsulation.None, - providers: [SignalRService, FloatingPanesService], - selector: 'app-finjs-dock-manager', - templateUrl: './grid-finjs-dock-manager.component.html', - styleUrls: ['./grid-finjs-dock-manager.component.scss'], - imports: [IgxSwitchComponent, FormsModule, IgxSelectComponent, IgxLabelDirective, IgxPrefixDirective, IgxIconComponent, IgxSelectItemComponent, IgxButtonDirective, IgxOverlayOutletDirective, IgxGridComponent, IgxColumnComponent, IgxCellTemplateDirective, IgxPaginatorComponent, GridHostDirective, AsyncPipe, CurrencyPipe], - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class GridFinJSDockManagerComponent implements OnInit, OnDestroy, AfterViewInit, DoCheck { - dataService = inject(SignalRService); - private paneService = inject(FloatingPanesService); - private cdr = inject(ChangeDetectorRef); - private elementRef = inject(ElementRef); - private renderer = inject(Renderer2); - - @ViewChild('grid1', { static: true }) public grid1: IgxGridComponent; - @ViewChild('grid2', { static: true }) public grid2: IgxGridComponent; - @ViewChild(GridHostDirective) public host: GridHostDirective; - @ViewChild('dock', { read: ElementRef }) public dockManager: ElementRef; - @ViewChild('priceTemplate', { read: TemplateRef }) - public priceTemplate: TemplateRef; - @ViewChild(IgxSelectComponent) public select: IgxSelectComponent; - @ViewChild('freq', { read: IgxSelectComponent }) public selectFrequency: IgxSelectComponent; - @ViewChild(IgxOverlayOutletDirective) outlet: IgxOverlayOutletDirective; - - public isDarkTheme = true; - - public frequencyItems: number[] = [300, 600, 900]; - public frequency = this.frequencyItems[1]; - public dataVolumeItems: number[] = [100, 500, 1000, 5000, 10000]; - public dataVolume: number = this.dataVolumeItems[1]; - public isLoading = true; - public data: any; - public liveData = true; - public columnFormat = { digitsInfo: '1.3-3'}; - public columnFormatChangeP = { digitsInfo: '2.3-3'}; - public slotCounter = 1; - public customOverlaySettings: OverlaySettings = { - positionStrategy: new ConnectedPositioningStrategy(), - scrollStrategy: new AbsoluteScrollStrategy() - }; - public freqOverlaySettings: OverlaySettings = { - positionStrategy: new ConnectedPositioningStrategy(), - scrollStrategy: new AbsoluteScrollStrategy() - }; - - public docLayout: IgcDockManagerLayout = { +import { WatchlistComponent } from './watchlist/watchlist.component'; +import { ChartPanelComponent } from './chart-panel/chart-panel.component'; +import { OrderTicketComponent } from './order-ticket/order-ticket.component'; +import { PositionsGridComponent } from './positions-grid/positions-grid.component'; +import { NewsPanelComponent } from './news-panel/news-panel.component'; +import { AppDataStore } from './app-data-store'; + +const APP_SHELL_FIXED_WIDTH = 58; +const COMMAND_BAR_HEIGHT = 46; +const APP_CHROME_VERTICAL_BORDERS = 2; + +export function buildLayout(viewportWidth: number, viewportHeight: number): IgcDockManagerLayout { + const safeViewportWidth = Number.isFinite(viewportWidth) ? viewportWidth : 1440; + const safeViewportHeight = Number.isFinite(viewportHeight) ? viewportHeight : 900; + const minWatchlistSize = 300; + const minInsightsPanelWidth = 300; + const minChartSize = 400; + const orderTicketHeightPx = 320; + const minOrderTicketHeight = orderTicketHeightPx; + const minMarketNewsHeight = 100; + const minPositionsHeight = 42; + const minInsightsPanelHeight = minOrderTicketHeight + minPositionsHeight + minMarketNewsHeight; + const stackSizingReferenceHeight = Math.max( + 1, + safeViewportHeight - COMMAND_BAR_HEIGHT - APP_CHROME_VERTICAL_BORDERS + ); + const orderTicketSize = Math.min(0.9, Math.max(0.05, orderTicketHeightPx / stackSizingReferenceHeight)); + const remainingPanelSectionSize = (1 - orderTicketSize) / 2; + const targetWatchlistSize = safeViewportWidth < 768 ? minWatchlistSize : 400; + const targetInsightsPanelWidth = safeViewportWidth < 768 ? minInsightsPanelWidth : 400; + const availableForSidePanes = Math.max(320, safeViewportWidth - APP_SHELL_FIXED_WIDTH - minChartSize); + const maxEachSidePaneWidth = Math.max(minInsightsPanelWidth, Math.floor(availableForSidePanes / 2)); + const watchlistSize = Math.min(targetWatchlistSize, maxEachSidePaneWidth); + const insightsPanelWidth = Math.min( + targetInsightsPanelWidth, + Math.max(minInsightsPanelWidth, availableForSidePanes - watchlistSize) + ); + const chartSize = Math.max(minChartSize, safeViewportWidth - APP_SHELL_FIXED_WIDTH - watchlistSize - insightsPanelWidth); + + return { rootPane: { type: IgcDockManagerPaneType.splitPane, orientation: IgcSplitPaneOrientation.horizontal, panes: [ { type: IgcDockManagerPaneType.contentPane, - contentId: 'actionPane', - header: 'Actions pane', - size: 20, - isPinned: false, - allowClose: false + contentId: 'watchlist', + header: 'Watchlist', + size: watchlistSize, + minResizeWidth: minWatchlistSize, + allowPinning: false }, { - size: 50, - type: IgcDockManagerPaneType.contentPane, - contentId: 'gridStockPrices', - header: 'Stock Market Data', - allowClose: false + type: IgcDockManagerPaneType.documentHost, + size: chartSize, + rootPane: { + type: IgcDockManagerPaneType.splitPane, + orientation: IgcSplitPaneOrientation.horizontal, + allowEmpty: true, + panes: [{ + type: IgcDockManagerPaneType.tabGroupPane, + panes: [ + { type: IgcDockManagerPaneType.contentPane, header: 'AAPL', contentId: 'chartAAPL', documentOnly: true }, + { type: IgcDockManagerPaneType.contentPane, header: 'TSLA', contentId: 'chartTSLA', documentOnly: true }, + { type: IgcDockManagerPaneType.contentPane, header: 'NVDA', contentId: 'chartNVDA', documentOnly: true } + ] + }] + } }, { type: IgcDockManagerPaneType.splitPane, orientation: IgcSplitPaneOrientation.vertical, - size: 50, + size: insightsPanelWidth, + minResizeWidth: minInsightsPanelWidth, + minResizeHeight: minInsightsPanelHeight, panes: [ { - type: IgcDockManagerPaneType.documentHost, - size: 50, - rootPane: { - type: IgcDockManagerPaneType.splitPane, - orientation: IgcSplitPaneOrientation.horizontal, - panes: [ - { - type: IgcDockManagerPaneType.tabGroupPane, - panes: [ - { - type: IgcDockManagerPaneType.contentPane, - contentId: 'forexMarket', - header: 'Market Data 1' - }, - { - type: IgcDockManagerPaneType.contentPane, - contentId: 'content4', - header: 'Market Data 2' - } - ] - } - ] - }}, - { - type: IgcDockManagerPaneType.contentPane, - contentId: 'etfStockPrices', - header: 'Market Data 3', - size: 50, - allowClose: false - } + type: IgcDockManagerPaneType.contentPane, + contentId: 'orderTicket', + header: 'Order Ticket', + size: orderTicketSize, + minResizeHeight: minOrderTicketHeight, + allowPinning: false + }, + { + type: IgcDockManagerPaneType.contentPane, + contentId: 'positions', + header: 'Positions & P&L', + size: remainingPanelSectionSize, + minResizeHeight: minPositionsHeight, + allowPinning: false + }, + { + type: IgcDockManagerPaneType.contentPane, + contentId: 'news', + header: 'Market News', + size: remainingPanelSectionSize, + minResizeHeight: minMarketNewsHeight, + allowPinning: false + } ] } ] - }, - floatingPanes: [] + } }; - - public columns: { field: string, - width: string, - sortable: boolean, - filterable: boolean, - type: GridColumnDataType, - groupable?: boolean, - cellClasses?: string, - bodyTemplate?: string } [] = [ - { field: 'buy', width: '110px', sortable: false, filterable: false, type: 'currency' }, - { field: 'sell', width: '110px', sortable: false, filterable: false, type: 'currency' }, - { field: 'openPrice', width: '120px', sortable: true, filterable: true, type: 'currency'}, - { field: 'lastUpdated', width: '120px', sortable: true, filterable: true, type: 'date'}, - { field: 'spread', width: '110px', sortable: false, filterable: false, type: 'number' }, - { field: 'volume', width: '110px', sortable: true, filterable: false, type: 'number' }, - { field: 'settlement', width: '100px', sortable: true, filterable: true, type: 'string', groupable: true }, - { field: 'country', width: '100px', sortable: true, filterable: true, type: 'string'}, - { field: 'highD', width: '110px', sortable: true, filterable: false, type: 'currency' }, - { field: 'lowD', width: '110px', sortable: true, filterable: false, type: 'currency' }, - { field: 'highY', width: '110px', sortable: true, filterable: false, type: 'currency' }, - { field: 'lowY', width: '110px', sortable: true, filterable: false, type: 'currency' }, - { field: 'startY', width: '110px', sortable: true, filterable: false, type: 'currency' }, - { field: 'indGrou', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'indSect', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'indSubg', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'secType', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'issuerN', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'moodys', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'fitch', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'dbrs', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'collatT', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'curncy', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'security', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'sector', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'cusip', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'ticker', width: '136px', sortable: false, filterable: false, type: 'string'}, - { field: 'cpn', width: '136px', sortable: false, filterable: false, type: 'string'} - ]; - - private destroy$ = new Subject(); - - public ngOnInit() { - this.dataService.startConnection(this.frequency, this.dataVolume, true, false); - this.data = this.dataService.data; - this.data.pipe(takeUntil(this.destroy$)).subscribe((data) => { - if (data.length !== 0) { - this.isLoading = false; - this.cdr.markForCheck(); - }; - }); +} +@Component({ + selector: 'app-finjs-dock-manager', + imports: [IgxIconComponent, WatchlistComponent, ChartPanelComponent, OrderTicketComponent, PositionsGridComponent, NewsPanelComponent], + templateUrl: './grid-finjs-dock-manager.component.html', + styleUrls: ['./grid-finjs-dock-manager.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GridFinJSDockManagerComponent { + private readonly appDataStore = inject(AppDataStore); + private readonly enabledNavLabels = new Set(['Watchlist']); + + public readonly isDark = signal(true); + public readonly activeNav = signal('Watchlist'); + public readonly activeStrategyIndex = signal(0); + public readonly activeSymbolIndex = signal(0); + public readonly searchQuery = signal(''); + + public readonly strategyTabs = ['Basic Trading', 'Options Trading', 'Analysis']; + public readonly symbolTabs = ['AAPL', 'TSLA', 'NVDA', 'Option Chain']; + public readonly navItems = this.appDataStore.navItems; + public readonly indices = this.appDataStore.indices; + public readonly layout = signal(buildLayout( + typeof window === 'undefined' ? 1440 : window.innerWidth, + typeof window === 'undefined' ? 900 : window.innerHeight + )); + + public toggleTheme(): void { + this.isDark.update(v => !v); } - public ngOnDestroy() { - this.dataService.stopLiveData(); - this.destroy$.next(true); - this.destroy$.complete(); + public isNavEnabled(label: string): boolean { + return this.enabledNavLabels.has(label); } - public ngDoCheck() { - if (this.isDarkTheme) { - this.renderer.removeClass(this.elementRef.nativeElement, 'light-theme'); - this.renderer.addClass(this.elementRef.nativeElement, 'dark-theme'); - } - else { - this.renderer.removeClass(this.elementRef.nativeElement, 'dark-theme'); - this.renderer.addClass(this.elementRef.nativeElement, 'light-theme'); + public setActiveNav(label: string): void { + if (!this.isNavEnabled(label)) { + return; } + + this.activeNav.set(label); } - public ngAfterViewInit() { - // This 500ms timeout is used as a workaround for StackBlitz ExpressionChangedAfterItHasBeenChecked Error - setTimeout(() => { - const x = (this.dockManager.nativeElement.getBoundingClientRect().width / 3); - const y = (this.dockManager.nativeElement.getBoundingClientRect().height / 3); + public updateSearchQuery(event: Event): void { + const target = event.target; + if (!(target instanceof HTMLInputElement)) { + return; + } - this.paneService.initialPanePosition = { x, y }; - this.grid2.selectColumns(['price', 'change', 'changeP']); - this.customOverlaySettings.target = this.select.inputGroup.element.nativeElement; - this.customOverlaySettings.outlet = this.outlet; - this.freqOverlaySettings.target = this.selectFrequency.inputGroup.element.nativeElement; - this.freqOverlaySettings.outlet = this.outlet; - this.grid1.groupingExpressions = [{ - dir: SortingDirection.Desc, - fieldName: 'category', - ignoreCase: false, - strategy: DefaultSortingStrategy.instance() - }, - { - dir: SortingDirection.Desc, - fieldName: 'type', - ignoreCase: false, - strategy: DefaultSortingStrategy.instance() - }, - { - dir: SortingDirection.Desc, - fieldName: 'settlement', - ignoreCase: false, - strategy: DefaultSortingStrategy.instance() - }]; - this.cdr.markForCheck(); - }, 500); + this.searchQuery.set(target.value); } - public paramsChanged() { - this.dataService.hasRemoteConnection ? this.dataService.broadcastParams(this.frequency, this.dataVolume, true, false) : - this.dataService.startConnection(this.frequency, this.dataVolume, true, false); - this.data = this.dataService.data; + public setStrategyTab(index: number): void { + this.activeStrategyIndex.set(index); } - public stopFeed() { - this.dataService.stopLiveData(); + public setSymbolTab(index: number): void { + this.activeSymbolIndex.set(index); } - public streamData(event) { - event.checked ? this.paramsChanged() : this.stopFeed(); - this.liveData = event.checked; + public onStrategyTabKeydown(event: KeyboardEvent, index: number): void { + this.handleTabKeydown(event, index, this.strategyTabs.length, 'strategy-tab', i => this.activeStrategyIndex.set(i)); } - /* eslint-disable @typescript-eslint/member-ordering */ - /** Grid CellStyles and CellClasses */ - private negative = (rowData: any): boolean => rowData['changeP'] < 0; - private positive = (rowData: any): boolean => rowData['changeP'] > 0; - private changeNegative = (rowData: any): boolean => rowData['changeP'] < 0 && rowData['changeP'] > -1; - private changePositive = (rowData: any): boolean => rowData['changeP'] > 0 && rowData['changeP'] < 1; - private strongPositive = (rowData: any): boolean => rowData['changeP'] >= 1; - private strongNegative = (rowData: any, key: string): boolean => rowData['changeP'] <= -1; - - public trends = { - changeNeg: this.changeNegative, - changePos: this.changePositive, - negative: this.negative, - positive: this.positive, - strongNegative: this.strongNegative, - strongPositive: this.strongPositive - }; - - public trendsChange = { - changeNeg2: this.changeNegative, - changePos2: this.changePositive, - strongNegative2: this.strongNegative, - strongPositive2: this.strongPositive - }; - - public createGrid() { - const id: string = 'slot-' + this.slotCounter++; - const splitPane: IgcSplitPane = { - type: IgcDockManagerPaneType.splitPane, - orientation: IgcSplitPaneOrientation.horizontal, - floatingWidth: 550, - floatingHeight: 350, - panes: [ - { - type: IgcDockManagerPaneType.contentPane, - header: id, - contentId: id - } - ] - }; - this.paneService.appendPane(splitPane); - this.dockManager.nativeElement.layout.floatingPanes.push(splitPane); - this.docLayout = { ...this.dockManager.nativeElement.layout }; - this.cdr.detectChanges(); - - // Create Dock Slot Component - const dockSlotComponent = this.host.viewContainerRef.createComponent(DockSlotComponent); - dockSlotComponent.instance.id = id; - dockSlotComponent.instance.viewInit.pipe(first()).subscribe(() => { - const gridViewContainerRef = dockSlotComponent.instance.gridHost.viewContainerRef; - this.loadGridComponent(gridViewContainerRef, dockSlotComponent.instance.destroy$); - }); + public onSymbolTabKeydown(event: KeyboardEvent, index: number): void { + this.handleTabKeydown(event, index, this.symbolTabs.length, 'symbol-tab', i => this.activeSymbolIndex.set(i)); } - public loadGridComponent(viewContainerRef: ViewContainerRef, destructor: Subject) { - viewContainerRef.clear(); + private handleTabKeydown( + event: KeyboardEvent, + index: number, + total: number, + idPrefix: string, + selectTab: (nextIndex: number) => void + ): void { + const key = event.key; + + if (key === ' ' || key === 'Enter') { + event.preventDefault(); + selectTab(index); + return; + } - const componentRef = viewContainerRef.createComponent(IgxGridComponent); - const grid = (componentRef.instance as IgxGridComponent); - grid.autoGenerate = true; - this.dataService.data.pipe(takeUntil(destructor)).subscribe(d => { - grid.data = d; - componentRef.changeDetectorRef.markForCheck(); - }); - grid.columnInit.pipe(takeUntil(destructor)).subscribe((col: IgxColumnComponent) => { - if (col.field === 'price') { - col.cellClasses = this.trends; - col.bodyTemplate = this.priceTemplate; - } - if (col.field === 'change' || col.field === 'changeP') { - col.cellClasses = this.trendsChange; - } - }); - grid.columnSelection = 'multiple'; - grid.cellSelection = 'none'; + let nextIndex = index; + + if (key === 'ArrowRight') { + nextIndex = (index + 1) % total; + } else if (key === 'ArrowLeft') { + nextIndex = (index - 1 + total) % total; + } else if (key === 'Home') { + nextIndex = 0; + } else if (key === 'End') { + nextIndex = total - 1; + } else { + return; + } - // Use detectChanges because of ExpressionChangedAfterItHasBeenChecked Error when creating a dynamic pane - this.cdr.detectChanges(); + event.preventDefault(); + selectTab(nextIndex); + if (typeof document !== 'undefined') { + document.getElementById(`${idPrefix}-${nextIndex}`)?.focus(); + } } - - /* eslint-enable @typescript-eslint/member-ordering */ } diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/news-panel/news-panel.component.html b/projects/app-lob/src/app/grid-finjs-dock-manager/news-panel/news-panel.component.html new file mode 100644 index 000000000..e7a9f6a3b --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/news-panel/news-panel.component.html @@ -0,0 +1,49 @@ +
+
+ + +
+ +
+ @if (activeTab() === 'news') { +
+ @for (item of newsItems; track item.id) { +
+
{{ item.headline }}
+
{{ item.source }} · {{ item.time }}
+
+ } +
+ } @else { +
+ @for (alert of alerts; track alert.id) { +
+ {{ alert.icon }} +
+
{{ alert.title }}
+
{{ alert.desc }}
+
+
+ } +
+ } +
+
diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/news-panel/news-panel.component.scss b/projects/app-lob/src/app/grid-finjs-dock-manager/news-panel/news-panel.component.scss new file mode 100644 index 000000000..0de198482 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/news-panel/news-panel.component.scss @@ -0,0 +1,132 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + background: var(--sd-surface); +} + +.news-tabs { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +} + +.news-tab-headers { + display: flex; + align-items: center; + border-bottom: 1px solid var(--sd-border); + background: var(--sd-panel); +} + +.news-tab-btn { + border: none; + background: transparent; + color: var(--sd-muted); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.8px; + height: 36px; + padding: 0 14px; + border-bottom: 2px solid transparent; + cursor: pointer; +} + +.news-tab-btn:focus-visible { + outline: 2px solid var(--sd-cyan); + outline-offset: -2px; +} + +.news-tab-active { + color: var(--sd-cyan); + border-bottom-color: var(--sd-cyan); +} + +.news-content { + flex: 1; + min-height: 0; + overflow-y: auto; + scrollbar-color: var(--sd-border) var(--sd-panel); + scrollbar-width: auto; +} + +.news-content::-webkit-scrollbar { + width: 14px; +} + +.news-content::-webkit-scrollbar-track { + background: var(--sd-panel); +} + +.news-content::-webkit-scrollbar-thumb { + background: var(--sd-border); + border-radius: 8px; +} + +.news-content::-webkit-scrollbar-thumb:hover { + background: var(--sd-muted); +} + +.item-list { + padding: 0; +} + +.news-item, +.alert-item { + padding: 10px 12px; + border-bottom: 1px solid var(--sd-border); +} + +.alert-item { + display: grid; + grid-template-columns: 20px 1fr; + align-items: start; + gap: 10px; +} + +.alert-icon { + font-size: 16px; + line-height: 1; + margin-top: 1px; +} + +.alert-title { + color: var(--sd-text); + font-size: 12px; + line-height: 1.3; +} + +.alert-desc { + color: var(--sd-muted); + font-size: 11px; + line-height: 1.35; +} + +.news-headline { + font-size: 12px; + line-height: 1.4; + color: var(--sd-text); + white-space: normal; +} + +.news-meta { + font-size: 11px; + color: var(--sd-muted); +} + +.alert-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--sd-red); + color: #fff; + font-size: 10px; + font-weight: 700; + margin-left: 6px; + vertical-align: middle; +} diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/news-panel/news-panel.component.ts b/projects/app-lob/src/app/grid-finjs-dock-manager/news-panel/news-panel.component.ts new file mode 100644 index 000000000..66a012c3e --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/news-panel/news-panel.component.ts @@ -0,0 +1,49 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { IgxIconComponent } from 'igniteui-angular/icon'; + +interface NewsItem { + id: number; + headline: string; + source: string; + time: string; +} + +interface AlertItem { + id: number; + title: string; + desc: string; + icon: string; + iconColor: string; +} + +@Component({ + selector: 'app-news-panel', + standalone: true, + imports: [IgxIconComponent], + templateUrl: './news-panel.component.html', + styleUrls: ['./news-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NewsPanelComponent { + public readonly activeTab = signal<'news' | 'alerts'>('news'); + + public readonly newsItems: NewsItem[] = [ + { id: 1, headline: 'Apple Beats Q3 Estimates on Services Growth', source: 'Reuters', time: '2m' }, + { id: 2, headline: 'Fed Signals Possible Rate Cut in September', source: 'Bloomberg', time: '14m' }, + { id: 3, headline: 'Tesla Deliveries Miss Street Expectations', source: 'CNBC', time: '32m' }, + { id: 4, headline: 'Nvidia Unveils Next-Gen AI Chip Architecture', source: 'TechWire', time: '1h' }, + { id: 5, headline: 'Oil Prices Slide on Supply Glut Concerns', source: 'MarketWatch', time: '2h' }, + { id: 6, headline: 'Microsoft Azure Revenue Jumps 29% Year-over-Year', source: 'WSJ', time: '3h' }, + { id: 7, headline: 'S&P 500 Closes at Record High for Third Week', source: 'Barrons', time: '4h' } + ]; + + public readonly alerts: AlertItem[] = [ + { id: 1, title: 'AAPL Target Reached', desc: 'Price crossed $225.00 target', icon: 'check_circle', iconColor: '#00e676' }, + { id: 2, title: 'VIX Spike Warning', desc: 'Volatility index above 20', icon: 'warning', iconColor: '#ffc107' }, + { id: 3, title: 'TSLA Down 5%', desc: 'Stop-loss level approaching', icon: 'trending_down', iconColor: '#ff5252' } + ]; + + public setActiveTab(tab: 'news' | 'alerts'): void { + this.activeTab.set(tab); + } +} diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/order-ticket/order-ticket.component.html b/projects/app-lob/src/app/grid-finjs-dock-manager/order-ticket/order-ticket.component.html new file mode 100644 index 000000000..eaf32644c --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/order-ticket/order-ticket.component.html @@ -0,0 +1,63 @@ +
+
+ + +
+ +
+
+
+ + + + +
+ +
+ + + @for (t of orderTypes; track t) { + {{ t }} + } + +
+
+ +
+ + + $ + + +
+ +
+ Est. total + ${{ estTotal() }} +
+ + +
+
diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/order-ticket/order-ticket.component.scss b/projects/app-lob/src/app/grid-finjs-dock-manager/order-ticket/order-ticket.component.scss new file mode 100644 index 000000000..bf8fe3432 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/order-ticket/order-ticket.component.scss @@ -0,0 +1,143 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; + background: var(--sd-surface); +} + +.ticket-container { + display: flex; + flex-direction: column; + padding: 10px; + gap: 10px; + height: 100%; + overflow: hidden; + box-sizing: border-box; +} + +.bs-toggle { + display: grid; + grid-template-columns: 1fr 1fr; + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--sd-border); + gap: 8px; + padding: 2px; + background: transparent; +} + +.bs-btn { + border-radius: 8px; + font-weight: 600; + font-size: 12px; + height: 34px; + background: var(--sd-panel); + color: var(--sd-muted); + border: 1px solid var(--sd-border); + cursor: pointer; + transition: background 0.15s, color 0.15s; + box-shadow: none; +} + +.bs-btn:hover { + color: var(--sd-text); +} + +.bs-active-buy { + background: var(--sd-green) !important; + color: #000 !important; +} + +.bs-active-sell { + background: var(--sd-red) !important; + color: #fff !important; +} + +.ticket-form { + display: flex; + flex-direction: column; + gap: 8px; +} + +.inline-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.form-row { + display: flex; + flex-direction: column; + gap: 4px; +} + +.field-label { + font-size: 10px; + color: var(--sd-muted); + letter-spacing: 0.2px; +} + +.form-field { + width: 100%; +} + +.small-field { + --ig-size: var(--ig-size-small); +} + +.prefix-dollar { + color: var(--sd-muted); + font-size: 12px; +} + +.est-total-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 2px 0 4px; + background: transparent; + border: none; +} + +.est-label { + font-size: 11px; + color: var(--sd-muted); +} + +.est-value { + font-size: 13px; + font-weight: 600; + color: var(--sd-text); + font-variant-numeric: tabular-nums; + font-family: 'DM Mono', monospace; +} + +.place-btn { + width: 100%; + height: 38px; + font-size: 12px; + font-weight: 700; + border-radius: 8px; + letter-spacing: 0.3px; +} + +@media (max-width: 520px) { + .inline-row { + grid-template-columns: 1fr; + } +} + +.buy-action { + background: var(--sd-green) !important; + color: #000 !important; +} + +.sell-action { + background: var(--sd-red) !important; + color: #fff !important; +} + +.place-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/order-ticket/order-ticket.component.ts b/projects/app-lob/src/app/grid-finjs-dock-manager/order-ticket/order-ticket.component.ts new file mode 100644 index 000000000..6e0998c4d --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/order-ticket/order-ticket.component.ts @@ -0,0 +1,54 @@ +import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { IgxButtonDirective } from 'igniteui-angular/directives'; +import { IgxInputDirective, IgxInputGroupComponent, IgxPrefixDirective } from 'igniteui-angular/input-group'; +import { IgxSelectComponent, IgxSelectItemComponent } from 'igniteui-angular/select'; + +@Component({ + selector: 'app-order-ticket', + standalone: true, + imports: [ + ReactiveFormsModule, + IgxInputGroupComponent, + IgxInputDirective, + IgxPrefixDirective, + IgxSelectComponent, + IgxSelectItemComponent, + IgxButtonDirective + ], + templateUrl: './order-ticket.component.html', + styleUrls: ['./order-ticket.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class OrderTicketComponent { + public readonly isBuy = signal(true); + public readonly symbol = signal('AAPL'); + + public readonly form = new FormGroup({ + quantity: new FormControl(100, [Validators.required, Validators.min(1)]), + orderType: new FormControl('Limit', Validators.required), + limitPrice: new FormControl(227.14, [Validators.required, Validators.min(0.01)]) + }); + + public readonly orderTypes = ['Market', 'Limit', 'Stop', 'Stop Limit', 'Trailing Stop']; + + private readonly formValue = toSignal(this.form.valueChanges, { + initialValue: this.form.getRawValue() + }); + + public readonly estTotal = computed(() => { + const v = this.formValue(); + const qty = v?.quantity ?? 0; + const price = v?.limitPrice ?? 0; + return (qty * price).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + }); + + public readonly btnLabel = computed(() => `Place ${this.isBuy() ? 'Buy' : 'Sell'} Order`); + + public placeOrder(): void { + if (this.form.invalid) return; + const v = this.form.getRawValue(); + console.log(`${this.isBuy() ? 'BUY' : 'SELL'} ${v.quantity} ${this.symbol()} @ ${v.orderType} ${v.limitPrice}`); + } +} diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/positions-grid/positions-grid.component.html b/projects/app-lob/src/app/grid-finjs-dock-manager/positions-grid/positions-grid.component.html new file mode 100644 index 000000000..e76898fa1 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/positions-grid/positions-grid.component.html @@ -0,0 +1,47 @@ +
+ + + + {{ cell.value }} + + + + + + + + + ${{ cell.value.toFixed(2) }} + + + + + +
+ {{ cell.row.data.plFormatted }} + {{ cell.row.data.plPct }} +
+
+
+
+
+ +
+ Total P&L + + {{ totalPL }} + +
diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/positions-grid/positions-grid.component.scss b/projects/app-lob/src/app/grid-finjs-dock-manager/positions-grid/positions-grid.component.scss new file mode 100644 index 000000000..8b9692d92 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/positions-grid/positions-grid.component.scss @@ -0,0 +1,87 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + background: var(--sd-surface); +} + +.grid-host { + flex: 1; + min-height: 0; + overflow: hidden; + border-bottom: 1px solid var(--sd-border); +} + +.positions-grid { + --ig-size: var(--ig-size-small); + --ig-grid-header-background: var(--sd-panel); + --ig-grid-header-text-color: var(--sd-muted); + --ig-grid-content-background: var(--sd-surface); + --ig-grid-row-odd-background: var(--sd-surface); + --ig-grid-row-even-background: var(--sd-panel); + --ig-grid-row-hover-background: rgba(129, 186, 255, 0.08); + --ig-grid-row-border-color: rgba(124, 155, 197, 0.14); + font-size: 12px; + --sb-thumb-bg-color: var(--sd-border); + --sb-thumb-bg-color-hover: var(--sd-muted); + --sb-thumb-bg-color-active: var(--sd-muted); +} + +.symbol-cell { + font-weight: 700; + color: #8cc8ff; + letter-spacing: 0.2px; +} + +.mono { + font-variant-numeric: tabular-nums; + font-family: 'DM Mono', 'Courier New', monospace; +} + +.pl-cell { + display: flex; + flex-direction: column; + gap: 2px; +} + +.pl-amount { + font-size: 13px; + font-weight: 600; + font-variant-numeric: tabular-nums; + font-family: 'DM Mono', 'Courier New', monospace; +} + +.pl-pct { + font-size: 11px; + opacity: 0.95; + font-variant-numeric: tabular-nums; +} + +.pos-pnl { color: var(--sd-green); } +.neg-pnl { color: var(--sd-red); } + +.total-bar { + flex-shrink: 0; + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: var(--sd-panel); + border-top: 1px solid var(--sd-border); +} + +.total-label { + font-size: 10px; + font-weight: 600; + color: var(--sd-muted); + text-transform: uppercase; + letter-spacing: 0.8px; +} + +.total-value { + font-size: 13px; + font-weight: 700; + font-variant-numeric: tabular-nums; + font-family: 'DM Mono', 'Courier New', monospace; +} diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/positions-grid/positions-grid.component.ts b/projects/app-lob/src/app/grid-finjs-dock-manager/positions-grid/positions-grid.component.ts new file mode 100644 index 000000000..cd84cf999 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/positions-grid/positions-grid.component.ts @@ -0,0 +1,120 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { IgxGridComponent } from 'igniteui-angular/grids/grid'; +import { IgxColumnComponent, IgxCellTemplateDirective } from 'igniteui-angular/grids/core'; + +interface Position { + symbol: string; + qty: number; + avgCost: number; + currentPrice: number; + pl: number; + plFormatted: string; + plPct: string; + positive: boolean; +} + +@Component({ + selector: 'app-positions-grid', + standalone: true, + imports: [IgxGridComponent, IgxColumnComponent, IgxCellTemplateDirective], + templateUrl: './positions-grid.component.html', + styleUrls: ['./positions-grid.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PositionsGridComponent { + public readonly positions: Position[] = [ + { + symbol: 'AAPL', + qty: 100, + avgCost: 210.40, + currentPrice: 227.14, + pl: 1674, + plFormatted: '+$1,674.00', + plPct: '+7.96%', + positive: true + }, + { + symbol: 'TSLA', + qty: 50, + avgCost: 260.00, + currentPrice: 248.50, + pl: -575, + plFormatted: '-$575.00', + plPct: '-4.42%', + positive: false + }, + { + symbol: 'NVDA', + qty: 30, + avgCost: 120.00, + currentPrice: 138.92, + pl: 567.60, + plFormatted: '+$567.60', + plPct: '+23.77%', + positive: true + }, + { + symbol: 'MSFT', + qty: 40, + avgCost: 420.00, + currentPrice: 441.58, + pl: 863.20, + plFormatted: '+$863.20', + plPct: '+5.14%', + positive: true + }, + { + symbol: 'AMZN', + qty: 60, + avgCost: 205.00, + currentPrice: 198.34, + pl: -399.60, + plFormatted: '-$399.60', + plPct: '-3.25%', + positive: false + }, + { + symbol: 'GOOGL', + qty: 75, + avgCost: 172.00, + currentPrice: 178.22, + pl: 466.50, + plFormatted: '+$466.50', + plPct: '+3.62%', + positive: true + }, + { + symbol: 'META', + qty: 20, + avgCost: 530.00, + currentPrice: 512.77, + pl: -344.60, + plFormatted: '-$344.60', + plPct: '-3.25%', + positive: false + }, + { + symbol: 'JPM', + qty: 120, + avgCost: 195.00, + currentPrice: 204.10, + pl: 1092.00, + plFormatted: '+$1,092.00', + plPct: '+4.67%', + positive: true + }, + { + symbol: 'NFLX', + qty: 15, + avgCost: 650.00, + currentPrice: 671.30, + pl: 319.50, + plFormatted: '+$319.50', + plPct: '+3.28%', + positive: true + } + ]; + + public readonly totalPL = '+$3,663.60'; + public readonly totalPositive = true; +} diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/watchlist/watchlist.component.html b/projects/app-lob/src/app/grid-finjs-dock-manager/watchlist/watchlist.component.html new file mode 100644 index 000000000..621211e74 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/watchlist/watchlist.component.html @@ -0,0 +1,109 @@ +
+ Market Watchlist + {{ filteredStocks().length }} +
+ +
+ + + + {{ cell.value }} + + + + + + {{ formatPrice(cell.value) }} + + + + + + {{ formatSignedPct(cell.value) }} + + + + + + {{ formatSigned(cell.value) }} + + + + + + + + + + + + + + + + + +
+ {{ formatPrice(cell.row.data.dayLow) }} + + {{ formatPrice(cell.row.data.dayHigh) }} +
+
+
+ + + +
+ {{ formatPrice(cell.row.data.week52Low) }} + + {{ formatPrice(cell.row.data.week52High) }} +
+
+
+ + +
+
+ +
+
+ @for (tab of moverTabs; track tab) { + + } +
+ +
+ @if (movers().length === 0) { +
No matches for current search.
+ } + @for (mover of movers(); track mover.symbol) { +
+ {{ mover.symbol }} + {{ formatPrice(mover.lastPrice) }} + {{ formatSignedPct(mover.changePct) }} +
+ } +
+
diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/watchlist/watchlist.component.scss b/projects/app-lob/src/app/grid-finjs-dock-manager/watchlist/watchlist.component.scss new file mode 100644 index 000000000..279ba6925 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/watchlist/watchlist.component.scss @@ -0,0 +1,184 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + background: var(--sd-surface); +} + +.wl-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 12px; + border-bottom: 1px solid var(--sd-border); + flex-shrink: 0; +} + +.wl-title { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--sd-muted); +} + +.wl-count { + font-size: 11px; + color: var(--sd-muted); + background: var(--sd-panel); + border: 1px solid var(--sd-border); + border-radius: 10px; + padding: 0 7px; +} + +.watchlist-grid-wrap { + flex: 1 1 auto; + min-height: 0; + overflow: hidden; + border-bottom: 1px solid var(--sd-border); +} + +.watchlist-grid { + --ig-size: var(--ig-size-small); + font-size: 12px; + --sb-thumb-bg-color: var(--sd-border); + --sb-thumb-bg-color-hover: var(--sd-muted); + --sb-thumb-bg-color-active: var(--sd-muted); +} + +.symbol-cell { + color: #8cc8ff; + font-weight: 700; + letter-spacing: 0.2px; +} + +.mono { + font-variant-numeric: tabular-nums; + font-family: 'DM Mono', 'Courier New', monospace; +} + +.shares-link { + border: none; + background: transparent; + color: var(--sd-text); + text-decoration: underline; + text-underline-offset: 2px; + font: inherit; + cursor: pointer; + padding: 0; +} + +.range-cell { + display: grid; + grid-template-columns: 64px 1fr 64px; + align-items: center; + gap: 8px; +} + +.range-value { + color: var(--sd-muted); + font-size: 11px; + font-variant-numeric: tabular-nums; + font-family: 'DM Mono', 'Courier New', monospace; +} + +.range-track { + position: relative; + height: 2px; + background: rgba(140, 200, 255, 0.35); +} + +.range-marker { + position: absolute; + top: 50%; + width: 8px; + height: 8px; + border-radius: 50%; + background: #8cc8ff; + transform: translate(-50%, -50%); + box-shadow: 0 0 0 1px var(--sd-border); +} + +.movers-panel { + flex: 0 0 150px; + display: flex; + flex-direction: column; + min-height: 0; + background: var(--sd-panel); +} + +.movers-tabs { + display: flex; + align-items: center; + border-bottom: 1px solid var(--sd-border); + padding: 0 8px; + gap: 4px; + height: 30px; +} + +.movers-tab { + border: none; + background: transparent; + color: var(--sd-muted); + font-size: 10px; + height: 100%; + padding: 0 6px; + border-bottom: 2px solid transparent; + cursor: pointer; +} + +.movers-tab:focus-visible { + outline: 2px solid var(--sd-cyan); + outline-offset: -2px; +} + +.active-mover { + color: var(--sd-text); + border-bottom-color: var(--sd-cyan); +} + +.movers-rows { + display: flex; + flex-direction: column; + overflow: auto; +} + +.movers-empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + min-height: 64px; + color: var(--sd-muted); + font-size: 11px; +} + +.mover-row { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 8px; + align-items: center; + height: 24px; + padding: 0 10px; + border-bottom: 1px solid rgba(124, 155, 197, 0.12); + font-size: 10px; +} + +.mover-symbol { + color: var(--sd-text); + font-weight: 700; +} + +.mover-last { + color: var(--sd-muted); + font-variant-numeric: tabular-nums; +} + +.mover-change { + font-variant-numeric: tabular-nums; + font-weight: 600; +} + +.pos { color: var(--sd-green); } +.neg { color: var(--sd-red); } diff --git a/projects/app-lob/src/app/grid-finjs-dock-manager/watchlist/watchlist.component.ts b/projects/app-lob/src/app/grid-finjs-dock-manager/watchlist/watchlist.component.ts new file mode 100644 index 000000000..eaf123bb8 --- /dev/null +++ b/projects/app-lob/src/app/grid-finjs-dock-manager/watchlist/watchlist.component.ts @@ -0,0 +1,145 @@ +import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core'; +import { IgxGridComponent } from 'igniteui-angular/grids/grid'; +import { IgxColumnComponent, IgxCellTemplateDirective } from 'igniteui-angular/grids/core'; + +interface StockItem { + symbol: string; + name: string; + lastPrice: number; + change: number; + changePct: number; + currency: string; + marketTime: string; + volume: string; + sharesAction: string; + avgVol3M: string; + dayLow: number; + dayHigh: number; + week52Low: number; + week52High: number; + marketCap: string; + positive: boolean; +} + +type MoversTab = 'Top Gainers' | 'Top Losers' | 'Most Active'; + +@Component({ + selector: 'app-watchlist', + standalone: true, + imports: [IgxGridComponent, IgxColumnComponent, IgxCellTemplateDirective], + templateUrl: './watchlist.component.html', + styleUrls: ['./watchlist.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WatchlistComponent { + public readonly searchQuery = input(''); + public readonly moverTabs: MoversTab[] = ['Top Gainers', 'Top Losers', 'Most Active']; + public readonly activeMoverTab = signal('Top Gainers'); + + public readonly stocks: StockItem[] = [ + { symbol: 'MELI', name: 'MercadoLibre', lastPrice: 1857.03, change: -16.85, changePct: -0.90, currency: 'USD', marketTime: '9:59AM EDT', volume: '48,228', sharesAction: 'Add', avgVol3M: '540,044', dayLow: 1495.00, dayHigh: 1905.49, week52Low: 1495.00, week52High: 2548.50, marketCap: '94.146B', positive: false }, + { symbol: 'FDX', name: 'FedEx', lastPrice: 314.30, change: 0.64, changePct: 0.21, currency: 'USD', marketTime: '10:00AM EDT', volume: '54,454', sharesAction: 'Add', avgVol3M: '2.105M', dayLow: 174.13, dayHigh: 316.00, week52Low: 174.13, week52High: 345.37, marketCap: '74.995B', positive: true }, + { symbol: 'WMT', name: 'Walmart', lastPrice: 113.75, change: 0.05, changePct: 0.04, currency: 'USD', marketTime: '10:00AM EDT', volume: '1.376M', sharesAction: 'Add', avgVol3M: '22.034M', dayLow: 94.43, dayHigh: 114.27, week52Low: 94.43, week52High: 135.16, marketCap: '905.192B', positive: true }, + { symbol: 'WM', name: 'Waste Management', lastPrice: 234.81, change: 0.52, changePct: 0.22, currency: 'USD', marketTime: '9:58AM EDT', volume: '84,026', sharesAction: 'Add', avgVol3M: '2.198M', dayLow: 194.11, dayHigh: 234.81, week52Low: 194.11, week52High: 248.13, marketCap: '94.294B', positive: true }, + { symbol: 'BRK-B', name: 'Berkshire B', lastPrice: 489.50, change: -1.59, changePct: -0.32, currency: 'USD', marketTime: '10:00AM EDT', volume: '415,865', sharesAction: 'Add', avgVol3M: '5.093M', dayLow: 455.19, dayHigh: 490.56, week52Low: 455.19, week52High: 516.85, marketCap: '1.056T', positive: false }, + { symbol: 'UNIT', name: 'Uniti Group', lastPrice: 11.03, change: 0.02, changePct: 0.18, currency: 'USD', marketTime: '9:56AM EDT', volume: '79,080', sharesAction: 'Add', avgVol3M: '2.523M', dayLow: 5.30, dayHigh: 11.07, week52Low: 5.30, week52High: 12.94, marketCap: '2.678B', positive: true }, + { symbol: 'PFE', name: 'Pfizer', lastPrice: 24.58, change: 0.32, changePct: 1.32, currency: 'USD', marketTime: '10:00AM EDT', volume: '3.284M', sharesAction: 'Add', avgVol3M: '40.668M', dayLow: 23.11, dayHigh: 24.62, week52Low: 23.11, week52High: 28.75, marketCap: '140.064B', positive: true }, + { symbol: 'WFC', name: 'Wells Fargo', lastPrice: 86.49, change: 1.14, changePct: 1.34, currency: 'USD', marketTime: '10:00AM EDT', volume: '2.399M', sharesAction: 'Add', avgVol3M: '16.49M', dayLow: 72.78, dayHigh: 87.63, week52Low: 72.78, week52High: 97.76, marketCap: '264.676B', positive: true }, + { symbol: 'C', name: 'Citigroup', lastPrice: 134.24, change: 0.97, changePct: 0.73, currency: 'USD', marketTime: '10:00AM EDT', volume: '2.32M', sharesAction: 'Add', avgVol3M: '12.256M', dayLow: 87.94, dayHigh: 134.65, week52Low: 87.94, week52High: 147.96, marketCap: '228.957B', positive: true }, + { symbol: 'PG', name: 'Procter & Gamble', lastPrice: 145.85, change: -0.25, changePct: -0.17, currency: 'USD', marketTime: '10:00AM EDT', volume: '597,359', sharesAction: 'Add', avgVol3M: '9.122M', dayLow: 137.62, dayHigh: 146.52, week52Low: 137.62, week52High: 167.25, marketCap: '339.626B', positive: false }, + { symbol: 'IEMG', name: 'iShares EM ETF', lastPrice: 79.85, change: 0.13, changePct: 0.16, currency: 'USD', marketTime: '10:00AM EDT', volume: '1.033M', sharesAction: 'Add', avgVol3M: '12.968M', dayLow: 59.68, dayHigh: 80.14, week52Low: 59.68, week52High: 86.49, marketCap: '--', positive: true }, + { symbol: 'ADBE', name: 'Adobe', lastPrice: 226.98, change: 6.20, changePct: 2.81, currency: 'USD', marketTime: '10:00AM EDT', volume: '560,217', sharesAction: 'Add', avgVol3M: '6.414M', dayLow: 190.12, dayHigh: 227.81, week52Low: 190.12, week52High: 376.16, marketCap: '90.225B', positive: true }, + { symbol: 'EQIX', name: 'Equinix', lastPrice: 1024.11, change: 0.57, changePct: 0.06, currency: 'USD', marketTime: '9:59AM EDT', volume: '43,869', sharesAction: 'Add', avgVol3M: '590,222', dayLow: 720.62, dayHigh: 1027.87, week52Low: 720.62, week52High: 1128.68, marketCap: '101.002B', positive: true }, + { symbol: 'QCOM', name: 'Qualcomm', lastPrice: 179.04, change: 0.94, changePct: 0.53, currency: 'USD', marketTime: '10:00AM EDT', volume: '990,650', sharesAction: 'Add', avgVol3M: '23.527M', dayLow: 121.99, dayHigh: 180.71, week52Low: 121.99, week52High: 259.92, marketCap: '188.708B', positive: true }, + { symbol: 'MSFT', name: 'Microsoft', lastPrice: 389.79, change: 4.86, changePct: 1.26, currency: 'USD', marketTime: '10:00AM EDT', volume: '4.508M', sharesAction: 'Add', avgVol3M: '39.743M', dayLow: 349.20, dayHigh: 391.46, week52Low: 349.20, week52High: 555.45, marketCap: '2.896T', positive: true }, + { symbol: 'BLK', name: 'BlackRock', lastPrice: 1098.58, change: 73.14, changePct: 7.13, currency: 'USD', marketTime: '10:00AM EDT', volume: '334,440', sharesAction: 'Add', avgVol3M: '719,777', dayLow: 917.39, dayHigh: 1109.99, week52Low: 917.39, week52High: 1219.94, marketCap: '178.632B', positive: true }, + { symbol: 'ABNB', name: 'Airbnb', lastPrice: 148.56, change: 2.02, changePct: 1.38, currency: 'USD', marketTime: '9:59AM EDT', volume: '287,751', sharesAction: 'Add', avgVol3M: '3.789M', dayLow: 110.81, dayHigh: 150.19, week52Low: 110.81, week52High: 150.19, marketCap: '88.171B', positive: true } + ]; + + public readonly filteredStocks = computed(() => { + const query = this.searchQuery().trim().toLowerCase(); + if (!query) { + return this.stocks; + } + + return this.stocks.filter(stock => { + const combined = `${stock.symbol} ${stock.name} ${stock.sharesAction}`.toLowerCase(); + return combined.includes(query); + }); + }); + + public readonly movers = computed(() => { + const dataset = this.filteredStocks(); + const tab = this.activeMoverTab(); + + if (tab === 'Top Gainers') { + return dataset + .filter(stock => stock.changePct > 0) + .sort((a, b) => b.changePct - a.changePct) + .slice(0, 5); + } + + if (tab === 'Top Losers') { + return dataset + .filter(stock => stock.changePct < 0) + .sort((a, b) => a.changePct - b.changePct) + .slice(0, 5); + } + + return dataset + .slice() + .sort((a, b) => this.parseVolume(b.volume) - this.parseVolume(a.volume)) + .slice(0, 5); + }); + + public formatPrice(value: number): string { + return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + public formatSigned(value: number): string { + const formatted = Math.abs(value).toFixed(2); + return `${value >= 0 ? '+' : '-'}${formatted}`; + } + + public formatSignedPct(value: number): string { + const formatted = Math.abs(value).toFixed(2); + return `${value >= 0 ? '+' : '-'}${formatted}%`; + } + + public rangePosition(value: number, low: number, high: number): number { + if (high <= low) { + return 0; + } + const ratio = ((value - low) / (high - low)) * 100; + return Math.min(100, Math.max(0, ratio)); + } + + public setActiveMoverTab(tab: MoversTab): void { + this.activeMoverTab.set(tab); + } + + private parseVolume(value: string): number { + const normalized = value.trim().toUpperCase().replace(/,/g, ''); + const match = normalized.match(/^(\d+(?:\.\d+)?)([KMB])?$/); + + if (!match) { + return 0; + } + + const amount = Number(match[1]); + const suffix = match[2]; + if (suffix === 'B') { + return amount * 1_000_000_000; + } + + if (suffix === 'M') { + return amount * 1_000_000; + } + + if (suffix === 'K') { + return amount * 1_000; + } + + return amount; + } +} diff --git a/projects/app-lob/src/main.ts b/projects/app-lob/src/main.ts index 785d0e1bc..c09a7e1e0 100644 --- a/projects/app-lob/src/main.ts +++ b/projects/app-lob/src/main.ts @@ -1,5 +1,5 @@ import { enableProdMode } from '@angular/core'; -import { defineCustomElements } from 'igniteui-dockmanager/loader'; +import { defineComponents, IgcDockManagerComponent } from 'igniteui-dockmanager'; import { environment } from './environments/environment'; @@ -13,4 +13,4 @@ if (environment.production) { bootstrapApplication(AppComponent, AppConfig).catch(err => console.error(err)); -defineCustomElements(window); +defineComponents(IgcDockManagerComponent); diff --git a/src/main.ts b/src/main.ts index fcc3112d6..6e8e7d55a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { enableProdMode } from '@angular/core'; import { environment } from './environments/environment'; -import { defineCustomElements } from 'igniteui-dockmanager/loader'; +import { defineComponents, IgcDockManagerComponent } from 'igniteui-dockmanager'; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { AppConfig } from './app/app.config'; @@ -11,4 +11,4 @@ if (environment.production) { bootstrapApplication(AppComponent, AppConfig).catch(err => console.log(err)); -defineCustomElements(window); +defineComponents(IgcDockManagerComponent);