Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 199 additions & 45 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@
"@angular/platform-browser": "^21",
"@angular/platform-browser-dynamic": "^21",
"@angular/router": "^21",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/state": "^6.5.4",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.39.13",
"@primeuix/themes": "^2.0.0",
"@tailwindcss/postcss": "^4.1.11",
"chart.js": "4.4.2",
"codemirror": "^6.0.2",
"js-yaml": "^4.1.1",
"primeclt": "^0.1.5",
"primeicons": "^7.0.0",
"primeng": "^21.0.2",
Expand All @@ -34,6 +40,7 @@
"@angular/cli": "^21",
"@angular/compiler-cli": "^21",
"@types/jasmine": "~5.1.0",
"@types/js-yaml": "^4.0.9",
"autoprefixer": "^10.4.20",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
Expand Down
14 changes: 12 additions & 2 deletions src/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { Component } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LayoutService } from './app/layout/service/layout.service';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterModule],
template: `<router-outlet></router-outlet>`
})
export class AppComponent {}
export class AppComponent implements OnInit {
private layoutService = inject(LayoutService);

ngOnInit() {
// Force initialization of LayoutService to load theme from storage
// This ensures the theme is applied before any components render
const config = this.layoutService.layoutConfig();
console.log('App initialized with theme config:', config);
}
}
4 changes: 4 additions & 0 deletions src/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { AppLayout } from './app/layout/component/app.layout';
import { Dashboard } from './app/pages/dashboard/dashboard';
import { Documentation } from './app/pages/documentation/documentation';
import { Landing } from './app/pages/landing/landing';
import { Layout } from './app/pages/layout/layout';
import { Notfound } from './app/pages/notfound/notfound';
import { LayoutBuilder } from './app/pages/builder/builder';

export const appRoutes: Routes = [
{
Expand All @@ -17,6 +19,8 @@ export const appRoutes: Routes = [
]
},
{ path: 'landing', component: Landing },
{ path: 'layout', component: Layout },
{ path: 'builder', component: LayoutBuilder },
{ path: 'notfound', component: Notfound },
{ path: 'auth', loadChildren: () => import('./app/pages/auth/auth.routes') },
{ path: '**', redirectTo: '/notfound' }
Expand Down
6 changes: 6 additions & 0 deletions src/app/layout/component/app.configurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Nora from '@primeuix/themes/nora';
import { PrimeNG } from 'primeng/config';
import { SelectButtonModule } from 'primeng/selectbutton';
import { LayoutService } from '@/app/layout/service/layout.service';
import { ConfigService } from '@/app/pages/service/config.service';

const presets = {
Aura,
Expand Down Expand Up @@ -102,6 +103,8 @@ export class AppConfigurator {

layoutService: LayoutService = inject(LayoutService);

configService = inject(ConfigService);

platformId = inject(PLATFORM_ID);

primeng = inject(PrimeNG);
Expand Down Expand Up @@ -417,8 +420,10 @@ export class AppConfigurator {
updateColors(event: any, type: string, color: any) {
if (type === 'primary') {
this.layoutService.layoutConfig.update((state) => ({ ...state, primary: color.name }));
this.configService.updateThemeConfig({ primaryColor: color.name });
} else if (type === 'surface') {
this.layoutService.layoutConfig.update((state) => ({ ...state, surface: color.name }));
this.configService.updateThemeConfig({ surfaceColor: color.name });
}
this.applyTheme(type, color);

Expand All @@ -435,6 +440,7 @@ export class AppConfigurator {

onPresetChange(event: any) {
this.layoutService.layoutConfig.update((state) => ({ ...state, preset: event }));
this.configService.updateThemeConfig({ preset: event });
const preset = presets[event as KeyOfType<typeof presets>];
const surfacePalette = this.surfaces.find((s) => s.name === this.selectedSurfaceColor())?.palette;
$t().preset(preset).preset(this.getPresetExt()).surfacePalette(surfacePalette).use({ useDefaultOptions: true });
Expand Down
15 changes: 15 additions & 0 deletions src/app/layout/component/app.menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ export class AppMenu {
label: 'Empty',
icon: 'pi pi-fw pi-circle-off',
routerLink: ['/pages/empty']
},
{
label: 'Layout',
icon: 'pi pi-fw pi-objects-column',
routerLink: ['/layout']
},
{
label: 'Config',
icon: 'pi pi-fw pi-cog',
routerLink: ['/pages/config']
},
{
label: 'Layout Builder',
icon: 'pi pi-fw pi-code',
routerLink: ['/builder']
}
]
},
Expand Down
27 changes: 10 additions & 17 deletions src/app/layout/component/app.topbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { MenuItem } from 'primeng/api';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { StyleClassModule } from 'primeng/styleclass';
import { AppConfigurator } from './app.configurator';
import { LayoutService } from '@/app/layout/service/layout.service';
import { ConfigService } from '@/app/pages/service/config.service';

@Component({
selector: 'app-topbar',
standalone: true,
imports: [RouterModule, CommonModule, StyleClassModule, AppConfigurator],
imports: [RouterModule, CommonModule, StyleClassModule],
template: ` <div class="layout-topbar">
<div class="layout-topbar-logo-container">
<button class="layout-menu-button layout-topbar-action" (click)="layoutService.onMenuToggle()">
Expand Down Expand Up @@ -42,20 +42,6 @@ import { LayoutService } from '@/app/layout/service/layout.service';
<button type="button" class="layout-topbar-action" (click)="toggleDarkMode()">
<i [ngClass]="{ 'pi ': true, 'pi-moon': layoutService.isDarkTheme(), 'pi-sun': !layoutService.isDarkTheme() }"></i>
</button>
<div class="relative">
<button
class="layout-topbar-action layout-topbar-action-highlight"
pStyleClass="@next"
enterFromClass="hidden"
enterActiveClass="animate-scalein"
leaveToClass="hidden"
leaveActiveClass="animate-fadeout"
[hideOnOutsideClick]="true"
>
<i class="pi pi-palette"></i>
</button>
<app-configurator />
</div>
</div>

<button class="layout-topbar-menu-button layout-topbar-action" pStyleClass="@next" enterFromClass="hidden" enterActiveClass="animate-scalein" leaveToClass="hidden" leaveActiveClass="animate-fadeout" [hideOnOutsideClick]="true">
Expand Down Expand Up @@ -85,11 +71,18 @@ export class AppTopbar {
items!: MenuItem[];

layoutService = inject(LayoutService);
configService = inject(ConfigService);

toggleDarkMode() {
const newDarkMode = !this.layoutService.layoutConfig().darkTheme;

// Update layout service
this.layoutService.layoutConfig.update((state) => ({
...state,
darkTheme: !state.darkTheme
darkTheme: newDarkMode
}));

// Update config service to persist
this.configService.updateThemeConfig({ darkMode: newDarkMode });
}
}
153 changes: 151 additions & 2 deletions src/app/layout/service/layout.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
import { Injectable, effect, signal, computed } from '@angular/core';
import { Injectable, effect, signal, computed, inject } from '@angular/core';
import { $t, updatePreset, updateSurfacePalette } from '@primeuix/themes';
import Aura from '@primeuix/themes/aura';
import Lara from '@primeuix/themes/lara';
import Nora from '@primeuix/themes/nora';

const presets = {
Aura,
Lara,
Nora
} as const;

declare type KeyOfType<T> = keyof T extends infer U ? U : never;

declare type SurfacesType = {
name?: string;
palette?: {
0?: string;
50?: string;
100?: string;
200?: string;
300?: string;
400?: string;
500?: string;
600?: string;
700?: string;
800?: string;
900?: string;
950?: string;
};
};

export interface LayoutConfig {
preset: string;
Expand Down Expand Up @@ -38,7 +68,7 @@ export class LayoutService {
activePath: null
});

theme = computed(() => (this.layoutConfig().darkTheme ? 'light' : 'dark'));
theme = computed(() => (this.layoutConfig().darkTheme ? 'dark' : 'light'));

isSidebarActive = computed(() => this.layoutState().overlayMenuActive || this.layoutState().mobileMenuActive);

Expand All @@ -54,7 +84,21 @@ export class LayoutService {

private initialized = false;

surfaces: SurfacesType[] = [
{ name: 'slate', palette: { 0: '#ffffff', 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', 950: '#020617' } },
{ name: 'gray', palette: { 0: '#ffffff', 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827', 950: '#030712' } },
{ name: 'zinc', palette: { 0: '#ffffff', 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' } },
{ name: 'neutral', palette: { 0: '#ffffff', 50: '#fafafa', 100: '#f5f5f5', 200: '#e5e5e5', 300: '#d4d4d4', 400: '#a3a3a3', 500: '#737373', 600: '#525252', 700: '#404040', 800: '#262626', 900: '#171717', 950: '#0a0a0a' } },
{ name: 'stone', palette: { 0: '#ffffff', 50: '#fafaf9', 100: '#f5f5f4', 200: '#e7e5e4', 300: '#d6d3d1', 400: '#a8a29e', 500: '#78716c', 600: '#57534e', 700: '#44403c', 800: '#292524', 900: '#1c1917', 950: '#0c0a09' } },
{ name: 'soho', palette: { 0: '#ffffff', 50: '#ececec', 100: '#dedfdf', 200: '#c4c4c6', 300: '#adaeb0', 400: '#97979b', 500: '#7f8084', 600: '#6a6b70', 700: '#55565b', 800: '#3f4046', 900: '#2c2c34', 950: '#16161d' } },
{ name: 'viva', palette: { 0: '#ffffff', 50: '#f3f3f3', 100: '#e7e7e8', 200: '#cfd0d0', 300: '#b7b8b9', 400: '#9fa1a1', 500: '#87898a', 600: '#6e7173', 700: '#565a5b', 800: '#3e4244', 900: '#262b2c', 950: '#0e1315' } },
{ name: 'ocean', palette: { 0: '#ffffff', 50: '#fbfcfc', 100: '#F7F9F8', 200: '#EFF3F2', 300: '#DADEDD', 400: '#B1B7B6', 500: '#828787', 600: '#5F7274', 700: '#415B61', 800: '#29444E', 900: '#183240', 950: '#0c1920' } }
];

constructor() {
// Load theme from localStorage on initialization
this.loadThemeFromStorage();

effect(() => {
const config = this.layoutConfig();

Expand All @@ -67,6 +111,111 @@ export class LayoutService {
});
}

private loadThemeFromStorage(): void {
try {
const storedConfig = localStorage.getItem('demo-config');
if (storedConfig) {
const config = JSON.parse(storedConfig);
if (config.theme) {
console.log('Loading theme from storage:', config.theme);
this.layoutConfig.update(state => ({
...state,
darkTheme: config.theme.darkMode ?? state.darkTheme,
primary: config.theme.primaryColor ?? state.primary,
surface: config.theme.surfaceColor ?? state.surface,
preset: config.theme.preset ?? state.preset
}));

// Apply dark mode immediately
if (config.theme.darkMode) {
document.documentElement.classList.add('app-dark');
} else {
document.documentElement.classList.remove('app-dark');
}

// Apply the full theme (preset, colors)
this.applyThemeFromConfig(config.theme);

console.log('Theme loaded and applied:', this.layoutConfig());
} else {
console.log('No theme config found in storage, using defaults');
}
} else {
console.log('No stored config found, using defaults');
}
} catch (e) {
console.error('Failed to load theme from localStorage', e);
}
}

private applyThemeFromConfig(themeConfig: any): void {
try {
// Apply preset
const preset = presets[themeConfig.preset as KeyOfType<typeof presets>] || Aura;

// Get primary and surface colors
const primaryColor = this.getPrimaryColorPalette(themeConfig.primaryColor || 'emerald');
const surfaceColor = this.surfaces.find(s => s.name === (themeConfig.surfaceColor || 'slate'));

// Apply theme
const presetExt = this.getPresetExt(themeConfig.primaryColor, themeConfig.preset);
$t()
.preset(preset)
.preset(presetExt)
.surfacePalette(surfaceColor?.palette)
.use({ useDefaultOptions: true });

console.log('Applied theme - Preset:', themeConfig.preset, 'Primary:', themeConfig.primaryColor, 'Surface:', themeConfig.surfaceColor);
} catch (e) {
console.error('Failed to apply theme:', e);
}
}

private getPrimaryColorPalette(colorName: string): any {
const presetPalette = presets[this.layoutConfig().preset as KeyOfType<typeof presets>].primitive;
if (colorName === 'noir') {
return {};
}
// Return the color palette, using type assertion to bypass strict typing
return (presetPalette as any)[colorName];
}

private getPresetExt(primaryColor: string, presetName: string): any {
const colorPalette = this.getPrimaryColorPalette(primaryColor);

if (primaryColor === 'noir') {
return {
semantic: {
primary: { 50: '{surface.50}', 100: '{surface.100}', 200: '{surface.200}', 300: '{surface.300}', 400: '{surface.400}', 500: '{surface.500}', 600: '{surface.600}', 700: '{surface.700}', 800: '{surface.800}', 900: '{surface.900}', 950: '{surface.950}' },
colorScheme: {
light: { primary: { color: '{primary.950}', contrastColor: '#ffffff', hoverColor: '{primary.800}', activeColor: '{primary.700}' }, highlight: { background: '{primary.950}', focusBackground: '{primary.700}', color: '#ffffff', focusColor: '#ffffff' } },
dark: { primary: { color: '{primary.50}', contrastColor: '{primary.950}', hoverColor: '{primary.200}', activeColor: '{primary.300}' }, highlight: { background: '{primary.50}', focusBackground: '{primary.300}', color: '{primary.950}', focusColor: '{primary.950}' } }
}
}
};
} else if (presetName === 'Nora') {
return {
semantic: {
primary: colorPalette,
colorScheme: {
light: { primary: { color: '{primary.600}', contrastColor: '#ffffff', hoverColor: '{primary.700}', activeColor: '{primary.800}' }, highlight: { background: '{primary.600}', focusBackground: '{primary.700}', color: '#ffffff', focusColor: '#ffffff' } },
dark: { primary: { color: '{primary.500}', contrastColor: '{surface.900}', hoverColor: '{primary.400}', activeColor: '{primary.300}' }, highlight: { background: '{primary.500}', focusBackground: '{primary.400}', color: '{surface.900}', focusColor: '{surface.900}' } }
}
}
};
} else {
return {
semantic: {
primary: colorPalette,
colorScheme: {
light: { primary: { color: '{primary.500}', contrastColor: '#ffffff', hoverColor: '{primary.600}', activeColor: '{primary.700}' }, highlight: { background: '{primary.50}', focusBackground: '{primary.100}', color: '{primary.700}', focusColor: '{primary.800}' } },
dark: { primary: { color: '{primary.400}', contrastColor: '{surface.900}', hoverColor: '{primary.300}', activeColor: '{primary.200}' }, highlight: { background: 'color-mix(in srgb, {primary.400}, transparent 84%)', focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)', color: 'rgba(255,255,255,.87)', focusColor: 'rgba(255,255,255,.87)' } }
}
}
};
}
}

private handleDarkModeTransition(config: LayoutConfig): void {
const supportsViewTransition = 'startViewTransition' in document;

Expand Down
Loading