From b0a9e8ed30fa0c4bc0a17773a4fa63f4230a007b Mon Sep 17 00:00:00 2001 From: Hasith Yaggahavita Date: Fri, 6 Sep 2024 08:58:50 +0530 Subject: [PATCH] Added theming support and structured the core library into multiple namespaces --- docs/theming.md | 56 ++++++++++ packages/@productled/core/src/Plugin.ts | 8 -- .../@productled/core/src/Productled.test.ts | 100 ------------------ packages/@productled/core/src/Productled.ts | 24 ++++- .../@productled/core/src/{ => hooks}/Hook.ts | 0 .../core/src/{ => hooks}/HookExecuter.ts | 11 +- .../core/src/{ => hooks}/HookStore.ts | 0 packages/@productled/core/src/index.ts | 5 +- .../@productled/core/src/plugins/Plugin.ts | 9 ++ .../core/src/{ => plugins}/PluginStore.ts | 0 .../core/src/theme/ThemeManager.ts | 73 +++++++++++++ .../core/src/theme/defaultTheme.ts | 18 ++++ .../@productled/spotlights/src/Spotlight.ts | 21 ++-- .../spotlights/src/SpotlightsPlugin.ts | 8 +- .../spotlights/src/StylesElement.ts | 11 +- packages/samples/react-sample/src/App.css | 4 + 16 files changed, 208 insertions(+), 140 deletions(-) create mode 100644 docs/theming.md delete mode 100644 packages/@productled/core/src/Plugin.ts delete mode 100644 packages/@productled/core/src/Productled.test.ts rename packages/@productled/core/src/{ => hooks}/Hook.ts (100%) rename packages/@productled/core/src/{ => hooks}/HookExecuter.ts (75%) rename packages/@productled/core/src/{ => hooks}/HookStore.ts (100%) create mode 100644 packages/@productled/core/src/plugins/Plugin.ts rename packages/@productled/core/src/{ => plugins}/PluginStore.ts (100%) create mode 100644 packages/@productled/core/src/theme/ThemeManager.ts create mode 100644 packages/@productled/core/src/theme/defaultTheme.ts diff --git a/docs/theming.md b/docs/theming.md new file mode 100644 index 0000000..6fefd25 --- /dev/null +++ b/docs/theming.md @@ -0,0 +1,56 @@ +--- +title: Theming +layout: home +nav_order: 4 +--- + +# Theming + +The Product-led library allows users to customize the look and feel of its components. It provides a default theme that can be easily overridden or changed to suit specific requirements. + +## Default Theme + +The library's default theme consists of the following properties: + +```typescript +export const defaultTheme: Theme = { + primaryColor: '#3498db', + secondaryColor: '#2ecc71', + backgroundColor: '#ffffff', + textColor: '#333333', + fontSize: '16px', + fontFamily: 'Arial, sans-serif', + borderRadius: '4px', + boxShadow: '0px 4px 6px rgba(0, 0, 0, 0.1)', + spacing: '8px', + linkColor: '#2980b9', + errorColor: '#e74c3c', + successColor: '#2ecc71', + warningColor: '#f39c12', + infoColor: '#3498db', +}; +``` + +## Overriding the Default Theme + +You can override the default theme by using CSS variables. To change specific properties, simply define your custom values in your CSS file. + +Example: + +```css +:root { + --primaryColor: #473ce7; +} +``` + +In this example, the primaryColor is set to a new value. + +## Programmatically Changing the Theme + +To dynamically update the theme in your application, use the following method: + +```typescript +Productled.getInstance().applyCustomTheme(customTheme: Partial); +``` + +This method allows you to pass a custom theme object to override the default or previously applied theme properties. You only need to provide the properties you want to change. \ No newline at end of file diff --git a/packages/@productled/core/src/Plugin.ts b/packages/@productled/core/src/Plugin.ts deleted file mode 100644 index 61f10b3..0000000 --- a/packages/@productled/core/src/Plugin.ts +++ /dev/null @@ -1,8 +0,0 @@ - -interface Plugin { - get Name(): string; - create(element: HTMLElement, conf: any): void; - removeAll(): void; -} - -export default Plugin; \ No newline at end of file diff --git a/packages/@productled/core/src/Productled.test.ts b/packages/@productled/core/src/Productled.test.ts deleted file mode 100644 index 5448113..0000000 --- a/packages/@productled/core/src/Productled.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import Productled from './Productled'; -import HookStore from './HookStore'; -import ConfigStore from './ConfigStore'; -import PluginStore from './PluginStore'; -import DocumentService from './DocumentService'; -import HookExecuter from './HookExecuter'; -import Plugin from './Plugin'; - -jest.mock('./HookStore'); -jest.mock('./ConfigStore'); -jest.mock('./PluginStore'); -jest.mock('./DocumentService'); -jest.mock('./HookExecuter'); - -describe('Productled', () => { - let config: any = { - "spotlights": [{ - "title": "New Click Me Feature", - "description": "You can now send emails directly from here", - "link": "http://myblog.com/new_feature_intro", - - "trigger": { - "url": "/page/subpage", - "selector": ".spot-me", - - "frequency": "always", - "schedule": { - "start": { "year": "2024", "month": "04", "date": "01", "time": "09:00" }, - "end": { "year": "2024", "month": "12", "date": "01", "time": "09:00" } - } - }, - "positioning": { - "alignment": "right-center", - "left": "75", - "top": "15" - } - }] - }; - - beforeEach(() => { - (HookStore as any).mockClear(); - (ConfigStore as any).mockClear(); - (PluginStore as any).mockClear(); - (DocumentService as any).mockClear(); - (HookExecuter as any).mockClear(); - }); - - it('should return the singleton instance', () => { - const instance1 = Productled.getInstance(); - const instance2 = Productled.getInstance(); - expect(instance1).toBe(instance2); - }); - - it('should call routeChanged and execute hooks', async () => { - const instance = Productled.getInstance(); - instance.loadConfig(config); - const mockHooks = ['hook1', 'hook2']; - const mockGetHooks = jest.fn().mockReturnValue(mockHooks); - const mockExecuteHooks = jest.fn(); - - HookStore.prototype.getHooks = mockGetHooks; - HookExecuter.prototype.executeHooks = mockExecuteHooks; - - Object.defineProperty(window, 'location', { - value: { - pathname: '/test-route' - }, - writable: true - }); - - await instance.routeChanged(); - - expect(mockGetHooks).toHaveBeenCalledWith('/test-route'); - expect(mockExecuteHooks).toHaveBeenCalledWith(mockHooks); - }); - - it('should register a plugin and add hooks', () => { - const instance = Productled.getInstance(); - instance.loadConfig(config); - - const mockPlugin = { - getName: jest.fn().mockReturnValue('testPlugin') - } as unknown as Plugin; - const mockHooks = ['hook1', 'hook2']; - const mockGetHooks = jest.fn().mockReturnValue(mockHooks); - const mockAddPlugin = jest.fn(); - const mockAddHooks = jest.fn(); - - ConfigStore.prototype.getHooks = mockGetHooks; - PluginStore.prototype.addPlugin = mockAddPlugin; - HookStore.prototype.addHooks = mockAddHooks; - - instance.registerPlugin(mockPlugin); - - expect(mockPlugin.Name).toHaveBeenCalled(); - expect(mockAddPlugin).toHaveBeenCalledWith(mockPlugin); - expect(mockGetHooks).toHaveBeenCalledWith('testPlugin'); - expect(mockAddHooks).toHaveBeenCalledWith(mockHooks, 'testPlugin'); - }); -}); \ No newline at end of file diff --git a/packages/@productled/core/src/Productled.ts b/packages/@productled/core/src/Productled.ts index 7d75ea3..89ccf71 100644 --- a/packages/@productled/core/src/Productled.ts +++ b/packages/@productled/core/src/Productled.ts @@ -1,10 +1,11 @@ -import HookExecuter from './HookExecuter'; -import HookStore from './HookStore'; -import Plugin from './Plugin'; +import HookExecuter from './hooks/HookExecuter'; +import HookStore from './hooks/HookStore'; +import Plugin from './plugins/Plugin'; import ConfigStore from './ConfigStore'; -import PluginStore from './PluginStore'; +import PluginStore from './plugins/PluginStore'; import DocumentService from './DocumentService'; import { RouteListener } from "./RouteListener"; +import { Theme, ThemeManager } from './theme/ThemeManager'; /** * The Productled class represents the core functionality of the Productled library. @@ -17,6 +18,7 @@ class Productled { protected pluginStore: PluginStore; protected documentService: DocumentService; protected routeListener: RouteListener; + protected themeManager: ThemeManager; /** * The constructor is private to ensure that the class is a singleton. @@ -28,6 +30,7 @@ class Productled { this.documentService = new DocumentService(); this.configStore = new ConfigStore(); this.routeListener = new RouteListener(); + this.themeManager = new ThemeManager(); this.routeListener.addListener(this.routeChanged.bind(this)); } @@ -43,6 +46,17 @@ class Productled { return Productled.instance; } + + /** + * Applies a custom theme to the product. + * + * @param customTheme - The partial theme object containing the custom theme properties. + * @returns void + */ + public applyCustomTheme(customTheme: Partial): void { + this.themeManager.applyCustomTheme(customTheme); + } + /** * This method registers the hook configuration with the Productled instance. */ @@ -59,7 +73,7 @@ class Productled { private routeChanged(url: string) { const hooks = this.hookStore.getHooks(url); - const hookExecuter = new HookExecuter(this.pluginStore, this.documentService); + const hookExecuter = new HookExecuter(this.pluginStore, this.documentService, this.themeManager.Theme); hookExecuter.executeHooks(hooks); } diff --git a/packages/@productled/core/src/Hook.ts b/packages/@productled/core/src/hooks/Hook.ts similarity index 100% rename from packages/@productled/core/src/Hook.ts rename to packages/@productled/core/src/hooks/Hook.ts diff --git a/packages/@productled/core/src/HookExecuter.ts b/packages/@productled/core/src/hooks/HookExecuter.ts similarity index 75% rename from packages/@productled/core/src/HookExecuter.ts rename to packages/@productled/core/src/hooks/HookExecuter.ts index e2a78fb..621e770 100644 --- a/packages/@productled/core/src/HookExecuter.ts +++ b/packages/@productled/core/src/hooks/HookExecuter.ts @@ -1,14 +1,17 @@ import Hook from "./Hook"; -import PluginStore from "./PluginStore"; -import DocumentService from "./DocumentService"; +import PluginStore from "../plugins/PluginStore"; +import DocumentService from "../DocumentService"; +import { Theme } from "../theme/ThemeManager"; class HookExecuter { private pluginStore: PluginStore; private documentService: DocumentService; + private theme: Theme; - constructor(pluginStore: PluginStore, documentService: DocumentService) { + constructor(pluginStore: PluginStore, documentService: DocumentService, theme: Theme) { this.pluginStore = pluginStore; this.documentService = documentService; + this.theme = theme; } public async executeHooks(hooks: Hook[]) { @@ -25,7 +28,7 @@ class HookExecuter { console.warn(`Plugin with name ${hook.pluginName} not found`); return; } - plugin.create(element, hook) + plugin.create(element, hook, this.theme) }; } } diff --git a/packages/@productled/core/src/HookStore.ts b/packages/@productled/core/src/hooks/HookStore.ts similarity index 100% rename from packages/@productled/core/src/HookStore.ts rename to packages/@productled/core/src/hooks/HookStore.ts diff --git a/packages/@productled/core/src/index.ts b/packages/@productled/core/src/index.ts index 961d343..4df0c6b 100644 --- a/packages/@productled/core/src/index.ts +++ b/packages/@productled/core/src/index.ts @@ -1,3 +1,4 @@ export { default as Productled } from './Productled'; -export { default as Plugin } from './Plugin'; -export { default as Hook } from './Hook'; \ No newline at end of file +export { default as Plugin } from './plugins/Plugin'; +export { default as Hook } from './hooks/Hook'; +export { Theme} from './theme/ThemeManager'; \ No newline at end of file diff --git a/packages/@productled/core/src/plugins/Plugin.ts b/packages/@productled/core/src/plugins/Plugin.ts new file mode 100644 index 0000000..5a8ed0e --- /dev/null +++ b/packages/@productled/core/src/plugins/Plugin.ts @@ -0,0 +1,9 @@ +import { Theme } from "../theme/ThemeManager"; + +interface Plugin { + get Name(): string; + create(element: HTMLElement, conf: any, theme: Theme): void; + removeAll(): void; +} + +export default Plugin; \ No newline at end of file diff --git a/packages/@productled/core/src/PluginStore.ts b/packages/@productled/core/src/plugins/PluginStore.ts similarity index 100% rename from packages/@productled/core/src/PluginStore.ts rename to packages/@productled/core/src/plugins/PluginStore.ts diff --git a/packages/@productled/core/src/theme/ThemeManager.ts b/packages/@productled/core/src/theme/ThemeManager.ts new file mode 100644 index 0000000..fc5298d --- /dev/null +++ b/packages/@productled/core/src/theme/ThemeManager.ts @@ -0,0 +1,73 @@ +import { defaultTheme } from "../theme/defaultTheme"; + +export interface Theme { + primaryColor: string; // Main color used in buttons, links, etc. + secondaryColor: string; // Supporting color for accents or highlights + backgroundColor: string; // General background color + textColor: string; // Main text color + fontSize: string; // Base font size for text + fontFamily: string; // Font family for text + borderRadius: string; // Rounded corners for UI components + boxShadow: string; // Shadow styling for depth + spacing: string; // General spacing (padding/margins) + linkColor: string; // Color for links + errorColor: string; // Color for error messages + successColor: string; // Color for success notifications + warningColor: string; // Color for warnings + infoColor: string; // Color for informational messages + [key: string]: string; // Allows additional properties + } + +export class ThemeManager { + + + private currentTheme: Theme; + + constructor() { + this.currentTheme = defaultTheme; + this.applyTheme(); + } + + public get Theme(): Theme { + return this.currentTheme; + } + + // Fetch theme values from CSS variables, fallback to default if not found + getCSSVariable(variable: string): string { + const value = getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`).trim(); + return value || this.currentTheme[variable]; + } + + // Apply the theme, picking from CSS variables or default values + applyTheme(): void { + Object.keys(defaultTheme).forEach((key) => { + const themeValue = this.getCSSVariable(key); + document.documentElement.style.setProperty(`--${key}`, themeValue); + }); + } + + // Update and apply the theme dynamically + updateTheme(newTheme: Theme): void { + Object.keys(newTheme).forEach((key) => { + this.currentTheme[key] = newTheme[key]; + document.documentElement.style.setProperty(`--${key}`, newTheme[key]); + }); + } + + /** + * Applies a new theme by merging it with the default theme and updating CSS variables. + * @param customTheme Partial theme object to override default theme properties. + */ + public applyCustomTheme(customTheme: Partial): void { + const newTheme = { ...this.currentTheme, ...customTheme } as Theme; + this.updateTheme(newTheme); + } + + /** + * Resets the theme to the default theme. + */ + public resetTheme(): void { + this.updateTheme(defaultTheme); + } + +} diff --git a/packages/@productled/core/src/theme/defaultTheme.ts b/packages/@productled/core/src/theme/defaultTheme.ts new file mode 100644 index 0000000..00c444c --- /dev/null +++ b/packages/@productled/core/src/theme/defaultTheme.ts @@ -0,0 +1,18 @@ +import { Theme } from './theme/ThemeManager'; + +export const defaultTheme: Theme = { + primaryColor: '#3498db', + secondaryColor: '#2ecc71', + backgroundColor: '#ffffff', + textColor: '#333333', + fontSize: '16px', + fontFamily: 'Arial, sans-serif', + borderRadius: '4px', + boxShadow: '0px 4px 6px rgba(0, 0, 0, 0.1)', + spacing: '8px', + linkColor: '#2980b9', + errorColor: '#e74c3c', + successColor: '#2ecc71', + warningColor: '#f39c12', + infoColor: '#3498db' +}; \ No newline at end of file diff --git a/packages/@productled/spotlights/src/Spotlight.ts b/packages/@productled/spotlights/src/Spotlight.ts index c41d065..98959c9 100644 --- a/packages/@productled/spotlights/src/Spotlight.ts +++ b/packages/@productled/spotlights/src/Spotlight.ts @@ -1,3 +1,4 @@ +import { Theme } from '@productled/core'; import { StylesElement } from './StylesElement'; export interface Positioning { @@ -14,30 +15,24 @@ export interface SpotlightConf { } export class Spotlight { private targetElement: Element; - private title: String; - private description:String; - private link: String; - private positioning: Positioning; + private theme: Theme; - constructor(targetElement: Element, title: String, description: String, link: String, positioning: Positioning) { + constructor(targetElement: Element, theme: Theme) { this.targetElement = targetElement; - this.positioning = positioning; - this.title = title; - this.description = description; - this.link = link; + this.theme = theme; } - create(): void { + create(title: String, description: String, link: String, positioning: Positioning): void { // Position the spotlight relative to the target element (this.targetElement as HTMLElement).style.position = 'relative'; const container = document.createElement('div'); container.classList.add('productled-spotlight'); container.style.position = 'absolute'; - container.style.left = `${this.positioning.left}px`; - container.style.top = `${this.positioning.top}px`; + container.style.left = `${positioning.left}px`; + container.style.top = `${positioning.top}px`; - const styles = new StylesElement(); + const styles = new StylesElement(this.theme); container.appendChild(styles.Element); this.targetElement.appendChild(container); diff --git a/packages/@productled/spotlights/src/SpotlightsPlugin.ts b/packages/@productled/spotlights/src/SpotlightsPlugin.ts index 7bae41c..67b9bcf 100644 --- a/packages/@productled/spotlights/src/SpotlightsPlugin.ts +++ b/packages/@productled/spotlights/src/SpotlightsPlugin.ts @@ -1,4 +1,4 @@ -import { type Plugin } from "@productled/core"; +import { Theme, type Plugin } from "@productled/core"; import { Spotlight } from "./Spotlight"; class SpotlightsPlugin implements Plugin { @@ -7,11 +7,11 @@ class SpotlightsPlugin implements Plugin { get Name(): string { return this.key; } - create(element: HTMLElement, spotlightConf: any): void { + create(element: HTMLElement, spotlightConf: any, theme: Theme): void { const { title, description, link, positioning } = spotlightConf; if (element) { - const spotlight = new Spotlight(element, title, description, link, positioning); - spotlight.create(); + const spotlight = new Spotlight(element, theme); + spotlight.create(title, description, link, positioning); } } removeAll(): void { diff --git a/packages/@productled/spotlights/src/StylesElement.ts b/packages/@productled/spotlights/src/StylesElement.ts index b341314..ebca064 100644 --- a/packages/@productled/spotlights/src/StylesElement.ts +++ b/packages/@productled/spotlights/src/StylesElement.ts @@ -1,8 +1,10 @@ +import { Theme } from "@productled/core"; + class StylesElement { private color: string; - constructor(color: string = 'rgba(219, 40, 40, .4)') { - this.color = color; + constructor(theme: Theme) { + this.color = theme.primaryColor; } public get Element(): HTMLStyleElement { @@ -15,7 +17,8 @@ class StylesElement { return ` .beacon{ position:absolute; - background-color: ${this.color}; + background-color: var(--primaryColor); + opacity: 0.4; border-style: none; border-width: 1px; height:1em; @@ -33,7 +36,7 @@ class StylesElement { top:0; background-color:transparent; border-radius:50%; - box-shadow:0px 0px 2px 2px ${this.color}; + box-shadow:0px 0px 2px 2px var(--primaryColor); -webkit-animation:active 2s infinite linear; animation:active 2s infinite linear; } diff --git a/packages/samples/react-sample/src/App.css b/packages/samples/react-sample/src/App.css index 74b5e05..a75cb7e 100644 --- a/packages/samples/react-sample/src/App.css +++ b/packages/samples/react-sample/src/App.css @@ -1,3 +1,7 @@ +:root { + --primaryColor: #473ce7; +} + .App { text-align: center; }