popeye-cli 1.4.7 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +264 -63
- package/dist/adapters/gemini.d.ts +1 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +9 -4
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +1 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +9 -4
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +35 -9
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +54 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts +29 -0
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +132 -7
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +8 -2
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +37 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +64 -0
- package/dist/generators/doc-parser.d.ts.map +1 -0
- package/dist/generators/doc-parser.js +407 -0
- package/dist/generators/doc-parser.js.map +1 -0
- package/dist/generators/frontend-design-analyzer.d.ts +30 -0
- package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
- package/dist/generators/frontend-design-analyzer.js +208 -0
- package/dist/generators/frontend-design-analyzer.js.map +1 -0
- package/dist/generators/shared-packages.d.ts +45 -0
- package/dist/generators/shared-packages.d.ts.map +1 -0
- package/dist/generators/shared-packages.js +456 -0
- package/dist/generators/shared-packages.js.map +1 -0
- package/dist/generators/templates/index.d.ts +8 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +8 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts +33 -0
- package/dist/generators/templates/website-components.d.ts.map +1 -0
- package/dist/generators/templates/website-components.js +303 -0
- package/dist/generators/templates/website-components.js.map +1 -0
- package/dist/generators/templates/website-config.d.ts +55 -0
- package/dist/generators/templates/website-config.d.ts.map +1 -0
- package/dist/generators/templates/website-config.js +425 -0
- package/dist/generators/templates/website-config.js.map +1 -0
- package/dist/generators/templates/website-conversion.d.ts +27 -0
- package/dist/generators/templates/website-conversion.d.ts.map +1 -0
- package/dist/generators/templates/website-conversion.js +326 -0
- package/dist/generators/templates/website-conversion.js.map +1 -0
- package/dist/generators/templates/website-landing.d.ts +24 -0
- package/dist/generators/templates/website-landing.d.ts.map +1 -0
- package/dist/generators/templates/website-landing.js +276 -0
- package/dist/generators/templates/website-landing.js.map +1 -0
- package/dist/generators/templates/website-layout.d.ts +42 -0
- package/dist/generators/templates/website-layout.d.ts.map +1 -0
- package/dist/generators/templates/website-layout.js +408 -0
- package/dist/generators/templates/website-layout.js.map +1 -0
- package/dist/generators/templates/website-pricing.d.ts +11 -0
- package/dist/generators/templates/website-pricing.d.ts.map +1 -0
- package/dist/generators/templates/website-pricing.js +313 -0
- package/dist/generators/templates/website-pricing.js.map +1 -0
- package/dist/generators/templates/website-sections.d.ts +102 -0
- package/dist/generators/templates/website-sections.d.ts.map +1 -0
- package/dist/generators/templates/website-sections.js +444 -0
- package/dist/generators/templates/website-sections.js.map +1 -0
- package/dist/generators/templates/website-seo.d.ts +76 -0
- package/dist/generators/templates/website-seo.d.ts.map +1 -0
- package/dist/generators/templates/website-seo.js +326 -0
- package/dist/generators/templates/website-seo.js.map +1 -0
- package/dist/generators/templates/website.d.ts +10 -83
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -875
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-scanner.d.ts +37 -0
- package/dist/generators/website-content-scanner.d.ts.map +1 -0
- package/dist/generators/website-content-scanner.js +165 -0
- package/dist/generators/website-content-scanner.js.map +1 -0
- package/dist/generators/website-context.d.ts +119 -0
- package/dist/generators/website-context.d.ts.map +1 -0
- package/dist/generators/website-context.js +350 -0
- package/dist/generators/website-context.js.map +1 -0
- package/dist/generators/website-debug.d.ts +68 -0
- package/dist/generators/website-debug.d.ts.map +1 -0
- package/dist/generators/website-debug.js +93 -0
- package/dist/generators/website-debug.js.map +1 -0
- package/dist/generators/website.d.ts +5 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +136 -11
- package/dist/generators/website.js.map +1 -1
- package/dist/generators/workspace-root.d.ts +27 -0
- package/dist/generators/workspace-root.d.ts.map +1 -0
- package/dist/generators/workspace-root.js +100 -0
- package/dist/generators/workspace-root.js.map +1 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +40 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +3 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +1 -0
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +263 -0
- package/dist/types/website-strategy.d.ts.map +1 -0
- package/dist/types/website-strategy.js +105 -0
- package/dist/types/website-strategy.js.map +1 -0
- package/dist/types/workflow.d.ts +21 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +8 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts +15 -0
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +52 -0
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/auto-fix-bundler.d.ts +37 -0
- package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
- package/dist/workflow/auto-fix-bundler.js +320 -0
- package/dist/workflow/auto-fix-bundler.js.map +1 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +10 -3
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +2 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +18 -0
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +4 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +37 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts +89 -0
- package/dist/workflow/overview.d.ts.map +1 -0
- package/dist/workflow/overview.js +358 -0
- package/dist/workflow/overview.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +6 -4
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +148 -6
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +79 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -0
- package/dist/workflow/website-strategy.js +310 -0
- package/dist/workflow/website-strategy.js.map +1 -0
- package/dist/workflow/website-updater.d.ts +17 -0
- package/dist/workflow/website-updater.d.ts.map +1 -0
- package/dist/workflow/website-updater.js +116 -0
- package/dist/workflow/website-updater.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +1 -1
- package/dist/workflow/workflow-logger.d.ts.map +1 -1
- package/dist/workflow/workflow-logger.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +10 -4
- package/src/adapters/grok.ts +10 -4
- package/src/adapters/openai.ts +38 -6
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +143 -7
- package/src/generators/all.ts +49 -332
- package/src/generators/doc-parser.ts +449 -0
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +8 -0
- package/src/generators/templates/website-components.ts +330 -0
- package/src/generators/templates/website-config.ts +444 -0
- package/src/generators/templates/website-conversion.ts +341 -0
- package/src/generators/templates/website-landing.ts +331 -0
- package/src/generators/templates/website-layout.ts +443 -0
- package/src/generators/templates/website-pricing.ts +330 -0
- package/src/generators/templates/website-sections.ts +541 -0
- package/src/generators/templates/website-seo.ts +370 -0
- package/src/generators/templates/website.ts +38 -905
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +493 -0
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +178 -20
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +56 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/website-strategy.ts +243 -0
- package/src/types/workflow.ts +21 -0
- package/src/upgrade/handlers.ts +65 -0
- package/src/workflow/auto-fix-bundler.ts +392 -0
- package/src/workflow/auto-fix.ts +11 -3
- package/src/workflow/consensus.ts +2 -0
- package/src/workflow/execution-mode.ts +21 -0
- package/src/workflow/index.ts +37 -0
- package/src/workflow/overview.ts +475 -0
- package/src/workflow/plan-mode.ts +193 -8
- package/src/workflow/website-strategy.ts +379 -0
- package/src/workflow/website-updater.ts +142 -0
- package/src/workflow/workflow-logger.ts +1 -0
- package/tests/adapters/persona-switching.test.ts +63 -0
- package/tests/cli/project-naming.test.ts +136 -0
- package/tests/generators/doc-parser.test.ts +121 -0
- package/tests/generators/frontend-design-analyzer.test.ts +90 -0
- package/tests/generators/quality-gate.test.ts +183 -0
- package/tests/generators/shared-packages.test.ts +83 -0
- package/tests/generators/website-components.test.ts +159 -0
- package/tests/generators/website-config.test.ts +84 -0
- package/tests/generators/website-content-scanner.test.ts +181 -0
- package/tests/generators/website-context.test.ts +331 -0
- package/tests/generators/website-debug.test.ts +77 -0
- package/tests/generators/website-landing.test.ts +188 -0
- package/tests/generators/website-pricing.test.ts +98 -0
- package/tests/generators/website-sections.test.ts +245 -0
- package/tests/generators/website-seo-quality.test.ts +246 -0
- package/tests/generators/workspace-root.test.ts +105 -0
- package/tests/upgrade/handlers.test.ts +162 -0
- package/tests/workflow/auto-fix-bundler.test.ts +242 -0
- package/tests/workflow/overview.test.ts +392 -0
- package/tests/workflow/plan-mode.test.ts +111 -1
- package/tests/workflow/website-strategy.test.ts +246 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend design language analyzer
|
|
3
|
+
* Extracts design tokens (colors, fonts, component library) from an existing
|
|
4
|
+
* frontend app to use as fallback for website generation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Analyzed design context from a frontend application
|
|
12
|
+
*/
|
|
13
|
+
export interface FrontendDesignContext {
|
|
14
|
+
colors?: Record<string, string>;
|
|
15
|
+
primaryColor?: string;
|
|
16
|
+
fonts?: { heading?: string; body?: string; mono?: string };
|
|
17
|
+
borderRadius?: string;
|
|
18
|
+
componentLibrary?: 'shadcn' | 'radix' | 'mui' | 'chakra' | 'unknown';
|
|
19
|
+
darkMode?: boolean;
|
|
20
|
+
source: 'tailwind-config' | 'css-variables' | 'defaults';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Analyze the frontend app design language
|
|
25
|
+
* Extracts colors, fonts, and component library from the project
|
|
26
|
+
*
|
|
27
|
+
* @param projectDir - Root project directory
|
|
28
|
+
* @returns Design context or null if no frontend app found
|
|
29
|
+
*/
|
|
30
|
+
export async function analyzeFrontendDesign(
|
|
31
|
+
projectDir: string
|
|
32
|
+
): Promise<FrontendDesignContext | null> {
|
|
33
|
+
const frontendDir = path.join(projectDir, 'apps', 'frontend');
|
|
34
|
+
|
|
35
|
+
// Check if frontend app exists
|
|
36
|
+
try {
|
|
37
|
+
await fs.access(frontendDir);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const context: FrontendDesignContext = {
|
|
43
|
+
source: 'defaults',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Detect component library from package.json
|
|
47
|
+
context.componentLibrary = await detectComponentLibrary(frontendDir);
|
|
48
|
+
|
|
49
|
+
// Try CSS custom properties first (shadcn/ui convention)
|
|
50
|
+
const cssResult = await extractCssVariables(frontendDir);
|
|
51
|
+
if (cssResult) {
|
|
52
|
+
if (cssResult.primaryColor) context.primaryColor = cssResult.primaryColor;
|
|
53
|
+
if (cssResult.colors) context.colors = cssResult.colors;
|
|
54
|
+
if (cssResult.borderRadius) context.borderRadius = cssResult.borderRadius;
|
|
55
|
+
context.darkMode = cssResult.darkMode;
|
|
56
|
+
context.source = 'css-variables';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Try tailwind config (regex-based since we can't import user's config)
|
|
60
|
+
const tailwindResult = await extractTailwindConfig(frontendDir);
|
|
61
|
+
if (tailwindResult) {
|
|
62
|
+
// Only override if CSS vars didn't provide the value
|
|
63
|
+
if (!context.primaryColor && tailwindResult.primaryColor) {
|
|
64
|
+
context.primaryColor = tailwindResult.primaryColor;
|
|
65
|
+
}
|
|
66
|
+
if (!context.colors && tailwindResult.colors) {
|
|
67
|
+
context.colors = tailwindResult.colors;
|
|
68
|
+
}
|
|
69
|
+
if (tailwindResult.fonts) context.fonts = tailwindResult.fonts;
|
|
70
|
+
if (tailwindResult.darkMode !== undefined && !cssResult) {
|
|
71
|
+
context.darkMode = tailwindResult.darkMode;
|
|
72
|
+
}
|
|
73
|
+
if (context.source === 'defaults') context.source = 'tailwind-config';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Return null if nothing meaningful was found
|
|
77
|
+
if (!context.primaryColor && !context.colors && !context.componentLibrary) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return context;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detect component library from package.json dependencies
|
|
86
|
+
*/
|
|
87
|
+
async function detectComponentLibrary(
|
|
88
|
+
frontendDir: string
|
|
89
|
+
): Promise<FrontendDesignContext['componentLibrary']> {
|
|
90
|
+
try {
|
|
91
|
+
const pkgContent = await fs.readFile(
|
|
92
|
+
path.join(frontendDir, 'package.json'),
|
|
93
|
+
'utf-8'
|
|
94
|
+
);
|
|
95
|
+
const pkg = JSON.parse(pkgContent);
|
|
96
|
+
const allDeps = {
|
|
97
|
+
...pkg.dependencies,
|
|
98
|
+
...pkg.devDependencies,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (allDeps['@shadcn/ui'] || allDeps['shadcn-ui']) return 'shadcn';
|
|
102
|
+
if (allDeps['@radix-ui/react-dialog'] || allDeps['@radix-ui/themes']) return 'radix';
|
|
103
|
+
if (allDeps['@mui/material']) return 'mui';
|
|
104
|
+
if (allDeps['@chakra-ui/react']) return 'chakra';
|
|
105
|
+
} catch {
|
|
106
|
+
// Package.json not readable
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract design tokens from CSS custom properties (index.css or globals.css)
|
|
114
|
+
*/
|
|
115
|
+
async function extractCssVariables(
|
|
116
|
+
frontendDir: string
|
|
117
|
+
): Promise<{
|
|
118
|
+
primaryColor?: string;
|
|
119
|
+
colors?: Record<string, string>;
|
|
120
|
+
borderRadius?: string;
|
|
121
|
+
darkMode?: boolean;
|
|
122
|
+
} | null> {
|
|
123
|
+
const cssFiles = [
|
|
124
|
+
path.join(frontendDir, 'src', 'index.css'),
|
|
125
|
+
path.join(frontendDir, 'src', 'globals.css'),
|
|
126
|
+
path.join(frontendDir, 'src', 'app', 'globals.css'),
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
let cssContent = '';
|
|
130
|
+
for (const cssFile of cssFiles) {
|
|
131
|
+
try {
|
|
132
|
+
cssContent = await fs.readFile(cssFile, 'utf-8');
|
|
133
|
+
break;
|
|
134
|
+
} catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!cssContent) return null;
|
|
140
|
+
|
|
141
|
+
const result: {
|
|
142
|
+
primaryColor?: string;
|
|
143
|
+
colors?: Record<string, string>;
|
|
144
|
+
borderRadius?: string;
|
|
145
|
+
darkMode?: boolean;
|
|
146
|
+
} = {};
|
|
147
|
+
|
|
148
|
+
// Detect shadcn/ui convention: HSL values like --primary: 222.2 47.4% 11.2%
|
|
149
|
+
const primaryHsl = cssContent.match(/--primary:\s*([\d.]+)\s+([\d.]+)%\s+([\d.]+)%/);
|
|
150
|
+
if (primaryHsl) {
|
|
151
|
+
const h = parseFloat(primaryHsl[1]);
|
|
152
|
+
const s = parseFloat(primaryHsl[2]) / 100;
|
|
153
|
+
const l = parseFloat(primaryHsl[3]) / 100;
|
|
154
|
+
result.primaryColor = hslToHex(h / 360, s, l);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Detect hex-based CSS variables
|
|
158
|
+
if (!result.primaryColor) {
|
|
159
|
+
const primaryHex = cssContent.match(/--primary(?:-color)?:\s*(#[0-9a-fA-F]{6})/);
|
|
160
|
+
if (primaryHex) result.primaryColor = primaryHex[1];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Extract border radius
|
|
164
|
+
const radiusMatch = cssContent.match(/--radius:\s*([\d.]+rem)/);
|
|
165
|
+
if (radiusMatch) result.borderRadius = radiusMatch[1];
|
|
166
|
+
|
|
167
|
+
// Detect dark mode blocks
|
|
168
|
+
result.darkMode = /\.dark\s*\{/.test(cssContent) || /@media\s*\(prefers-color-scheme:\s*dark\)/.test(cssContent);
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Extract design tokens from tailwind config (regex-based)
|
|
175
|
+
*/
|
|
176
|
+
async function extractTailwindConfig(
|
|
177
|
+
frontendDir: string
|
|
178
|
+
): Promise<{
|
|
179
|
+
primaryColor?: string;
|
|
180
|
+
colors?: Record<string, string>;
|
|
181
|
+
fonts?: { heading?: string; body?: string; mono?: string };
|
|
182
|
+
darkMode?: boolean;
|
|
183
|
+
} | null> {
|
|
184
|
+
const configFiles = [
|
|
185
|
+
path.join(frontendDir, 'tailwind.config.ts'),
|
|
186
|
+
path.join(frontendDir, 'tailwind.config.js'),
|
|
187
|
+
path.join(frontendDir, 'tailwind.config.mjs'),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
let configContent = '';
|
|
191
|
+
for (const configFile of configFiles) {
|
|
192
|
+
try {
|
|
193
|
+
configContent = await fs.readFile(configFile, 'utf-8');
|
|
194
|
+
break;
|
|
195
|
+
} catch {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!configContent) return null;
|
|
201
|
+
|
|
202
|
+
const result: {
|
|
203
|
+
primaryColor?: string;
|
|
204
|
+
colors?: Record<string, string>;
|
|
205
|
+
fonts?: { heading?: string; body?: string; mono?: string };
|
|
206
|
+
darkMode?: boolean;
|
|
207
|
+
} = {};
|
|
208
|
+
|
|
209
|
+
// Extract primary color: primary: { 500: '#...' } or primary: '#...'
|
|
210
|
+
const primary500 = configContent.match(/primary[^}]*?500:\s*['"]?(#[0-9a-fA-F]{6})/);
|
|
211
|
+
if (primary500) {
|
|
212
|
+
result.primaryColor = primary500[1];
|
|
213
|
+
} else {
|
|
214
|
+
const primaryDirect = configContent.match(/primary:\s*['"]?(#[0-9a-fA-F]{6})/);
|
|
215
|
+
if (primaryDirect) result.primaryColor = primaryDirect[1];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Extract font family
|
|
219
|
+
const sansFont = configContent.match(/sans:\s*\[['"]([^'"]+)['"]/);
|
|
220
|
+
if (sansFont) {
|
|
221
|
+
result.fonts = { body: sansFont[1] };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Detect dark mode configuration
|
|
225
|
+
result.darkMode = /darkMode:\s*['"]class['"]/.test(configContent);
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Color conversion helper ---
|
|
231
|
+
|
|
232
|
+
function hslToHex(h: number, s: number, l: number): string {
|
|
233
|
+
const hue2rgb = (p: number, q: number, t: number): number => {
|
|
234
|
+
let tn = t;
|
|
235
|
+
if (tn < 0) tn += 1;
|
|
236
|
+
if (tn > 1) tn -= 1;
|
|
237
|
+
if (tn < 1 / 6) return p + (q - p) * 6 * tn;
|
|
238
|
+
if (tn < 1 / 2) return q;
|
|
239
|
+
if (tn < 2 / 3) return p + (q - p) * (2 / 3 - tn) * 6;
|
|
240
|
+
return p;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
let r: number, g: number, b: number;
|
|
244
|
+
|
|
245
|
+
if (s === 0) {
|
|
246
|
+
r = g = b = l;
|
|
247
|
+
} else {
|
|
248
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
249
|
+
const p = 2 * l - q;
|
|
250
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
251
|
+
g = hue2rgb(p, q, h);
|
|
252
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const toHex = (n: number): string => {
|
|
256
|
+
const hex = Math.round(n * 255).toString(16);
|
|
257
|
+
return hex.length === 1 ? '0' + hex : hex;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
261
|
+
}
|