Add comprehensive library expansion with new components and demos
- Add new libraries: ui-accessibility, ui-animations, ui-backgrounds, ui-code-display, ui-data-utils, ui-font-manager, hcl-studio - Add extensive layout components: gallery-grid, infinite-scroll-container, kanban-board, masonry, split-view, sticky-layout - Add comprehensive demo components for all new features - Update project configuration and dependencies - Expand component exports and routing structure - Add UI landing pages planning document 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
63
projects/hcl-studio/README.md
Normal file
63
projects/hcl-studio/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# HclStudio
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.0.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the library, run:
|
||||
|
||||
```bash
|
||||
ng build hcl-studio
|
||||
```
|
||||
|
||||
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
|
||||
|
||||
### Publishing the Library
|
||||
|
||||
Once the project is built, you can publish your library by following these steps:
|
||||
|
||||
1. Navigate to the `dist` directory:
|
||||
```bash
|
||||
cd dist/hcl-studio
|
||||
```
|
||||
|
||||
2. Run the `npm publish` command to publish your library to the npm registry:
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
7
projects/hcl-studio/ng-package.json
Normal file
7
projects/hcl-studio/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/hcl-studio",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
12
projects/hcl-studio/package.json
Normal file
12
projects/hcl-studio/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "hcl-studio",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/core": "^19.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
324
projects/hcl-studio/src/lib/core/hcl-converter.ts
Normal file
324
projects/hcl-studio/src/lib/core/hcl-converter.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* ==========================================================================
|
||||
* HCL COLOR CONVERTER
|
||||
* ==========================================================================
|
||||
* Accurate color space conversions between RGB, LAB, and HCL
|
||||
* Based on CIE LAB color space for perceptual uniformity
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
import { RGB, LAB, HCL } from '../models/hcl.models';
|
||||
|
||||
export class HCLConverter {
|
||||
|
||||
// ==========================================================================
|
||||
// RGB ↔ HCL CONVERSIONS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Convert RGB to HCL color space
|
||||
* @param r Red component (0-255)
|
||||
* @param g Green component (0-255)
|
||||
* @param b Blue component (0-255)
|
||||
* @returns HCL color
|
||||
*/
|
||||
static rgbToHcl(r: number, g: number, b: number): HCL {
|
||||
const lab = this.rgbToLab(r, g, b);
|
||||
return this.labToHcl(lab.l, lab.a, lab.b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HCL to RGB color space
|
||||
* @param h Hue (0-360)
|
||||
* @param c Chroma (0-100+)
|
||||
* @param l Lightness (0-100)
|
||||
* @returns RGB color
|
||||
*/
|
||||
static hclToRgb(h: number, c: number, l: number): RGB {
|
||||
const lab = this.hclToLab(h, c, l);
|
||||
return this.labToRgb(lab.l, lab.a, lab.b);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// HEX CONVERSIONS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Convert hex string to HCL
|
||||
* @param hex Hex color string (#RRGGBB or #RGB)
|
||||
* @returns HCL color
|
||||
*/
|
||||
static hexToHcl(hex: string): HCL {
|
||||
const rgb = this.hexToRgb(hex);
|
||||
return this.rgbToHcl(rgb.r, rgb.g, rgb.b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HCL to hex string
|
||||
* @param h Hue (0-360)
|
||||
* @param c Chroma (0-100+)
|
||||
* @param l Lightness (0-100)
|
||||
* @returns Hex color string
|
||||
*/
|
||||
static hclToHex(h: number, c: number, l: number): string {
|
||||
const rgb = this.hclToRgb(h, c, l);
|
||||
return this.rgbToHex(rgb.r, rgb.g, rgb.b);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// RGB ↔ LAB CONVERSIONS (via XYZ)
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Convert RGB to LAB color space
|
||||
* @param r Red component (0-255)
|
||||
* @param g Green component (0-255)
|
||||
* @param b Blue component (0-255)
|
||||
* @returns LAB color
|
||||
*/
|
||||
static rgbToLab(r: number, g: number, b: number): LAB {
|
||||
// Convert RGB to XYZ first
|
||||
let rNorm = r / 255;
|
||||
let gNorm = g / 255;
|
||||
let bNorm = b / 255;
|
||||
|
||||
// Apply gamma correction (sRGB)
|
||||
rNorm = rNorm > 0.04045 ? Math.pow((rNorm + 0.055) / 1.055, 2.4) : rNorm / 12.92;
|
||||
gNorm = gNorm > 0.04045 ? Math.pow((gNorm + 0.055) / 1.055, 2.4) : gNorm / 12.92;
|
||||
bNorm = bNorm > 0.04045 ? Math.pow((bNorm + 0.055) / 1.055, 2.4) : bNorm / 12.92;
|
||||
|
||||
// Convert to XYZ using sRGB matrix
|
||||
let x = rNorm * 0.4124564 + gNorm * 0.3575761 + bNorm * 0.1804375;
|
||||
let y = rNorm * 0.2126729 + gNorm * 0.7151522 + bNorm * 0.0721750;
|
||||
let z = rNorm * 0.0193339 + gNorm * 0.1191920 + bNorm * 0.9503041;
|
||||
|
||||
// Normalize by D65 illuminant
|
||||
x = x / 0.95047;
|
||||
y = y / 1.00000;
|
||||
z = z / 1.08883;
|
||||
|
||||
// Convert XYZ to LAB
|
||||
const fx = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x + 16/116);
|
||||
const fy = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y + 16/116);
|
||||
const fz = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z + 16/116);
|
||||
|
||||
const l = (116 * fy) - 16;
|
||||
const a = 500 * (fx - fy);
|
||||
const bValue = 200 * (fy - fz);
|
||||
|
||||
return { l: Math.max(0, Math.min(100, l)), a, b: bValue };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LAB to RGB color space
|
||||
* @param l Lightness (0-100)
|
||||
* @param a Green-Red axis
|
||||
* @param b Blue-Yellow axis
|
||||
* @returns RGB color
|
||||
*/
|
||||
static labToRgb(l: number, a: number, b: number): RGB {
|
||||
// Convert LAB to XYZ
|
||||
const fy = (l + 16) / 116;
|
||||
const fx = a / 500 + fy;
|
||||
const fz = fy - b / 200;
|
||||
|
||||
const x = (fx > 0.206897 ? Math.pow(fx, 3) : (fx - 16/116) / 7.787) * 0.95047;
|
||||
const y = (fy > 0.206897 ? Math.pow(fy, 3) : (fy - 16/116) / 7.787) * 1.00000;
|
||||
const z = (fz > 0.206897 ? Math.pow(fz, 3) : (fz - 16/116) / 7.787) * 1.08883;
|
||||
|
||||
// Convert XYZ to RGB
|
||||
let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
|
||||
let g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560;
|
||||
let b2 = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
|
||||
|
||||
// Apply inverse gamma correction
|
||||
r = r > 0.0031308 ? 1.055 * Math.pow(r, 1/2.4) - 0.055 : 12.92 * r;
|
||||
g = g > 0.0031308 ? 1.055 * Math.pow(g, 1/2.4) - 0.055 : 12.92 * g;
|
||||
b2 = b2 > 0.0031308 ? 1.055 * Math.pow(b2, 1/2.4) - 0.055 : 12.92 * b2;
|
||||
|
||||
// Clamp to valid RGB range
|
||||
const rFinal = Math.max(0, Math.min(255, Math.round(r * 255)));
|
||||
const gFinal = Math.max(0, Math.min(255, Math.round(g * 255)));
|
||||
const bFinal = Math.max(0, Math.min(255, Math.round(b2 * 255)));
|
||||
|
||||
return { r: rFinal, g: gFinal, b: bFinal };
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// LAB ↔ HCL CONVERSIONS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Convert LAB to HCL (cylindrical representation)
|
||||
* @param l Lightness (0-100)
|
||||
* @param a Green-Red axis
|
||||
* @param b Blue-Yellow axis
|
||||
* @returns HCL color
|
||||
*/
|
||||
static labToHcl(l: number, a: number, b: number): HCL {
|
||||
const c = Math.sqrt(a * a + b * b);
|
||||
let h = Math.atan2(b, a) * (180 / Math.PI);
|
||||
|
||||
// Normalize hue to 0-360
|
||||
if (h < 0) h += 360;
|
||||
|
||||
return {
|
||||
h: isNaN(h) ? 0 : h,
|
||||
c: isNaN(c) ? 0 : c,
|
||||
l: Math.max(0, Math.min(100, l))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HCL to LAB color space
|
||||
* @param h Hue (0-360)
|
||||
* @param c Chroma (0-100+)
|
||||
* @param l Lightness (0-100)
|
||||
* @returns LAB color
|
||||
*/
|
||||
static hclToLab(h: number, c: number, l: number): LAB {
|
||||
const hRad = (h * Math.PI) / 180;
|
||||
const a = c * Math.cos(hRad);
|
||||
const b = c * Math.sin(hRad);
|
||||
|
||||
return { l, a, b };
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// HEX UTILITY METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Convert hex string to RGB
|
||||
* @param hex Hex color string
|
||||
* @returns RGB color
|
||||
*/
|
||||
static hexToRgb(hex: string): RGB {
|
||||
// Remove # if present
|
||||
hex = hex.replace('#', '');
|
||||
|
||||
// Handle 3-digit hex
|
||||
if (hex.length === 3) {
|
||||
hex = hex.split('').map(h => h + h).join('');
|
||||
}
|
||||
|
||||
if (hex.length !== 6) {
|
||||
throw new Error(`Invalid hex color: ${hex}`);
|
||||
}
|
||||
|
||||
const r = parseInt(hex.substr(0, 2), 16);
|
||||
const g = parseInt(hex.substr(2, 2), 16);
|
||||
const b = parseInt(hex.substr(4, 2), 16);
|
||||
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert RGB to hex string
|
||||
* @param r Red component (0-255)
|
||||
* @param g Green component (0-255)
|
||||
* @param b Blue component (0-255)
|
||||
* @returns Hex color string
|
||||
*/
|
||||
static rgbToHex(r: number, g: number, b: number): string {
|
||||
const toHex = (n: number) => {
|
||||
const hex = Math.max(0, Math.min(255, Math.round(n))).toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
};
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// COLOR MANIPULATION METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Adjust lightness of a color
|
||||
* @param hex Original color
|
||||
* @param amount Lightness adjustment (-100 to 100)
|
||||
* @returns Adjusted color
|
||||
*/
|
||||
static adjustLightness(hex: string, amount: number): string {
|
||||
const hcl = this.hexToHcl(hex);
|
||||
const newL = Math.max(0, Math.min(100, hcl.l + amount));
|
||||
return this.hclToHex(hcl.h, hcl.c, newL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust chroma (saturation) of a color
|
||||
* @param hex Original color
|
||||
* @param amount Chroma adjustment (-100 to 100)
|
||||
* @returns Adjusted color
|
||||
*/
|
||||
static adjustChroma(hex: string, amount: number): string {
|
||||
const hcl = this.hexToHcl(hex);
|
||||
const newC = Math.max(0, hcl.c + amount);
|
||||
return this.hclToHex(hcl.h, newC, hcl.l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust hue of a color
|
||||
* @param hex Original color
|
||||
* @param amount Hue adjustment in degrees (-360 to 360)
|
||||
* @returns Adjusted color
|
||||
*/
|
||||
static adjustHue(hex: string, amount: number): string {
|
||||
const hcl = this.hexToHcl(hex);
|
||||
let newH = hcl.h + amount;
|
||||
|
||||
// Normalize hue to 0-360
|
||||
while (newH < 0) newH += 360;
|
||||
while (newH >= 360) newH -= 360;
|
||||
|
||||
return this.hclToHex(newH, hcl.c, hcl.l);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// VALIDATION METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Validate if a string is a valid hex color
|
||||
* @param hex Color string to validate
|
||||
* @returns True if valid hex color
|
||||
*/
|
||||
static isValidHex(hex: string): boolean {
|
||||
try {
|
||||
const normalizedHex = hex.replace('#', '');
|
||||
return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(normalizedHex);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate relative luminance of a color (for contrast calculation)
|
||||
* @param hex Hex color string
|
||||
* @returns Relative luminance (0-1)
|
||||
*/
|
||||
static getRelativeLuminance(hex: string): number {
|
||||
const rgb = this.hexToRgb(hex);
|
||||
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => {
|
||||
c = c / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate contrast ratio between two colors
|
||||
* @param color1 First color
|
||||
* @param color2 Second color
|
||||
* @returns Contrast ratio (1-21)
|
||||
*/
|
||||
static getContrastRatio(color1: string, color2: string): number {
|
||||
const lum1 = this.getRelativeLuminance(color1);
|
||||
const lum2 = this.getRelativeLuminance(color2);
|
||||
const lighter = Math.max(lum1, lum2);
|
||||
const darker = Math.min(lum1, lum2);
|
||||
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
}
|
||||
353
projects/hcl-studio/src/lib/core/palette-generator.ts
Normal file
353
projects/hcl-studio/src/lib/core/palette-generator.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* ==========================================================================
|
||||
* PALETTE GENERATOR
|
||||
* ==========================================================================
|
||||
* Generate Material Design 3 compliant color palettes using HCL color space
|
||||
* Creates perceptually uniform tonal variations with proper contrast ratios
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
import { HCLConverter } from './hcl-converter';
|
||||
import { TonalPalette, MaterialPalette, BrandColors, HCL } from '../models/hcl.models';
|
||||
|
||||
export class PaletteGenerator {
|
||||
|
||||
// ==========================================================================
|
||||
// MATERIAL DESIGN 3 TONE LEVELS
|
||||
// ==========================================================================
|
||||
|
||||
private static readonly TONE_LEVELS = [0, 4, 5, 6, 10, 12, 15, 17, 20, 22, 25, 30, 35, 40, 50, 60, 70, 80, 87, 90, 92, 94, 95, 96, 98, 99, 100] as const;
|
||||
|
||||
// ==========================================================================
|
||||
// MAIN PALETTE GENERATION
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Generate complete Material Design 3 palette from brand colors
|
||||
* @param brandColors Primary, secondary, tertiary seed colors
|
||||
* @param mode Light or dark mode
|
||||
* @returns Complete Material Design 3 palette
|
||||
*/
|
||||
static generateMaterialPalette(brandColors: BrandColors, mode: 'light' | 'dark' = 'light'): MaterialPalette {
|
||||
return {
|
||||
primary: this.generateTonalPalette(brandColors.primary),
|
||||
secondary: this.generateTonalPalette(brandColors.secondary),
|
||||
tertiary: this.generateTonalPalette(brandColors.tertiary),
|
||||
neutral: this.generateNeutralPalette(brandColors.primary),
|
||||
neutralVariant: this.generateNeutralVariantPalette(brandColors.primary),
|
||||
error: this.generateTonalPalette(brandColors.error || '#B3261E') // Material Design default error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tonal palette from seed color
|
||||
* @param seedColor Hex color string
|
||||
* @returns Tonal palette with all tone levels
|
||||
*/
|
||||
static generateTonalPalette(seedColor: string): TonalPalette {
|
||||
const seedHcl = HCLConverter.hexToHcl(seedColor);
|
||||
|
||||
// Create palette object with all tone levels
|
||||
const palette = {} as TonalPalette;
|
||||
|
||||
this.TONE_LEVELS.forEach(tone => {
|
||||
palette[tone as keyof TonalPalette] = this.generateTone(seedHcl, tone);
|
||||
});
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate neutral palette from primary color
|
||||
* @param primaryColor Primary seed color
|
||||
* @returns Neutral tonal palette
|
||||
*/
|
||||
static generateNeutralPalette(primaryColor: string): TonalPalette {
|
||||
const primaryHcl = HCLConverter.hexToHcl(primaryColor);
|
||||
|
||||
// Create neutral variation with reduced chroma and adjusted hue
|
||||
const neutralHcl: HCL = {
|
||||
h: primaryHcl.h,
|
||||
c: Math.min(primaryHcl.c * 0.08, 8), // Very low chroma for neutral
|
||||
l: primaryHcl.l
|
||||
};
|
||||
|
||||
const palette = {} as TonalPalette;
|
||||
|
||||
this.TONE_LEVELS.forEach(tone => {
|
||||
palette[tone as keyof TonalPalette] = this.generateTone(neutralHcl, tone);
|
||||
});
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate neutral variant palette from primary color
|
||||
* @param primaryColor Primary seed color
|
||||
* @returns Neutral variant tonal palette
|
||||
*/
|
||||
static generateNeutralVariantPalette(primaryColor: string): TonalPalette {
|
||||
const primaryHcl = HCLConverter.hexToHcl(primaryColor);
|
||||
|
||||
// Create neutral variant with slightly more chroma than neutral
|
||||
const neutralVariantHcl: HCL = {
|
||||
h: primaryHcl.h,
|
||||
c: Math.min(primaryHcl.c * 0.16, 16), // Low chroma but higher than neutral
|
||||
l: primaryHcl.l
|
||||
};
|
||||
|
||||
const palette = {} as TonalPalette;
|
||||
|
||||
this.TONE_LEVELS.forEach(tone => {
|
||||
palette[tone as keyof TonalPalette] = this.generateTone(neutralVariantHcl, tone);
|
||||
});
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// TONE GENERATION
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Generate specific tone from HCL seed
|
||||
* @param seedHcl Seed color in HCL space
|
||||
* @param tone Target tone level (0-100)
|
||||
* @returns Hex color string
|
||||
*/
|
||||
private static generateTone(seedHcl: HCL, tone: number): string {
|
||||
// Apply tone-specific chroma adjustments for better visual consistency
|
||||
const chromaMultiplier = this.getChromaMultiplier(tone);
|
||||
const adjustedChroma = seedHcl.c * chromaMultiplier;
|
||||
|
||||
// Create tone with adjusted lightness and chroma
|
||||
const toneHcl: HCL = {
|
||||
h: seedHcl.h,
|
||||
c: Math.max(0, adjustedChroma),
|
||||
l: tone
|
||||
};
|
||||
|
||||
return HCLConverter.hclToHex(toneHcl.h, toneHcl.c, toneHcl.l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chroma multiplier based on tone level
|
||||
* Reduces chroma at very light and very dark tones for better visual balance
|
||||
* @param tone Tone level (0-100)
|
||||
* @returns Chroma multiplier (0-1)
|
||||
*/
|
||||
private static getChromaMultiplier(tone: number): number {
|
||||
// Reduce chroma at extreme lightness values
|
||||
if (tone <= 10) return 0.6 + (tone / 10) * 0.3; // 0.6 to 0.9
|
||||
if (tone >= 90) return 1.1 - ((tone - 90) / 10) * 0.5; // 1.1 to 0.6
|
||||
if (tone >= 80) return 1.0 + ((tone - 80) / 10) * 0.1; // 1.0 to 1.1
|
||||
|
||||
// Full chroma in middle ranges
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// COLOR HARMONIES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Generate complementary color from seed
|
||||
* @param seedColor Seed color
|
||||
* @returns Complementary color
|
||||
*/
|
||||
static generateComplementary(seedColor: string): string {
|
||||
const hcl = HCLConverter.hexToHcl(seedColor);
|
||||
const compHue = (hcl.h + 180) % 360;
|
||||
return HCLConverter.hclToHex(compHue, hcl.c, hcl.l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate analogous colors from seed
|
||||
* @param seedColor Seed color
|
||||
* @param count Number of analogous colors to generate
|
||||
* @returns Array of analogous colors
|
||||
*/
|
||||
static generateAnalogous(seedColor: string, count: number = 3): string[] {
|
||||
const hcl = HCLConverter.hexToHcl(seedColor);
|
||||
const colors: string[] = [];
|
||||
const step = 30; // 30-degree steps
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hue = (hcl.h + (i - Math.floor(count / 2)) * step) % 360;
|
||||
const adjustedHue = hue < 0 ? hue + 360 : hue;
|
||||
colors.push(HCLConverter.hclToHex(adjustedHue, hcl.c, hcl.l));
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate triadic colors from seed
|
||||
* @param seedColor Seed color
|
||||
* @returns Array of triadic colors (including original)
|
||||
*/
|
||||
static generateTriadic(seedColor: string): string[] {
|
||||
const hcl = HCLConverter.hexToHcl(seedColor);
|
||||
return [
|
||||
seedColor,
|
||||
HCLConverter.hclToHex((hcl.h + 120) % 360, hcl.c, hcl.l),
|
||||
HCLConverter.hclToHex((hcl.h + 240) % 360, hcl.c, hcl.l)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tetradic colors from seed
|
||||
* @param seedColor Seed color
|
||||
* @returns Array of tetradic colors
|
||||
*/
|
||||
static generateTetradic(seedColor: string): string[] {
|
||||
const hcl = HCLConverter.hexToHcl(seedColor);
|
||||
return [
|
||||
seedColor,
|
||||
HCLConverter.hclToHex((hcl.h + 90) % 360, hcl.c, hcl.l),
|
||||
HCLConverter.hclToHex((hcl.h + 180) % 360, hcl.c, hcl.l),
|
||||
HCLConverter.hclToHex((hcl.h + 270) % 360, hcl.c, hcl.l)
|
||||
];
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// ACCESSIBILITY HELPERS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Ensure minimum contrast ratio for text readability
|
||||
* @param backgroundColor Background color
|
||||
* @param textColor Text color
|
||||
* @param minRatio Minimum contrast ratio (default: 4.5 for WCAG AA)
|
||||
* @returns Adjusted text color
|
||||
*/
|
||||
static ensureContrast(backgroundColor: string, textColor: string, minRatio: number = 4.5): string {
|
||||
const currentRatio = HCLConverter.getContrastRatio(backgroundColor, textColor);
|
||||
|
||||
if (currentRatio >= minRatio) {
|
||||
return textColor; // Already meets contrast requirements
|
||||
}
|
||||
|
||||
const textHcl = HCLConverter.hexToHcl(textColor);
|
||||
const backgroundLuminance = HCLConverter.getRelativeLuminance(backgroundColor);
|
||||
|
||||
// Adjust lightness to meet contrast requirements
|
||||
let adjustedL = textHcl.l;
|
||||
let bestColor = textColor;
|
||||
let bestRatio = currentRatio;
|
||||
|
||||
// Try both lighter and darker variations
|
||||
for (let lightness = 0; lightness <= 100; lightness += 5) {
|
||||
const testColor = HCLConverter.hclToHex(textHcl.h, textHcl.c, lightness);
|
||||
const testRatio = HCLConverter.getContrastRatio(backgroundColor, testColor);
|
||||
|
||||
if (testRatio >= minRatio && testRatio > bestRatio) {
|
||||
bestColor = testColor;
|
||||
bestRatio = testRatio;
|
||||
adjustedL = lightness;
|
||||
}
|
||||
}
|
||||
|
||||
return bestColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimal text color (black or white) for background
|
||||
* @param backgroundColor Background color
|
||||
* @returns Optimal text color (#000000 or #FFFFFF)
|
||||
*/
|
||||
static getOptimalTextColor(backgroundColor: string): string {
|
||||
const blackRatio = HCLConverter.getContrastRatio(backgroundColor, '#000000');
|
||||
const whiteRatio = HCLConverter.getContrastRatio(backgroundColor, '#FFFFFF');
|
||||
|
||||
return blackRatio > whiteRatio ? '#000000' : '#FFFFFF';
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// PALETTE UTILITIES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Get primary tone for specific use case
|
||||
* @param palette Tonal palette
|
||||
* @param usage Usage context ('main', 'container', 'on-container', etc.)
|
||||
* @param mode Light or dark mode
|
||||
* @returns Appropriate tone
|
||||
*/
|
||||
static getPrimaryTone(palette: TonalPalette, usage: 'main' | 'container' | 'on-container' | 'surface', mode: 'light' | 'dark' = 'light'): string {
|
||||
if (mode === 'light') {
|
||||
switch (usage) {
|
||||
case 'main': return palette[40];
|
||||
case 'container': return palette[90];
|
||||
case 'on-container': return palette[10];
|
||||
case 'surface': return palette[98];
|
||||
default: return palette[40];
|
||||
}
|
||||
} else {
|
||||
switch (usage) {
|
||||
case 'main': return palette[80];
|
||||
case 'container': return palette[30];
|
||||
case 'on-container': return palette[90];
|
||||
case 'surface': return palette[10];
|
||||
default: return palette[80];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate palette for accessibility and visual consistency
|
||||
* @param palette Material palette to validate
|
||||
* @returns Validation results
|
||||
*/
|
||||
static validatePalette(palette: MaterialPalette): { isValid: boolean; issues: string[] } {
|
||||
const issues: string[] = [];
|
||||
|
||||
// Check contrast ratios for common combinations
|
||||
const primaryMain = palette.primary[40];
|
||||
const primaryContainer = palette.primary[90];
|
||||
const onPrimary = palette.primary[100];
|
||||
const onPrimaryContainer = palette.primary[10];
|
||||
|
||||
// Validate primary combinations
|
||||
const primaryContrastRatio = HCLConverter.getContrastRatio(primaryMain, onPrimary);
|
||||
if (primaryContrastRatio < 4.5) {
|
||||
issues.push(`Primary contrast ratio too low: ${primaryContrastRatio.toFixed(2)}`);
|
||||
}
|
||||
|
||||
const containerContrastRatio = HCLConverter.getContrastRatio(primaryContainer, onPrimaryContainer);
|
||||
if (containerContrastRatio < 4.5) {
|
||||
issues.push(`Container contrast ratio too low: ${containerContrastRatio.toFixed(2)}`);
|
||||
}
|
||||
|
||||
// Additional checks could be added here (color difference, chroma consistency, etc.)
|
||||
|
||||
return {
|
||||
isValid: issues.length === 0,
|
||||
issues
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// PREVIEW GENERATION
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Generate color preview for theme selection UI
|
||||
* @param brandColors Brand colors
|
||||
* @returns Preview color object
|
||||
*/
|
||||
static generateColorPreview(brandColors: BrandColors): { [key: string]: string } {
|
||||
const palette = this.generateMaterialPalette(brandColors);
|
||||
|
||||
return {
|
||||
'Primary': palette.primary[40],
|
||||
'Primary Container': palette.primary[90],
|
||||
'Secondary': palette.secondary[40],
|
||||
'Secondary Container': palette.secondary[90],
|
||||
'Tertiary': palette.tertiary[40],
|
||||
'Tertiary Container': palette.tertiary[90],
|
||||
'Surface': palette.neutral[98],
|
||||
'Surface Container': palette.neutral[94]
|
||||
};
|
||||
}
|
||||
}
|
||||
23
projects/hcl-studio/src/lib/hcl-studio.component.spec.ts
Normal file
23
projects/hcl-studio/src/lib/hcl-studio.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HclStudioComponent } from './hcl-studio.component';
|
||||
|
||||
describe('HclStudioComponent', () => {
|
||||
let component: HclStudioComponent;
|
||||
let fixture: ComponentFixture<HclStudioComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HclStudioComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HclStudioComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
15
projects/hcl-studio/src/lib/hcl-studio.component.ts
Normal file
15
projects/hcl-studio/src/lib/hcl-studio.component.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-hcl-studio',
|
||||
imports: [],
|
||||
template: `
|
||||
<p>
|
||||
hcl-studio works!
|
||||
</p>
|
||||
`,
|
||||
styles: ``
|
||||
})
|
||||
export class HclStudioComponent {
|
||||
|
||||
}
|
||||
16
projects/hcl-studio/src/lib/hcl-studio.service.spec.ts
Normal file
16
projects/hcl-studio/src/lib/hcl-studio.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HclStudioService } from './hcl-studio.service';
|
||||
|
||||
describe('HclStudioService', () => {
|
||||
let service: HclStudioService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(HclStudioService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
512
projects/hcl-studio/src/lib/hcl-studio.service.ts
Normal file
512
projects/hcl-studio/src/lib/hcl-studio.service.ts
Normal file
@@ -0,0 +1,512 @@
|
||||
/**
|
||||
* ==========================================================================
|
||||
* HCL STUDIO SERVICE
|
||||
* ==========================================================================
|
||||
* Main service for HCL-based color palette generation and theme management
|
||||
* Provides complete theme switching, persistence, and reactive updates
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import {
|
||||
ThemeConfig,
|
||||
Theme,
|
||||
BrandColors,
|
||||
MaterialPalette,
|
||||
ThemePreview,
|
||||
ThemeCollection,
|
||||
ThemeState,
|
||||
HCLStudioOptions,
|
||||
ColorValidation
|
||||
} from './models/hcl.models';
|
||||
|
||||
import { PaletteGenerator } from './core/palette-generator';
|
||||
import { HCLConverter } from './core/hcl-converter';
|
||||
import { CSSVariableManager } from './themes/css-variable-manager';
|
||||
import { ThemeStorage } from './themes/theme-storage';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HCLStudioService {
|
||||
|
||||
// ==========================================================================
|
||||
// REACTIVE STATE
|
||||
// ==========================================================================
|
||||
|
||||
private _themeState$ = new BehaviorSubject<ThemeState>({
|
||||
currentTheme: null,
|
||||
availableThemes: [],
|
||||
mode: 'light',
|
||||
isInitialized: false
|
||||
});
|
||||
|
||||
private _currentMode$ = new BehaviorSubject<'light' | 'dark'>('light');
|
||||
|
||||
// Public observables
|
||||
public readonly themeState$: Observable<ThemeState> = this._themeState$.asObservable();
|
||||
public readonly currentMode$: Observable<'light' | 'dark'> = this._currentMode$.asObservable();
|
||||
|
||||
// ==========================================================================
|
||||
// PRIVATE STATE
|
||||
// ==========================================================================
|
||||
|
||||
private builtInThemes = new Map<string, Theme>();
|
||||
private customThemes = new Map<string, Theme>();
|
||||
private currentTheme: Theme | null = null;
|
||||
private currentMode: 'light' | 'dark' = 'light';
|
||||
private options: HCLStudioOptions = {};
|
||||
|
||||
// ==========================================================================
|
||||
// INITIALIZATION
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Initialize HCL Studio with options and themes
|
||||
* @param options Initialization options
|
||||
*/
|
||||
initialize(options: HCLStudioOptions = {}): void {
|
||||
this.options = {
|
||||
autoMode: false,
|
||||
storageKey: 'hcl-studio',
|
||||
validation: {
|
||||
enableContrastCheck: true,
|
||||
enableColorBlindCheck: false,
|
||||
minContrastRatio: 4.5
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
// Load built-in themes if provided
|
||||
if (options.themes) {
|
||||
this.registerThemes(options.themes);
|
||||
}
|
||||
|
||||
// Load custom themes from storage
|
||||
this.loadCustomThemesFromStorage();
|
||||
|
||||
// Restore user's last theme and mode
|
||||
this.restoreUserPreferences();
|
||||
|
||||
// Apply default theme if specified
|
||||
if (options.defaultTheme) {
|
||||
if (typeof options.defaultTheme === 'string') {
|
||||
this.switchTheme(options.defaultTheme);
|
||||
} else {
|
||||
this.applyThemeConfig(options.defaultTheme);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up system theme detection if enabled
|
||||
if (options.autoMode) {
|
||||
this.setupAutoModeDetection();
|
||||
}
|
||||
|
||||
this.updateThemeState();
|
||||
this.markAsInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick initialize with brand colors
|
||||
* @param brandColors Primary, secondary, tertiary colors
|
||||
* @param mode Initial mode
|
||||
*/
|
||||
initializeWithColors(brandColors: BrandColors, mode: 'light' | 'dark' = 'light'): void {
|
||||
const themeConfig: ThemeConfig = {
|
||||
id: 'brand-theme',
|
||||
name: 'Brand Theme',
|
||||
colors: brandColors,
|
||||
mode,
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
this.initialize({
|
||||
defaultTheme: themeConfig,
|
||||
autoMode: false
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// THEME MANAGEMENT
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Register multiple themes
|
||||
* @param themes Theme collection
|
||||
*/
|
||||
registerThemes(themes: ThemeCollection): void {
|
||||
Object.entries(themes).forEach(([id, config]) => {
|
||||
const themeConfig: ThemeConfig = { id, ...config };
|
||||
const theme = this.createCompleteTheme(themeConfig);
|
||||
this.builtInThemes.set(id, theme);
|
||||
});
|
||||
|
||||
this.updateThemeState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and save custom theme
|
||||
* @param id Theme identifier
|
||||
* @param name Theme display name
|
||||
* @param brandColors Brand colors
|
||||
* @param description Optional description
|
||||
* @returns Created theme
|
||||
*/
|
||||
createCustomTheme(id: string, name: string, brandColors: BrandColors, description?: string): Theme {
|
||||
const themeConfig: ThemeConfig = {
|
||||
id,
|
||||
name,
|
||||
colors: brandColors,
|
||||
description,
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
const theme = this.createCompleteTheme(themeConfig);
|
||||
this.customThemes.set(id, theme);
|
||||
|
||||
// Save to storage
|
||||
ThemeStorage.saveCustomTheme(themeConfig);
|
||||
|
||||
this.updateThemeState();
|
||||
return theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to different theme
|
||||
* @param themeId Theme identifier
|
||||
* @param mode Optional mode override
|
||||
*/
|
||||
switchTheme(themeId: string, mode?: 'light' | 'dark'): void {
|
||||
const theme = this.getTheme(themeId);
|
||||
if (!theme) {
|
||||
console.warn(`Theme not found: ${themeId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentTheme = theme;
|
||||
|
||||
if (mode) {
|
||||
this.currentMode = mode;
|
||||
this._currentMode$.next(mode);
|
||||
ThemeStorage.saveCurrentMode(mode);
|
||||
}
|
||||
|
||||
// Apply the palette
|
||||
const palette = this.currentMode === 'light' ? theme.lightPalette : theme.darkPalette;
|
||||
CSSVariableManager.applyPaletteWithTransition(palette, this.currentMode);
|
||||
|
||||
// Save preference
|
||||
ThemeStorage.saveCurrentTheme(themeId);
|
||||
|
||||
this.updateThemeState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme configuration directly
|
||||
* @param themeConfig Theme configuration
|
||||
*/
|
||||
applyThemeConfig(themeConfig: ThemeConfig): void {
|
||||
const theme = this.createCompleteTheme(themeConfig);
|
||||
this.currentTheme = theme;
|
||||
|
||||
const resolvedMode = themeConfig.mode === 'auto' ? this.currentMode : (themeConfig.mode || this.currentMode);
|
||||
const palette = resolvedMode === 'light' ? theme.lightPalette : theme.darkPalette;
|
||||
|
||||
CSSVariableManager.applyPaletteWithTransition(palette, resolvedMode);
|
||||
|
||||
this.currentMode = resolvedMode;
|
||||
this._currentMode$.next(resolvedMode);
|
||||
|
||||
this.updateThemeState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between light and dark mode
|
||||
*/
|
||||
toggleMode(): void {
|
||||
const newMode = this.currentMode === 'light' ? 'dark' : 'light';
|
||||
this.switchMode(newMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to specific mode
|
||||
* @param mode Target mode
|
||||
*/
|
||||
switchMode(mode: 'light' | 'dark'): void {
|
||||
if (this.currentMode === mode) return;
|
||||
|
||||
this.currentMode = mode;
|
||||
this._currentMode$.next(mode);
|
||||
|
||||
if (this.currentTheme) {
|
||||
const palette = mode === 'light' ? this.currentTheme.lightPalette : this.currentTheme.darkPalette;
|
||||
CSSVariableManager.applyPaletteWithTransition(palette, mode);
|
||||
}
|
||||
|
||||
ThemeStorage.saveCurrentMode(mode);
|
||||
this.updateThemeState();
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// COLOR UTILITIES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Generate color preview for theme selection
|
||||
* @param brandColors Brand colors
|
||||
* @returns Preview color object
|
||||
*/
|
||||
generateColorPreview(brandColors: BrandColors): { [key: string]: string } {
|
||||
return PaletteGenerator.generateColorPreview(brandColors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate color format and accessibility
|
||||
* @param color Color string to validate
|
||||
* @returns Validation results
|
||||
*/
|
||||
validateColor(color: string): ColorValidation {
|
||||
const warnings: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
// Check format validity
|
||||
if (!HCLConverter.isValidHex(color)) {
|
||||
warnings.push('Invalid hex color format');
|
||||
return { isValid: false, warnings, recommendations };
|
||||
}
|
||||
|
||||
// Check if color is too light/dark for accessibility
|
||||
const hcl = HCLConverter.hexToHcl(color);
|
||||
|
||||
if (hcl.l < 10) {
|
||||
warnings.push('Color may be too dark for some use cases');
|
||||
recommendations.push('Consider using lighter tones for better contrast');
|
||||
}
|
||||
|
||||
if (hcl.l > 90) {
|
||||
warnings.push('Color may be too light for some use cases');
|
||||
recommendations.push('Consider using darker tones for better contrast');
|
||||
}
|
||||
|
||||
// Check chroma levels
|
||||
if (hcl.c < 10) {
|
||||
recommendations.push('Low chroma - color may appear washed out');
|
||||
}
|
||||
|
||||
if (hcl.c > 80) {
|
||||
recommendations.push('High chroma - consider reducing saturation for better balance');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: warnings.length === 0,
|
||||
warnings,
|
||||
recommendations: recommendations.length > 0 ? recommendations : undefined
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// GETTERS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Get current theme state
|
||||
* @returns Current theme state
|
||||
*/
|
||||
getCurrentThemeState(): ThemeState {
|
||||
return this._themeState$.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme
|
||||
* @returns Current theme or null
|
||||
*/
|
||||
getCurrentTheme(): Theme | null {
|
||||
return this.currentTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current mode
|
||||
* @returns Current mode
|
||||
*/
|
||||
getCurrentMode(): 'light' | 'dark' {
|
||||
return this.currentMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available themes
|
||||
* @returns Array of theme previews
|
||||
*/
|
||||
getAvailableThemes(): ThemePreview[] {
|
||||
const themes: ThemePreview[] = [];
|
||||
|
||||
// Add built-in themes
|
||||
this.builtInThemes.forEach(theme => {
|
||||
themes.push({
|
||||
id: theme.config.id,
|
||||
name: theme.config.name,
|
||||
description: theme.config.description,
|
||||
primary: theme.config.colors.primary,
|
||||
secondary: theme.config.colors.secondary,
|
||||
tertiary: theme.config.colors.tertiary
|
||||
});
|
||||
});
|
||||
|
||||
// Add custom themes
|
||||
this.customThemes.forEach(theme => {
|
||||
themes.push({
|
||||
id: theme.config.id,
|
||||
name: theme.config.name,
|
||||
description: theme.config.description,
|
||||
primary: theme.config.colors.primary,
|
||||
secondary: theme.config.colors.secondary,
|
||||
tertiary: theme.config.colors.tertiary
|
||||
});
|
||||
});
|
||||
|
||||
return themes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if service is initialized
|
||||
* @returns True if initialized
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this._themeState$.value.isInitialized;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// IMPORT/EXPORT
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Export theme configuration
|
||||
* @param themeId Theme to export
|
||||
* @returns JSON string or null
|
||||
*/
|
||||
exportTheme(themeId: string): string | null {
|
||||
return ThemeStorage.exportTheme(themeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import theme from JSON
|
||||
* @param jsonString Theme JSON data
|
||||
* @returns Imported theme or null
|
||||
*/
|
||||
importTheme(jsonString: string): ThemeConfig | null {
|
||||
const imported = ThemeStorage.importTheme(jsonString);
|
||||
if (imported) {
|
||||
// Reload custom themes to include the new one
|
||||
this.loadCustomThemesFromStorage();
|
||||
this.updateThemeState();
|
||||
}
|
||||
return imported;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// PRIVATE METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Create complete theme with both light and dark palettes
|
||||
* @param config Theme configuration
|
||||
* @returns Complete theme
|
||||
*/
|
||||
private createCompleteTheme(config: ThemeConfig): Theme {
|
||||
const lightPalette = PaletteGenerator.generateMaterialPalette(config.colors, 'light');
|
||||
const darkPalette = PaletteGenerator.generateMaterialPalette(config.colors, 'dark');
|
||||
|
||||
const lightCSSVariables = CSSVariableManager.generateCSSVariables(lightPalette, 'light');
|
||||
const darkCSSVariables = CSSVariableManager.generateCSSVariables(darkPalette, 'dark');
|
||||
|
||||
return {
|
||||
config,
|
||||
lightPalette,
|
||||
darkPalette,
|
||||
cssVariables: lightCSSVariables // Default to light mode variables
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme by ID (checks both built-in and custom)
|
||||
* @param themeId Theme identifier
|
||||
* @returns Theme or null
|
||||
*/
|
||||
private getTheme(themeId: string): Theme | null {
|
||||
return this.builtInThemes.get(themeId) || this.customThemes.get(themeId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load custom themes from storage
|
||||
*/
|
||||
private loadCustomThemesFromStorage(): void {
|
||||
const customThemes = ThemeStorage.getCustomThemes();
|
||||
|
||||
Object.values(customThemes).forEach(config => {
|
||||
const theme = this.createCompleteTheme(config);
|
||||
this.customThemes.set(config.id, theme);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore user's theme preferences
|
||||
*/
|
||||
private restoreUserPreferences(): void {
|
||||
const savedThemeId = ThemeStorage.getCurrentTheme();
|
||||
const savedMode = ThemeStorage.getCurrentMode();
|
||||
|
||||
this.currentMode = savedMode;
|
||||
this._currentMode$.next(savedMode);
|
||||
|
||||
if (savedThemeId && this.getTheme(savedThemeId)) {
|
||||
this.switchTheme(savedThemeId, savedMode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up automatic mode detection based on system preference
|
||||
*/
|
||||
private setupAutoModeDetection(): void {
|
||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
const systemMode = e.matches ? 'dark' : 'light';
|
||||
this.switchMode(systemMode);
|
||||
};
|
||||
|
||||
// Set initial mode
|
||||
const initialSystemMode = mediaQuery.matches ? 'dark' : 'light';
|
||||
this.switchMode(initialSystemMode);
|
||||
|
||||
// Listen for changes
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update reactive theme state
|
||||
*/
|
||||
private updateThemeState(): void {
|
||||
const state: ThemeState = {
|
||||
currentTheme: this.currentTheme,
|
||||
availableThemes: this.getAvailableThemes(),
|
||||
mode: this.currentMode,
|
||||
isInitialized: this._themeState$.value.isInitialized
|
||||
};
|
||||
|
||||
this._themeState$.next(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark service as initialized
|
||||
*/
|
||||
private markAsInitialized(): void {
|
||||
const currentState = this._themeState$.value;
|
||||
this._themeState$.next({
|
||||
...currentState,
|
||||
isInitialized: true
|
||||
});
|
||||
}
|
||||
}
|
||||
239
projects/hcl-studio/src/lib/models/hcl.models.ts
Normal file
239
projects/hcl-studio/src/lib/models/hcl.models.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* ==========================================================================
|
||||
* HCL STUDIO - TYPE DEFINITIONS
|
||||
* ==========================================================================
|
||||
* Core interfaces and types for HCL color manipulation and theme management
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
// ==========================================================================
|
||||
// COLOR TYPES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* HCL color representation
|
||||
*/
|
||||
export interface HCL {
|
||||
/** Hue (0-360 degrees) */
|
||||
h: number;
|
||||
/** Chroma (0-100+) */
|
||||
c: number;
|
||||
/** Lightness (0-100) */
|
||||
l: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RGB color representation
|
||||
*/
|
||||
export interface RGB {
|
||||
/** Red (0-255) */
|
||||
r: number;
|
||||
/** Green (0-255) */
|
||||
g: number;
|
||||
/** Blue (0-255) */
|
||||
b: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LAB color representation (intermediate for HCL conversion)
|
||||
*/
|
||||
export interface LAB {
|
||||
/** Lightness (0-100) */
|
||||
l: number;
|
||||
/** Green-Red axis (-128 to 127) */
|
||||
a: number;
|
||||
/** Blue-Yellow axis (-128 to 127) */
|
||||
b: number;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// PALETTE TYPES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Tonal palette with Material Design 3 tone levels
|
||||
*/
|
||||
export interface TonalPalette {
|
||||
0: string;
|
||||
4: string;
|
||||
5: string;
|
||||
6: string;
|
||||
10: string;
|
||||
12: string;
|
||||
15: string;
|
||||
17: string;
|
||||
20: string;
|
||||
22: string;
|
||||
25: string;
|
||||
30: string;
|
||||
35: string;
|
||||
40: string;
|
||||
50: string;
|
||||
60: string;
|
||||
70: string;
|
||||
80: string;
|
||||
87: string;
|
||||
90: string;
|
||||
92: string;
|
||||
94: string;
|
||||
95: string;
|
||||
96: string;
|
||||
98: string;
|
||||
99: string;
|
||||
100: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete Material Design 3 color palette
|
||||
*/
|
||||
export interface MaterialPalette {
|
||||
primary: TonalPalette;
|
||||
secondary: TonalPalette;
|
||||
tertiary: TonalPalette;
|
||||
neutral: TonalPalette;
|
||||
neutralVariant: TonalPalette;
|
||||
error: TonalPalette;
|
||||
}
|
||||
|
||||
/**
|
||||
* Brand seed colors
|
||||
*/
|
||||
export interface BrandColors {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
tertiary: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// THEME TYPES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Theme configuration
|
||||
*/
|
||||
export interface ThemeConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
colors: BrandColors;
|
||||
mode?: 'light' | 'dark' | 'auto';
|
||||
createdAt?: Date;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete theme with generated palettes
|
||||
*/
|
||||
export interface Theme {
|
||||
config: ThemeConfig;
|
||||
lightPalette: MaterialPalette;
|
||||
darkPalette: MaterialPalette;
|
||||
cssVariables: CSSVariables;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS custom properties mapping
|
||||
*/
|
||||
export interface CSSVariables {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme preview information
|
||||
*/
|
||||
export interface ThemePreview {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
primary: string;
|
||||
secondary: string;
|
||||
tertiary: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme collection
|
||||
*/
|
||||
export interface ThemeCollection {
|
||||
[themeId: string]: Omit<ThemeConfig, 'id'>;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// VALIDATION TYPES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Color validation result
|
||||
*/
|
||||
export interface ColorValidation {
|
||||
isValid: boolean;
|
||||
warnings: string[];
|
||||
recommendations?: string[];
|
||||
accessibility?: AccessibilityInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessibility information
|
||||
*/
|
||||
export interface AccessibilityInfo {
|
||||
contrastRatio: number;
|
||||
wcagLevel: 'AA' | 'AAA' | 'fail';
|
||||
colorBlindSafe: boolean;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// SERVICE CONFIGURATION TYPES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* HCL Studio initialization options
|
||||
*/
|
||||
export interface HCLStudioOptions {
|
||||
/** Default theme to apply */
|
||||
defaultTheme?: string | ThemeConfig;
|
||||
/** Enable automatic mode switching based on system preference */
|
||||
autoMode?: boolean;
|
||||
/** Storage key for theme persistence */
|
||||
storageKey?: string;
|
||||
/** Custom theme presets */
|
||||
themes?: ThemeCollection;
|
||||
/** Validation options */
|
||||
validation?: {
|
||||
enableContrastCheck: boolean;
|
||||
enableColorBlindCheck: boolean;
|
||||
minContrastRatio: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme state for reactive updates
|
||||
*/
|
||||
export interface ThemeState {
|
||||
currentTheme: Theme | null;
|
||||
availableThemes: ThemePreview[];
|
||||
mode: 'light' | 'dark';
|
||||
isInitialized: boolean;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// UTILITY TYPES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Color format types
|
||||
*/
|
||||
export type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'hcl';
|
||||
|
||||
/**
|
||||
* Mode switching options
|
||||
*/
|
||||
export type ThemeMode = 'light' | 'dark' | 'auto';
|
||||
|
||||
/**
|
||||
* Contrast levels for WCAG compliance
|
||||
*/
|
||||
export type ContrastLevel = 'AA' | 'AAA';
|
||||
|
||||
/**
|
||||
* Color harmony types
|
||||
*/
|
||||
export type ColorHarmony = 'complementary' | 'analogous' | 'triadic' | 'tetradic' | 'monochromatic';
|
||||
320
projects/hcl-studio/src/lib/themes/css-variable-manager.ts
Normal file
320
projects/hcl-studio/src/lib/themes/css-variable-manager.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* ==========================================================================
|
||||
* CSS VARIABLE MANAGER
|
||||
* ==========================================================================
|
||||
* Manages CSS custom properties for dynamic theme switching
|
||||
* Maps Material Design 3 palette to existing design system variables
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
import { MaterialPalette, CSSVariables } from '../models/hcl.models';
|
||||
import { PaletteGenerator } from '../core/palette-generator';
|
||||
|
||||
export class CSSVariableManager {
|
||||
|
||||
// ==========================================================================
|
||||
// VARIABLE MAPPING
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Generate CSS variables from Material Design 3 palette
|
||||
* @param palette Material palette
|
||||
* @param mode Light or dark mode
|
||||
* @returns CSS variables object
|
||||
*/
|
||||
static generateCSSVariables(palette: MaterialPalette, mode: 'light' | 'dark' = 'light'): CSSVariables {
|
||||
const variables: CSSVariables = {};
|
||||
|
||||
// ==========================================================================
|
||||
// PRIMARY COLORS
|
||||
// ==========================================================================
|
||||
variables['--color-primary-key'] = palette.primary[40];
|
||||
variables['--color-primary'] = PaletteGenerator.getPrimaryTone(palette.primary, 'main', mode);
|
||||
variables['--color-on-primary'] = mode === 'light' ? palette.primary[100] : palette.primary[20];
|
||||
variables['--color-primary-container'] = PaletteGenerator.getPrimaryTone(palette.primary, 'container', mode);
|
||||
variables['--color-on-primary-container'] = PaletteGenerator.getPrimaryTone(palette.primary, 'on-container', mode);
|
||||
variables['--color-inverse-primary'] = mode === 'light' ? palette.primary[80] : palette.primary[40];
|
||||
|
||||
// ==========================================================================
|
||||
// SECONDARY COLORS
|
||||
// ==========================================================================
|
||||
variables['--color-secondary-key'] = palette.secondary[40];
|
||||
variables['--color-secondary'] = mode === 'light' ? palette.secondary[40] : palette.secondary[80];
|
||||
variables['--color-on-secondary'] = mode === 'light' ? palette.secondary[100] : palette.secondary[20];
|
||||
variables['--color-secondary-container'] = mode === 'light' ? palette.secondary[90] : palette.secondary[30];
|
||||
variables['--color-on-secondary-container'] = mode === 'light' ? palette.secondary[10] : palette.secondary[90];
|
||||
|
||||
// ==========================================================================
|
||||
// TERTIARY COLORS
|
||||
// ==========================================================================
|
||||
variables['--color-tertiary-key'] = palette.tertiary[40];
|
||||
variables['--color-tertiary'] = mode === 'light' ? palette.tertiary[40] : palette.tertiary[80];
|
||||
variables['--color-on-tertiary'] = mode === 'light' ? palette.tertiary[100] : palette.tertiary[20];
|
||||
variables['--color-tertiary-container'] = mode === 'light' ? palette.tertiary[90] : palette.tertiary[30];
|
||||
variables['--color-on-tertiary-container'] = mode === 'light' ? palette.tertiary[10] : palette.tertiary[90];
|
||||
|
||||
// ==========================================================================
|
||||
// ERROR COLORS
|
||||
// ==========================================================================
|
||||
variables['--color-error-key'] = palette.error[40];
|
||||
variables['--color-error'] = mode === 'light' ? palette.error[40] : palette.error[80];
|
||||
variables['--color-on-error'] = mode === 'light' ? palette.error[100] : palette.error[20];
|
||||
variables['--color-error-container'] = mode === 'light' ? palette.error[90] : palette.error[30];
|
||||
variables['--color-on-error-container'] = mode === 'light' ? palette.error[10] : palette.error[90];
|
||||
|
||||
// ==========================================================================
|
||||
// NEUTRAL COLORS
|
||||
// ==========================================================================
|
||||
variables['--color-neutral-key'] = palette.neutral[10];
|
||||
variables['--color-neutral'] = mode === 'light' ? palette.neutral[25] : palette.neutral[80];
|
||||
variables['--color-neutral-variant-key'] = palette.neutralVariant[15];
|
||||
variables['--color-neutral-variant'] = mode === 'light' ? palette.neutralVariant[30] : palette.neutralVariant[70];
|
||||
|
||||
// ==========================================================================
|
||||
// SURFACE COLORS (The most important for your existing system)
|
||||
// ==========================================================================
|
||||
if (mode === 'light') {
|
||||
variables['--color-surface'] = palette.neutral[99];
|
||||
variables['--color-surface-bright'] = palette.neutral[99];
|
||||
variables['--color-surface-dim'] = palette.neutral[87];
|
||||
variables['--color-on-surface'] = palette.neutral[10];
|
||||
variables['--color-surface-lowest'] = palette.neutral[100];
|
||||
variables['--color-surface-low'] = palette.neutral[96];
|
||||
variables['--color-surface-container'] = palette.neutral[94];
|
||||
variables['--color-surface-high'] = palette.neutral[92];
|
||||
variables['--color-surface-highest'] = palette.neutral[90];
|
||||
variables['--color-surface-variant'] = palette.neutralVariant[90];
|
||||
variables['--color-on-surface-variant'] = palette.neutralVariant[30];
|
||||
variables['--color-inverse-surface'] = palette.neutral[20];
|
||||
variables['--color-inverse-on-surface'] = palette.neutral[95];
|
||||
} else {
|
||||
variables['--color-surface'] = palette.neutral[10];
|
||||
variables['--color-surface-bright'] = palette.neutral[20];
|
||||
variables['--color-surface-dim'] = palette.neutral[6];
|
||||
variables['--color-on-surface'] = palette.neutral[90];
|
||||
variables['--color-surface-lowest'] = palette.neutral[0];
|
||||
variables['--color-surface-low'] = palette.neutral[4];
|
||||
variables['--color-surface-container'] = palette.neutral[12];
|
||||
variables['--color-surface-high'] = palette.neutral[17];
|
||||
variables['--color-surface-highest'] = palette.neutral[22];
|
||||
variables['--color-surface-variant'] = palette.neutralVariant[30];
|
||||
variables['--color-on-surface-variant'] = palette.neutralVariant[80];
|
||||
variables['--color-inverse-surface'] = palette.neutral[90];
|
||||
variables['--color-inverse-on-surface'] = palette.neutral[20];
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// BACKGROUND COLORS
|
||||
// ==========================================================================
|
||||
variables['--color-background'] = variables['--color-surface'];
|
||||
variables['--color-on-background'] = variables['--color-on-surface'];
|
||||
|
||||
// ==========================================================================
|
||||
// OUTLINE COLORS
|
||||
// ==========================================================================
|
||||
variables['--color-outline'] = mode === 'light' ? palette.neutralVariant[50] : palette.neutralVariant[60];
|
||||
variables['--color-outline-variant'] = mode === 'light' ? palette.neutralVariant[80] : palette.neutralVariant[30];
|
||||
|
||||
// ==========================================================================
|
||||
// UTILITY COLORS
|
||||
// ==========================================================================
|
||||
variables['--color-shadow'] = '#000000';
|
||||
variables['--color-surface-tint'] = variables['--color-primary'];
|
||||
variables['--color-scrim'] = '#000000';
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// APPLY METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Apply CSS variables to document root
|
||||
* @param variables CSS variables to apply
|
||||
*/
|
||||
static applyVariables(variables: CSSVariables): void {
|
||||
const root = document.documentElement;
|
||||
|
||||
Object.entries(variables).forEach(([property, value]) => {
|
||||
root.style.setProperty(property, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply palette to document root
|
||||
* @param palette Material palette
|
||||
* @param mode Light or dark mode
|
||||
*/
|
||||
static applyPalette(palette: MaterialPalette, mode: 'light' | 'dark' = 'light'): void {
|
||||
const variables = this.generateCSSVariables(palette, mode);
|
||||
this.applyVariables(variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all theme-related CSS variables
|
||||
*/
|
||||
static clearVariables(): void {
|
||||
const root = document.documentElement;
|
||||
const variableNames = this.getAllVariableNames();
|
||||
|
||||
variableNames.forEach(name => {
|
||||
root.style.removeProperty(name);
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// UTILITY METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Get all CSS variable names managed by HCL Studio
|
||||
* @returns Array of CSS variable names
|
||||
*/
|
||||
private static getAllVariableNames(): string[] {
|
||||
return [
|
||||
// Primary
|
||||
'--color-primary-key',
|
||||
'--color-primary',
|
||||
'--color-on-primary',
|
||||
'--color-primary-container',
|
||||
'--color-on-primary-container',
|
||||
'--color-inverse-primary',
|
||||
|
||||
// Secondary
|
||||
'--color-secondary-key',
|
||||
'--color-secondary',
|
||||
'--color-on-secondary',
|
||||
'--color-secondary-container',
|
||||
'--color-on-secondary-container',
|
||||
|
||||
// Tertiary
|
||||
'--color-tertiary-key',
|
||||
'--color-tertiary',
|
||||
'--color-on-tertiary',
|
||||
'--color-tertiary-container',
|
||||
'--color-on-tertiary-container',
|
||||
|
||||
// Error
|
||||
'--color-error-key',
|
||||
'--color-error',
|
||||
'--color-on-error',
|
||||
'--color-error-container',
|
||||
'--color-on-error-container',
|
||||
|
||||
// Neutral
|
||||
'--color-neutral-key',
|
||||
'--color-neutral',
|
||||
'--color-neutral-variant-key',
|
||||
'--color-neutral-variant',
|
||||
|
||||
// Surface
|
||||
'--color-surface',
|
||||
'--color-surface-bright',
|
||||
'--color-surface-dim',
|
||||
'--color-on-surface',
|
||||
'--color-surface-lowest',
|
||||
'--color-surface-low',
|
||||
'--color-surface-container',
|
||||
'--color-surface-high',
|
||||
'--color-surface-highest',
|
||||
'--color-surface-variant',
|
||||
'--color-on-surface-variant',
|
||||
'--color-inverse-surface',
|
||||
'--color-inverse-on-surface',
|
||||
|
||||
// Background
|
||||
'--color-background',
|
||||
'--color-on-background',
|
||||
|
||||
// Outline
|
||||
'--color-outline',
|
||||
'--color-outline-variant',
|
||||
|
||||
// Utility
|
||||
'--color-shadow',
|
||||
'--color-surface-tint',
|
||||
'--color-scrim'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current CSS variable value
|
||||
* @param variableName CSS variable name (with or without --)
|
||||
* @returns Current variable value
|
||||
*/
|
||||
static getCurrentVariableValue(variableName: string): string {
|
||||
const name = variableName.startsWith('--') ? variableName : `--${variableName}`;
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if dark mode is currently active
|
||||
* @returns True if dark mode is active
|
||||
*/
|
||||
static isDarkModeActive(): boolean {
|
||||
// Check if surface color is dark (low lightness)
|
||||
const surfaceColor = this.getCurrentVariableValue('--color-surface');
|
||||
if (!surfaceColor) return false;
|
||||
|
||||
try {
|
||||
// Simple check: if surface is closer to black than white, it's dark mode
|
||||
const surfaceHex = surfaceColor.startsWith('#') ? surfaceColor : '#FFFFFF';
|
||||
const surfaceRgb = this.hexToRgb(surfaceHex);
|
||||
const brightness = (surfaceRgb.r * 299 + surfaceRgb.g * 587 + surfaceRgb.b * 114) / 1000;
|
||||
return brightness < 128;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple hex to RGB conversion for utility
|
||||
* @param hex Hex color
|
||||
* @returns RGB values
|
||||
*/
|
||||
private static hexToRgb(hex: string): { r: number, g: number, b: number } {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : { r: 0, g: 0, b: 0 };
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// ANIMATION SUPPORT
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Apply variables with smooth transition
|
||||
* @param variables CSS variables to apply
|
||||
* @param duration Transition duration in milliseconds
|
||||
*/
|
||||
static applyVariablesWithTransition(variables: CSSVariables, duration: number = 300): void {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Add transition for smooth theme changes
|
||||
const transitionProperties = Object.keys(variables).join(', ');
|
||||
root.style.setProperty('transition', `${transitionProperties} ${duration}ms ease-in-out`);
|
||||
|
||||
// Apply variables
|
||||
this.applyVariables(variables);
|
||||
|
||||
// Remove transition after animation completes
|
||||
setTimeout(() => {
|
||||
root.style.removeProperty('transition');
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply palette with smooth transition
|
||||
* @param palette Material palette
|
||||
* @param mode Light or dark mode
|
||||
* @param duration Transition duration in milliseconds
|
||||
*/
|
||||
static applyPaletteWithTransition(palette: MaterialPalette, mode: 'light' | 'dark' = 'light', duration: number = 300): void {
|
||||
const variables = this.generateCSSVariables(palette, mode);
|
||||
this.applyVariablesWithTransition(variables, duration);
|
||||
}
|
||||
}
|
||||
357
projects/hcl-studio/src/lib/themes/theme-presets.ts
Normal file
357
projects/hcl-studio/src/lib/themes/theme-presets.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* ==========================================================================
|
||||
* DEFAULT THEME PRESETS
|
||||
* ==========================================================================
|
||||
* Built-in theme configurations with carefully selected color combinations
|
||||
* Provides professionally designed themes for immediate use
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
import { ThemeCollection } from '../models/hcl.models';
|
||||
|
||||
/**
|
||||
* Default theme collection with professionally curated color combinations
|
||||
*/
|
||||
export const DEFAULT_THEMES: ThemeCollection = {
|
||||
|
||||
// ==========================================================================
|
||||
// MATERIAL DESIGN THEMES
|
||||
// ==========================================================================
|
||||
|
||||
'material-purple': {
|
||||
name: 'Material Purple',
|
||||
description: 'Classic Material Design purple theme',
|
||||
colors: {
|
||||
primary: '#6750A4',
|
||||
secondary: '#625B71',
|
||||
tertiary: '#7D5260'
|
||||
}
|
||||
},
|
||||
|
||||
'material-blue': {
|
||||
name: 'Material Blue',
|
||||
description: 'Material Design blue theme',
|
||||
colors: {
|
||||
primary: '#1976D2',
|
||||
secondary: '#455A64',
|
||||
tertiary: '#7B1FA2'
|
||||
}
|
||||
},
|
||||
|
||||
'material-green': {
|
||||
name: 'Material Green',
|
||||
description: 'Material Design green theme',
|
||||
colors: {
|
||||
primary: '#2E7D32',
|
||||
secondary: '#5D4037',
|
||||
tertiary: '#1976D2'
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// CORPORATE/BUSINESS THEMES
|
||||
// ==========================================================================
|
||||
|
||||
'corporate-blue': {
|
||||
name: 'Corporate Blue',
|
||||
description: 'Professional blue theme for business applications',
|
||||
colors: {
|
||||
primary: '#1565C0',
|
||||
secondary: '#37474F',
|
||||
tertiary: '#5E35B1'
|
||||
}
|
||||
},
|
||||
|
||||
'executive': {
|
||||
name: 'Executive',
|
||||
description: 'Sophisticated theme with navy and gold accents',
|
||||
colors: {
|
||||
primary: '#263238',
|
||||
secondary: '#455A64',
|
||||
tertiary: '#FF8F00'
|
||||
}
|
||||
},
|
||||
|
||||
'modern-corporate': {
|
||||
name: 'Modern Corporate',
|
||||
description: 'Contemporary corporate theme with teal accents',
|
||||
colors: {
|
||||
primary: '#00695C',
|
||||
secondary: '#424242',
|
||||
tertiary: '#7B1FA2'
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// NATURE-INSPIRED THEMES
|
||||
// ==========================================================================
|
||||
|
||||
'forest': {
|
||||
name: 'Forest',
|
||||
description: 'Deep greens inspired by forest landscapes',
|
||||
colors: {
|
||||
primary: '#2E7D32',
|
||||
secondary: '#5D4037',
|
||||
tertiary: '#FF6F00'
|
||||
}
|
||||
},
|
||||
|
||||
'ocean': {
|
||||
name: 'Ocean',
|
||||
description: 'Cool blues and teals reminiscent of ocean depths',
|
||||
colors: {
|
||||
primary: '#006064',
|
||||
secondary: '#263238',
|
||||
tertiary: '#00ACC1'
|
||||
}
|
||||
},
|
||||
|
||||
'sunset': {
|
||||
name: 'Sunset',
|
||||
description: 'Warm oranges and deep purples of a sunset sky',
|
||||
colors: {
|
||||
primary: '#FF6F00',
|
||||
secondary: '#5D4037',
|
||||
tertiary: '#7B1FA2'
|
||||
}
|
||||
},
|
||||
|
||||
'lavender': {
|
||||
name: 'Lavender Fields',
|
||||
description: 'Soft purples and greens inspired by lavender fields',
|
||||
colors: {
|
||||
primary: '#7E57C2',
|
||||
secondary: '#689F38',
|
||||
tertiary: '#8BC34A'
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// VIBRANT/CREATIVE THEMES
|
||||
// ==========================================================================
|
||||
|
||||
'electric': {
|
||||
name: 'Electric',
|
||||
description: 'High-energy theme with electric blues and purples',
|
||||
colors: {
|
||||
primary: '#3F51B5',
|
||||
secondary: '#9C27B0',
|
||||
tertiary: '#00BCD4'
|
||||
}
|
||||
},
|
||||
|
||||
'neon': {
|
||||
name: 'Neon',
|
||||
description: 'Bold neon-inspired theme for creative applications',
|
||||
colors: {
|
||||
primary: '#E91E63',
|
||||
secondary: '#9C27B0',
|
||||
tertiary: '#00BCD4'
|
||||
}
|
||||
},
|
||||
|
||||
'cyberpunk': {
|
||||
name: 'Cyberpunk',
|
||||
description: 'Futuristic theme with cyan and magenta accents',
|
||||
colors: {
|
||||
primary: '#00E5FF',
|
||||
secondary: '#263238',
|
||||
tertiary: '#FF1744'
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// WARM/COZY THEMES
|
||||
// ==========================================================================
|
||||
|
||||
'autumn': {
|
||||
name: 'Autumn',
|
||||
description: 'Warm autumn colors with orange and brown tones',
|
||||
colors: {
|
||||
primary: '#FF5722',
|
||||
secondary: '#5D4037',
|
||||
tertiary: '#FF9800'
|
||||
}
|
||||
},
|
||||
|
||||
'coffee': {
|
||||
name: 'Coffee House',
|
||||
description: 'Rich browns and warm tones for a cozy feel',
|
||||
colors: {
|
||||
primary: '#5D4037',
|
||||
secondary: '#8D6E63',
|
||||
tertiary: '#FF8F00'
|
||||
}
|
||||
},
|
||||
|
||||
'golden': {
|
||||
name: 'Golden',
|
||||
description: 'Luxurious theme with gold and deep blue accents',
|
||||
colors: {
|
||||
primary: '#FF8F00',
|
||||
secondary: '#1565C0',
|
||||
tertiary: '#7B1FA2'
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// MONOCHROMATIC THEMES
|
||||
// ==========================================================================
|
||||
|
||||
'midnight': {
|
||||
name: 'Midnight',
|
||||
description: 'Dark theme with subtle blue undertones',
|
||||
colors: {
|
||||
primary: '#1A237E',
|
||||
secondary: '#283593',
|
||||
tertiary: '#3F51B5'
|
||||
}
|
||||
},
|
||||
|
||||
'slate': {
|
||||
name: 'Slate',
|
||||
description: 'Sophisticated grays with blue-gray undertones',
|
||||
colors: {
|
||||
primary: '#455A64',
|
||||
secondary: '#607D8B',
|
||||
tertiary: '#90A4AE'
|
||||
}
|
||||
},
|
||||
|
||||
'charcoal': {
|
||||
name: 'Charcoal',
|
||||
description: 'Dark charcoal theme with minimal color accents',
|
||||
colors: {
|
||||
primary: '#263238',
|
||||
secondary: '#37474F',
|
||||
tertiary: '#546E7A'
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// PASTEL/SOFT THEMES
|
||||
// ==========================================================================
|
||||
|
||||
'soft-pink': {
|
||||
name: 'Soft Pink',
|
||||
description: 'Gentle theme with soft pink and lavender tones',
|
||||
colors: {
|
||||
primary: '#AD1457',
|
||||
secondary: '#7B1FA2',
|
||||
tertiary: '#512DA8'
|
||||
}
|
||||
},
|
||||
|
||||
'mint': {
|
||||
name: 'Mint',
|
||||
description: 'Fresh theme with mint greens and soft blues',
|
||||
colors: {
|
||||
primary: '#00695C',
|
||||
secondary: '#00838F',
|
||||
tertiary: '#0277BD'
|
||||
}
|
||||
},
|
||||
|
||||
'cream': {
|
||||
name: 'Cream',
|
||||
description: 'Warm, soft theme with cream and beige tones',
|
||||
colors: {
|
||||
primary: '#8D6E63',
|
||||
secondary: '#A1887F',
|
||||
tertiary: '#FF8A65'
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// HIGH CONTRAST THEMES
|
||||
// ==========================================================================
|
||||
|
||||
'high-contrast': {
|
||||
name: 'High Contrast',
|
||||
description: 'Maximum contrast theme for accessibility',
|
||||
colors: {
|
||||
primary: '#000000',
|
||||
secondary: '#424242',
|
||||
tertiary: '#757575'
|
||||
}
|
||||
},
|
||||
|
||||
'accessibility': {
|
||||
name: 'Accessibility',
|
||||
description: 'Optimized for maximum readability and contrast',
|
||||
colors: {
|
||||
primary: '#1565C0',
|
||||
secondary: '#0D47A1',
|
||||
tertiary: '#1A237E'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get theme by category
|
||||
*/
|
||||
export const getThemesByCategory = () => {
|
||||
return {
|
||||
material: ['material-purple', 'material-blue', 'material-green'],
|
||||
corporate: ['corporate-blue', 'executive', 'modern-corporate'],
|
||||
nature: ['forest', 'ocean', 'sunset', 'lavender'],
|
||||
vibrant: ['electric', 'neon', 'cyberpunk'],
|
||||
warm: ['autumn', 'coffee', 'golden'],
|
||||
monochrome: ['midnight', 'slate', 'charcoal'],
|
||||
pastel: ['soft-pink', 'mint', 'cream'],
|
||||
accessibility: ['high-contrast', 'accessibility']
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get recommended themes for specific use cases
|
||||
*/
|
||||
export const getRecommendedThemes = () => {
|
||||
return {
|
||||
business: ['corporate-blue', 'executive', 'modern-corporate', 'slate'],
|
||||
creative: ['electric', 'neon', 'cyberpunk', 'sunset'],
|
||||
healthcare: ['soft-pink', 'mint', 'ocean', 'accessibility'],
|
||||
finance: ['corporate-blue', 'executive', 'charcoal', 'midnight'],
|
||||
education: ['material-blue', 'forest', 'accessibility', 'mint'],
|
||||
ecommerce: ['material-purple', 'golden', 'sunset', 'electric'],
|
||||
dashboard: ['corporate-blue', 'slate', 'midnight', 'accessibility'],
|
||||
mobile: ['material-purple', 'material-blue', 'ocean', 'neon']
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get theme metadata
|
||||
* @param themeId Theme identifier
|
||||
* @returns Theme metadata or null
|
||||
*/
|
||||
export const getThemeMetadata = (themeId: string) => {
|
||||
const theme = DEFAULT_THEMES[themeId];
|
||||
if (!theme) return null;
|
||||
|
||||
const categories = getThemesByCategory();
|
||||
const recommendations = getRecommendedThemes();
|
||||
|
||||
// Find which category this theme belongs to
|
||||
let category = 'other';
|
||||
for (const [cat, themes] of Object.entries(categories)) {
|
||||
if (themes.includes(themeId)) {
|
||||
category = cat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find recommended use cases
|
||||
const useCases: string[] = [];
|
||||
for (const [useCase, themes] of Object.entries(recommendations)) {
|
||||
if (themes.includes(themeId)) {
|
||||
useCases.push(useCase);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...theme,
|
||||
category,
|
||||
useCases,
|
||||
id: themeId
|
||||
};
|
||||
};
|
||||
423
projects/hcl-studio/src/lib/themes/theme-storage.ts
Normal file
423
projects/hcl-studio/src/lib/themes/theme-storage.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* ==========================================================================
|
||||
* THEME STORAGE SERVICE
|
||||
* ==========================================================================
|
||||
* Handles persistence of themes and user preferences using localStorage
|
||||
* Provides backup and import/export functionality
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
import { ThemeConfig, ThemeCollection } from '../models/hcl.models';
|
||||
|
||||
export class ThemeStorage {
|
||||
|
||||
private static readonly STORAGE_KEYS = {
|
||||
CURRENT_THEME: 'hcl-studio-current-theme',
|
||||
CURRENT_MODE: 'hcl-studio-current-mode',
|
||||
CUSTOM_THEMES: 'hcl-studio-custom-themes',
|
||||
USER_PREFERENCES: 'hcl-studio-preferences'
|
||||
} as const;
|
||||
|
||||
// ==========================================================================
|
||||
// CURRENT THEME PERSISTENCE
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Save current theme ID
|
||||
* @param themeId Theme identifier
|
||||
*/
|
||||
static saveCurrentTheme(themeId: string): void {
|
||||
try {
|
||||
localStorage.setItem(this.STORAGE_KEYS.CURRENT_THEME, themeId);
|
||||
} catch (error) {
|
||||
console.warn('Failed to save current theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme ID
|
||||
* @returns Current theme ID or null
|
||||
*/
|
||||
static getCurrentTheme(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(this.STORAGE_KEYS.CURRENT_THEME);
|
||||
} catch (error) {
|
||||
console.warn('Failed to get current theme:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current mode (light/dark)
|
||||
* @param mode Current mode
|
||||
*/
|
||||
static saveCurrentMode(mode: 'light' | 'dark'): void {
|
||||
try {
|
||||
localStorage.setItem(this.STORAGE_KEYS.CURRENT_MODE, mode);
|
||||
} catch (error) {
|
||||
console.warn('Failed to save current mode:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current mode
|
||||
* @returns Current mode or 'light' as default
|
||||
*/
|
||||
static getCurrentMode(): 'light' | 'dark' {
|
||||
try {
|
||||
const mode = localStorage.getItem(this.STORAGE_KEYS.CURRENT_MODE);
|
||||
return mode === 'dark' ? 'dark' : 'light';
|
||||
} catch (error) {
|
||||
console.warn('Failed to get current mode:', error);
|
||||
return 'light';
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// CUSTOM THEMES MANAGEMENT
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Save a custom theme
|
||||
* @param themeConfig Theme configuration
|
||||
*/
|
||||
static saveCustomTheme(themeConfig: ThemeConfig): void {
|
||||
try {
|
||||
const customThemes = this.getCustomThemes();
|
||||
customThemes[themeConfig.id] = {
|
||||
...themeConfig,
|
||||
createdAt: themeConfig.createdAt || new Date()
|
||||
};
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEYS.CUSTOM_THEMES, JSON.stringify(customThemes));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save custom theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all custom themes
|
||||
* @returns Custom themes collection
|
||||
*/
|
||||
static getCustomThemes(): { [id: string]: ThemeConfig } {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.STORAGE_KEYS.CUSTOM_THEMES);
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
} catch (error) {
|
||||
console.warn('Failed to get custom themes:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific custom theme
|
||||
* @param themeId Theme ID
|
||||
* @returns Theme configuration or null
|
||||
*/
|
||||
static getCustomTheme(themeId: string): ThemeConfig | null {
|
||||
const customThemes = this.getCustomThemes();
|
||||
return customThemes[themeId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete custom theme
|
||||
* @param themeId Theme ID to delete
|
||||
*/
|
||||
static deleteCustomTheme(themeId: string): void {
|
||||
try {
|
||||
const customThemes = this.getCustomThemes();
|
||||
delete customThemes[themeId];
|
||||
localStorage.setItem(this.STORAGE_KEYS.CUSTOM_THEMES, JSON.stringify(customThemes));
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete custom theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if theme is custom (user-created)
|
||||
* @param themeId Theme ID
|
||||
* @returns True if theme is custom
|
||||
*/
|
||||
static isCustomTheme(themeId: string): boolean {
|
||||
const customThemes = this.getCustomThemes();
|
||||
return themeId in customThemes;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// USER PREFERENCES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Save user preferences
|
||||
* @param preferences User preference object
|
||||
*/
|
||||
static savePreferences(preferences: any): void {
|
||||
try {
|
||||
localStorage.setItem(this.STORAGE_KEYS.USER_PREFERENCES, JSON.stringify(preferences));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save preferences:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user preferences
|
||||
* @returns User preferences or default object
|
||||
*/
|
||||
static getPreferences(): any {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.STORAGE_KEYS.USER_PREFERENCES);
|
||||
return stored ? JSON.parse(stored) : {
|
||||
autoMode: false,
|
||||
transitionDuration: 300,
|
||||
enableSystemTheme: true
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to get preferences:', error);
|
||||
return {
|
||||
autoMode: false,
|
||||
transitionDuration: 300,
|
||||
enableSystemTheme: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// IMPORT/EXPORT FUNCTIONALITY
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Export theme configuration as JSON string
|
||||
* @param themeId Theme ID to export
|
||||
* @returns JSON string or null if theme not found
|
||||
*/
|
||||
static exportTheme(themeId: string): string | null {
|
||||
const customTheme = this.getCustomTheme(themeId);
|
||||
if (!customTheme) return null;
|
||||
|
||||
const exportData = {
|
||||
version: '1.0',
|
||||
exportDate: new Date().toISOString(),
|
||||
theme: customTheme
|
||||
};
|
||||
|
||||
return JSON.stringify(exportData, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import theme from JSON string
|
||||
* @param jsonString JSON theme data
|
||||
* @returns Imported theme config or null if invalid
|
||||
*/
|
||||
static importTheme(jsonString: string): ThemeConfig | null {
|
||||
try {
|
||||
const importData = JSON.parse(jsonString);
|
||||
|
||||
// Validate import data structure
|
||||
if (!importData.theme || !importData.theme.id || !importData.theme.name || !importData.theme.colors) {
|
||||
throw new Error('Invalid theme format');
|
||||
}
|
||||
|
||||
const themeConfig: ThemeConfig = {
|
||||
...importData.theme,
|
||||
createdAt: new Date(),
|
||||
description: importData.theme.description || 'Imported theme'
|
||||
};
|
||||
|
||||
// Check for ID conflicts and rename if necessary
|
||||
const existingThemes = this.getCustomThemes();
|
||||
if (existingThemes[themeConfig.id]) {
|
||||
themeConfig.id = this.generateUniqueId(themeConfig.id, existingThemes);
|
||||
}
|
||||
|
||||
// Save imported theme
|
||||
this.saveCustomTheme(themeConfig);
|
||||
|
||||
return themeConfig;
|
||||
} catch (error) {
|
||||
console.warn('Failed to import theme:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all custom themes
|
||||
* @returns JSON string with all custom themes
|
||||
*/
|
||||
static exportAllThemes(): string {
|
||||
const customThemes = this.getCustomThemes();
|
||||
|
||||
const exportData = {
|
||||
version: '1.0',
|
||||
exportDate: new Date().toISOString(),
|
||||
themes: customThemes
|
||||
};
|
||||
|
||||
return JSON.stringify(exportData, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import multiple themes from JSON string
|
||||
* @param jsonString JSON data with multiple themes
|
||||
* @returns Array of imported theme IDs
|
||||
*/
|
||||
static importMultipleThemes(jsonString: string): string[] {
|
||||
try {
|
||||
const importData = JSON.parse(jsonString);
|
||||
const importedIds: string[] = [];
|
||||
|
||||
if (!importData.themes) {
|
||||
throw new Error('Invalid multi-theme format');
|
||||
}
|
||||
|
||||
const existingThemes = this.getCustomThemes();
|
||||
|
||||
Object.values(importData.themes).forEach((theme: any) => {
|
||||
if (theme.id && theme.name && theme.colors) {
|
||||
let themeConfig: ThemeConfig = {
|
||||
...theme,
|
||||
createdAt: new Date(),
|
||||
description: theme.description || 'Imported theme'
|
||||
};
|
||||
|
||||
// Handle ID conflicts
|
||||
if (existingThemes[themeConfig.id]) {
|
||||
themeConfig.id = this.generateUniqueId(themeConfig.id, existingThemes);
|
||||
}
|
||||
|
||||
this.saveCustomTheme(themeConfig);
|
||||
importedIds.push(themeConfig.id);
|
||||
}
|
||||
});
|
||||
|
||||
return importedIds;
|
||||
} catch (error) {
|
||||
console.warn('Failed to import multiple themes:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// UTILITY METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Generate unique theme ID to avoid conflicts
|
||||
* @param baseId Base theme ID
|
||||
* @param existingThemes Existing themes to check against
|
||||
* @returns Unique theme ID
|
||||
*/
|
||||
private static generateUniqueId(baseId: string, existingThemes: { [id: string]: any }): string {
|
||||
let counter = 1;
|
||||
let newId = `${baseId}-${counter}`;
|
||||
|
||||
while (existingThemes[newId]) {
|
||||
counter++;
|
||||
newId = `${baseId}-${counter}`;
|
||||
}
|
||||
|
||||
return newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all stored data (reset to defaults)
|
||||
*/
|
||||
static clearAllData(): void {
|
||||
try {
|
||||
Object.values(this.STORAGE_KEYS).forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to clear storage data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage usage information
|
||||
* @returns Storage usage stats
|
||||
*/
|
||||
static getStorageInfo(): { customThemes: number; totalSize: number; isAvailable: boolean } {
|
||||
try {
|
||||
const customThemes = Object.keys(this.getCustomThemes()).length;
|
||||
let totalSize = 0;
|
||||
|
||||
Object.values(this.STORAGE_KEYS).forEach(key => {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item) {
|
||||
totalSize += item.length;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
customThemes,
|
||||
totalSize,
|
||||
isAvailable: true
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
customThemes: 0,
|
||||
totalSize: 0,
|
||||
isAvailable: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if localStorage is available
|
||||
* @returns True if localStorage is available
|
||||
*/
|
||||
static isStorageAvailable(): boolean {
|
||||
try {
|
||||
const test = 'hcl-studio-test';
|
||||
localStorage.setItem(test, test);
|
||||
localStorage.removeItem(test);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup of all theme data
|
||||
* @returns Backup data object
|
||||
*/
|
||||
static createBackup(): any {
|
||||
return {
|
||||
version: '1.0',
|
||||
backupDate: new Date().toISOString(),
|
||||
currentTheme: this.getCurrentTheme(),
|
||||
currentMode: this.getCurrentMode(),
|
||||
customThemes: this.getCustomThemes(),
|
||||
preferences: this.getPreferences()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from backup data
|
||||
* @param backupData Backup data object
|
||||
* @returns True if restore was successful
|
||||
*/
|
||||
static restoreFromBackup(backupData: any): boolean {
|
||||
try {
|
||||
if (backupData.currentTheme) {
|
||||
this.saveCurrentTheme(backupData.currentTheme);
|
||||
}
|
||||
|
||||
if (backupData.currentMode) {
|
||||
this.saveCurrentMode(backupData.currentMode);
|
||||
}
|
||||
|
||||
if (backupData.customThemes) {
|
||||
localStorage.setItem(this.STORAGE_KEYS.CUSTOM_THEMES, JSON.stringify(backupData.customThemes));
|
||||
}
|
||||
|
||||
if (backupData.preferences) {
|
||||
this.savePreferences(backupData.preferences);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore from backup:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
projects/hcl-studio/src/public-api.ts
Normal file
35
projects/hcl-studio/src/public-api.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* ==========================================================================
|
||||
* HCL STUDIO - PUBLIC API
|
||||
* ==========================================================================
|
||||
* Export all public interfaces, services, and utilities
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
// ==========================================================================
|
||||
// MAIN SERVICE
|
||||
// ==========================================================================
|
||||
export * from './lib/hcl-studio.service';
|
||||
|
||||
// ==========================================================================
|
||||
// CORE FUNCTIONALITY
|
||||
// ==========================================================================
|
||||
export * from './lib/core/hcl-converter';
|
||||
export * from './lib/core/palette-generator';
|
||||
|
||||
// ==========================================================================
|
||||
// THEME MANAGEMENT
|
||||
// ==========================================================================
|
||||
export * from './lib/themes/css-variable-manager';
|
||||
export * from './lib/themes/theme-storage';
|
||||
export * from './lib/themes/theme-presets';
|
||||
|
||||
// ==========================================================================
|
||||
// TYPE DEFINITIONS
|
||||
// ==========================================================================
|
||||
export * from './lib/models/hcl.models';
|
||||
|
||||
// ==========================================================================
|
||||
// COMPONENTS (if any are created)
|
||||
// ==========================================================================
|
||||
export * from './lib/hcl-studio.component';
|
||||
15
projects/hcl-studio/tsconfig.lib.json
Normal file
15
projects/hcl-studio/tsconfig.lib.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
11
projects/hcl-studio/tsconfig.lib.prod.json
Normal file
11
projects/hcl-studio/tsconfig.lib.prod.json
Normal file
@@ -0,0 +1,11 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
}
|
||||
}
|
||||
15
projects/hcl-studio/tsconfig.spec.json
Normal file
15
projects/hcl-studio/tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user