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:
skyai_dev
2025-09-05 05:37:37 +10:00
parent 876eb301a0
commit 5346d6d0c9
5476 changed files with 350855 additions and 10 deletions

View 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.

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/hcl-studio",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View 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
}

View 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);
}
}

View 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]
};
}
}

View 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();
});
});

View 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 {
}

View 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();
});
});

View 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
});
}
}

View 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';

View 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);
}
}

View 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
};
};

View 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;
}
}
}

View 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';

View 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"
]
}

View 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"
}
}

View 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"
]
}