From 2856ad9d5c47c82885560c4081e08587fb468fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87etin?= <69278826+cetincakiroglu@users.noreply.github.com> Date: Fri, 3 Jan 2025 02:38:00 +0300 Subject: [PATCH] Add configservice, configurator & floatingconfigurator --- src/components/floatingconfigurator.ts | 38 ++ src/layout/appconfigurator.ts | 491 +++++++++++++++++++++++++ src/service/appconfigservice.ts | 135 +++++++ 3 files changed, 664 insertions(+) create mode 100644 src/components/floatingconfigurator.ts create mode 100644 src/layout/appconfigurator.ts create mode 100644 src/service/appconfigservice.ts diff --git a/src/components/floatingconfigurator.ts b/src/components/floatingconfigurator.ts new file mode 100644 index 0000000..e625081 --- /dev/null +++ b/src/components/floatingconfigurator.ts @@ -0,0 +1,38 @@ +import { Component, computed, inject } from '@angular/core'; +import { ButtonModule } from 'primeng/button'; +import { StyleClassModule } from 'primeng/styleclass'; +import { AppConfigurator } from '@/src/layout/appconfigurator'; +import { AppConfigService } from '@/src/service/appconfigservice'; + +@Component({ + selector: 'floating-configurator', + imports: [ButtonModule, StyleClassModule, AppConfigurator], + template: ` +
+ +
+ + +
+
+ `, +}) +export class FloatingConfigurator { + configService = inject(AppConfigService); + + isDarkTheme = computed(() => this.configService.appState().darkTheme); + + toggleDarkMode() { + this.configService.appState.update((state) => ({ ...state, darkTheme: !state.darkTheme })); + } +} diff --git a/src/layout/appconfigurator.ts b/src/layout/appconfigurator.ts new file mode 100644 index 0000000..e347b31 --- /dev/null +++ b/src/layout/appconfigurator.ts @@ -0,0 +1,491 @@ + +import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { Component, computed, inject, PLATFORM_ID } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { $t, updatePreset, updateSurfacePalette } from '@primeng/themes'; +import Aura from '@primeng/themes/aura'; +import Lara from '@primeng/themes/lara'; +import { ButtonModule } from 'primeng/button'; +import { PrimeNG } from 'primeng/config'; +import { InputSwitchModule } from 'primeng/inputswitch'; +import { RadioButtonModule } from 'primeng/radiobutton'; +import { SelectButton } from 'primeng/selectbutton'; +import { ToggleSwitchModule } from 'primeng/toggleswitch'; +import { AppConfigService } from '@/src/service/appconfigservice'; + +const presets = { + Aura, + Lara +}; + +@Component({ + selector: 'app-configurator', + standalone: true, + template: ` +
+
+ Primary +
+ @for (primaryColor of primaryColors(); track primaryColor.name) { + + } +
+
+ +
+ Surface +
+ @for (surface of surfaces; track surface.name) { + + } +
+
+ +
+ Presets + +
+
+
+
+ Ripple + +
+
+
+
+ RTL + +
+
+
+
+ `, + host: { + class: 'config-panel hidden' + }, + imports: [CommonModule, FormsModule, InputSwitchModule, ButtonModule, RadioButtonModule, SelectButton, ToggleSwitchModule] +}) +export class AppConfigurator { + get ripple() { + return this.config.ripple(); + } + + set ripple(value: boolean) { + this.config.ripple.set(value); + } + + get isRTL() { + return this.configService.appState().RTL; + } + + config: PrimeNG = inject(PrimeNG); + + configService: AppConfigService = inject(AppConfigService); + + platformId = inject(PLATFORM_ID); + + presets = Object.keys(presets); + + onRTLChange(value: boolean) { + this.configService.appState.update((state) => ({ ...state, RTL: value })); + if (!(document as any).startViewTransition) { + this.toggleRTL(value); + return; + } + + (document as any).startViewTransition(() => this.toggleRTL(value)); + } + + toggleRTL(value: boolean) { + const htmlElement = document.documentElement; + + if (value) { + htmlElement.setAttribute('dir', 'rtl'); + } else { + htmlElement.removeAttribute('dir'); + } + } + + ngOnInit() { + if (isPlatformBrowser(this.platformId)) { + this.onPresetChange(this.configService.appState().preset); + this.toggleRTL(this.configService.appState().RTL); + } + } + + surfaces = [ + { + 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' + } + } + ]; + + selectedPrimaryColor = computed(() => { + return this.configService.appState().primary; + }); + + selectedSurfaceColor = computed(() => this.configService.appState().surface); + + selectedPreset = computed(() => this.configService.appState().preset); + + primaryColors = computed(() => { + const presetPalette = presets[this.configService.appState().preset].primitive; + const colors = ['emerald', 'green', 'lime', 'orange', 'amber', 'yellow', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose']; + const palettes = [{ name: 'noir', palette: {} }]; + + colors.forEach((color) => { + palettes.push({ + name: color, + palette: presetPalette[color] + }); + }); + + return palettes; + }); + + getPresetExt() { + const color = this.primaryColors().find((c) => c.name === this.selectedPrimaryColor()); + + if (color.name === '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 (this.configService.appState().preset === 'Nora') { + return { + semantic: { + primary: color.palette, + 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 if (this.configService.appState().preset === 'Material') { + return { + semantic: { + primary: color.palette, + colorScheme: { + light: { + primary: { + color: '{primary.500}', + contrastColor: '#ffffff', + hoverColor: '{primary.400}', + activeColor: '{primary.300}' + }, + highlight: { + background: 'color-mix(in srgb, {primary.color}, transparent 88%)', + focusBackground: 'color-mix(in srgb, {primary.color}, transparent 76%)', + 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)' + } + } + } + } + }; + } else { + return { + semantic: { + primary: color.palette, + 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)' + } + } + } + } + }; + } + } + } + + updateColors(event: any, type: string, color: any) { + if (type === 'primary') { + this.configService.appState.update((state) => ({ ...state, primary: color.name })); + } else if (type === 'surface') { + this.configService.appState.update((state) => ({ ...state, surface: color.name })); + } + this.applyTheme(type, color); + + event.stopPropagation(); + } + + applyTheme(type: string, color: any) { + if (type === 'primary') { + updatePreset(this.getPresetExt()); + } else if (type === 'surface') { + updateSurfacePalette(color.palette); + } + } + + onPresetChange(event: any) { + this.configService.appState.update((state) => ({ ...state, preset: event })); + const preset = presets[event]; + const surfacePalette = this.surfaces.find((s) => s.name === this.selectedSurfaceColor())?.palette; + if (this.configService.appState().preset === 'Material') { + document.body.classList.add('material'); + this.config.ripple.set(true); + } else { + document.body.classList.remove('material'); + this.config.ripple.set(false); + } + $t().preset(preset).preset(this.getPresetExt()).surfacePalette(surfacePalette).use({ useDefaultOptions: true }); + } +} diff --git a/src/service/appconfigservice.ts b/src/service/appconfigservice.ts new file mode 100644 index 0000000..d299a69 --- /dev/null +++ b/src/service/appconfigservice.ts @@ -0,0 +1,135 @@ +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { computed, effect, inject, Injectable, PLATFORM_ID, signal } from '@angular/core'; + +interface AppState { + preset?: string; + primary?: string; + surface?: string; + darkTheme?: boolean; + menuActive?: boolean; + designerKey?: string; + RTL?: boolean; + overlayMenuActive?: boolean; + menuMode?: string; + staticMenuDesktopInactive?: boolean; + staticMenuMobileActive?: boolean; + profileSidebarVisible?: boolean; + configSidebarVisible?: boolean; + menuHoverActive?: boolean; + activeMenuItem?: boolean; +} + + +@Injectable({ + providedIn: 'root' +}) +export class AppConfigService { + private readonly STORAGE_KEY = 'appConfigState'; + + appState = signal(null); + + document = inject(DOCUMENT); + + platformId = inject(PLATFORM_ID); + + theme = computed(() => (this.appState()?.darkTheme ? 'dark' : 'light')); + + transitionComplete = signal(false); + + private initialized = false; + + constructor() { + effect(() => { + this.appState.set({ ...this.loadAppState() }); + const state = this.appState(); + + if (!this.initialized || !state) { + this.initialized = true; + return; + } + + this.saveAppState(state); + this.handleDarkModeTransition(state); + }); + } + + private handleDarkModeTransition(state: AppState): void { + if (isPlatformBrowser(this.platformId)) { + if ((document as any).startViewTransition) { + this.startViewTransition(state); + } else { + this.toggleDarkMode(state); + this.onTransitionEnd(); + } + } + } + + private startViewTransition(state: AppState): void { + const transition = (document as any).startViewTransition(() => { + this.toggleDarkMode(state); + }); + + transition.ready.then(() => this.onTransitionEnd()); + } + + private toggleDarkMode(state: AppState): void { + if (state.darkTheme) { + this.document.documentElement.classList.add('app-dark'); + } else { + this.document.documentElement.classList.remove('app-dark'); + } + } + + private toggleMenu () { + const {menuMode, overlayMenuActive, staticMenuDesktopInactive, staticMenuMobileActive} = this.appState(); + + if (menuMode === 'overlay') { + this.appState.update((prev) => ({...prev, overlayMenuActive: !overlayMenuActive})); + } + + if (window.innerWidth > 991) { + this.appState.update((prev) => ({...prev, staticMenuDesktopInactive: !staticMenuDesktopInactive})); + } else { + this.appState.update((prev) => ({...prev, staticMenuMobileActive: !staticMenuMobileActive})); + } + }; + + isSidebarActive = computed(() => this.appState().overlayMenuActive || this.appState().staticMenuMobileActive); + + isDarkTheme = computed(() => this.appState().darkTheme); + + getPrimary = computed(() => this.appState().primary); + + getSurface = computed(() => this.appState().surface); + + private onTransitionEnd() { + this.transitionComplete.set(true); + setTimeout(() => { + this.transitionComplete.set(false); + }); + } + + private loadAppState(): any { + if (isPlatformBrowser(this.platformId)) { + const storedState = localStorage.getItem(this.STORAGE_KEY); + if (storedState) { + return JSON.parse(storedState); + } + } + return { + preset: 'Aura', + primary: 'noir', + surface: null, + darkTheme: false, + menuActive: false, + designerKey: 'primeng-designer-theme', + RTL: false + }; + } + + private saveAppState(state: any): void { + if (isPlatformBrowser(this.platformId)) { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state)); + } + } +}