igniteui-theming 25.1.0 → 25.2.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.
Files changed (128) hide show
  1. package/dist/index.d.ts +75 -0
  2. package/dist/index.js +12 -0
  3. package/dist/json/components/bootstrap.json +1 -0
  4. package/dist/json/components/fluent.json +1 -0
  5. package/dist/json/components/indigo.json +1 -0
  6. package/dist/json/components/material.json +1 -0
  7. package/{json → dist/json}/components/themes.json +31 -1
  8. package/dist/mcp/generators/css.d.ts +7 -4
  9. package/dist/mcp/generators/css.js +129 -104
  10. package/dist/mcp/generators/sass.js +227 -254
  11. package/dist/mcp/index.js +259 -323
  12. package/dist/mcp/knowledge/color-usage.js +524 -502
  13. package/dist/mcp/knowledge/colors.js +61 -50
  14. package/dist/mcp/knowledge/component-metadata.js +697 -598
  15. package/dist/mcp/knowledge/component-themes.js +70 -57
  16. package/dist/mcp/knowledge/custom-palettes.js +4 -9
  17. package/dist/mcp/knowledge/docs/colors/guidance.js +4 -0
  18. package/dist/mcp/knowledge/docs/colors/usage.js +4 -0
  19. package/dist/mcp/knowledge/docs/layout/functions/border-radius.js +4 -0
  20. package/dist/mcp/knowledge/docs/layout/functions/pad.js +4 -0
  21. package/dist/mcp/knowledge/docs/layout/functions/sizable.js +4 -0
  22. package/dist/mcp/knowledge/docs/layout/mixins/sizable.js +4 -0
  23. package/dist/mcp/knowledge/docs/layout/mixins/sizing.js +4 -0
  24. package/dist/mcp/knowledge/docs/layout/mixins/spacing.js +4 -0
  25. package/dist/mcp/knowledge/docs/layout/overview.js +4 -0
  26. package/dist/mcp/knowledge/docs/setup/platform.js +4 -0
  27. package/dist/mcp/knowledge/elevations.d.ts +1 -1
  28. package/dist/mcp/knowledge/elevations.js +26 -12
  29. package/dist/mcp/knowledge/index.js +23 -87
  30. package/dist/mcp/knowledge/layout-docs.d.ts +1 -1
  31. package/dist/mcp/knowledge/multipliers.js +5 -0
  32. package/dist/mcp/knowledge/palettes.js +29 -17
  33. package/dist/mcp/knowledge/platforms/angular.js +98 -120
  34. package/dist/mcp/knowledge/platforms/blazor.js +39 -34
  35. package/dist/mcp/knowledge/platforms/common.js +83 -68
  36. package/dist/mcp/knowledge/platforms/index.js +265 -242
  37. package/dist/mcp/knowledge/platforms/react.js +43 -35
  38. package/dist/mcp/knowledge/platforms/webcomponents.js +266 -292
  39. package/dist/mcp/knowledge/sass-api.js +1 -0
  40. package/dist/mcp/knowledge/typography.js +13 -5
  41. package/dist/mcp/resources/index.js +1 -0
  42. package/dist/mcp/resources/presets.js +409 -508
  43. package/dist/mcp/theming/dist/json/colors/meta/multipliers.js +50 -0
  44. package/dist/mcp/theming/dist/json/colors/presets/palettes.js +85 -0
  45. package/dist/mcp/theming/dist/json/components/themes.js +5792 -0
  46. package/dist/mcp/theming/dist/json/elevations/indigo.js +29 -0
  47. package/dist/mcp/theming/dist/json/elevations/material.js +3 -0
  48. package/dist/mcp/theming/dist/json/typography/presets/typescales.js +621 -0
  49. package/dist/mcp/tools/descriptions.js +98 -154
  50. package/dist/mcp/tools/handlers/color.js +58 -56
  51. package/dist/mcp/tools/handlers/component-theme.js +163 -225
  52. package/dist/mcp/tools/handlers/component-tokens.js +159 -219
  53. package/dist/mcp/tools/handlers/custom-palette.js +138 -179
  54. package/dist/mcp/tools/handlers/elevations.js +27 -28
  55. package/dist/mcp/tools/handlers/index.js +11 -0
  56. package/dist/mcp/tools/handlers/layout.js +125 -176
  57. package/dist/mcp/tools/handlers/palette.js +105 -120
  58. package/dist/mcp/tools/handlers/platform.js +289 -311
  59. package/dist/mcp/tools/handlers/resource.js +22 -31
  60. package/dist/mcp/tools/handlers/theme.js +86 -103
  61. package/dist/mcp/tools/handlers/typography.js +29 -30
  62. package/dist/mcp/tools/index.js +13 -0
  63. package/dist/mcp/tools/schemas.js +239 -218
  64. package/dist/mcp/utils/color.js +277 -239
  65. package/dist/mcp/utils/preprocessing.js +57 -30
  66. package/dist/mcp/utils/result.js +43 -45
  67. package/dist/mcp/utils/sass.js +271 -191
  68. package/dist/mcp/utils/theming-resolve.d.ts +19 -0
  69. package/dist/mcp/utils/theming-resolve.js +57 -0
  70. package/dist/mcp/utils/types.js +96 -53
  71. package/dist/mcp/validators/custom-palette.js +218 -243
  72. package/dist/mcp/validators/index.js +3 -0
  73. package/dist/mcp/validators/palette.js +231 -229
  74. package/dist/tailwind/utilities/bootstrap.css +1 -0
  75. package/dist/tailwind/utilities/fluent.css +1 -0
  76. package/dist/tailwind/utilities/indigo.css +1 -0
  77. package/dist/tailwind/utilities/material.css +1 -0
  78. package/package.json +45 -64
  79. package/sass/json/README.md +12 -7
  80. package/sass/themes/_mixins.scss +1 -0
  81. package/sass/themes/components/button-group/_button-group-theme.scss +42 -0
  82. package/sass/themes/components/grid/_grid-theme.scss +1 -1
  83. package/sass/themes/schemas/components/dark/_button-group.scss +173 -50
  84. package/sass/themes/schemas/components/dark/_grid.scss +0 -16
  85. package/sass/themes/schemas/components/light/_button-group.scss +221 -99
  86. package/sass/themes/schemas/components/light/_grid.scss +14 -20
  87. package/LICENSE +0 -21
  88. package/README.md +0 -391
  89. package/dist/mcp/json/colors/presets/palettes.json.js +0 -13
  90. package/dist/mcp/json/components/themes.json.js +0 -143
  91. package/dist/mcp/json/elevations/indigo.json.js +0 -8
  92. package/dist/mcp/json/elevations/material.json.js +0 -8
  93. package/dist/mcp/json/typography/presets/typescales.json.js +0 -17
  94. package/dist/mcp/knowledge/docs/colors/guidance.md.js +0 -4
  95. package/dist/mcp/knowledge/docs/colors/usage.md.js +0 -4
  96. package/dist/mcp/knowledge/docs/layout/functions/border-radius.md.js +0 -4
  97. package/dist/mcp/knowledge/docs/layout/functions/pad.md.js +0 -4
  98. package/dist/mcp/knowledge/docs/layout/functions/sizable.md.js +0 -4
  99. package/dist/mcp/knowledge/docs/layout/mixins/sizable.md.js +0 -4
  100. package/dist/mcp/knowledge/docs/layout/mixins/sizing.md.js +0 -4
  101. package/dist/mcp/knowledge/docs/layout/mixins/spacing.md.js +0 -4
  102. package/dist/mcp/knowledge/docs/layout/overview.md.js +0 -4
  103. package/dist/mcp/knowledge/docs/setup/platform.md.js +0 -4
  104. package/dist/mcp/vite-env.d.ts +0 -18
  105. package/index.js +0 -5
  106. package/json/components/bootstrap.json +0 -1
  107. package/json/components/fluent.json +0 -1
  108. package/json/components/indigo.json +0 -1
  109. package/json/components/material.json +0 -1
  110. package/tailwind/utilities/bootstrap.css +0 -1
  111. package/tailwind/utilities/fluent.css +0 -1
  112. package/tailwind/utilities/indigo.css +0 -1
  113. package/tailwind/utilities/material.css +0 -1
  114. /package/{json → dist/json}/colors/meta/multipliers.json +0 -0
  115. /package/{json → dist/json}/colors/meta/palette.json +0 -0
  116. /package/{json → dist/json}/colors/presets/palettes.json +0 -0
  117. /package/{json → dist/json}/elevations/indigo.json +0 -0
  118. /package/{json → dist/json}/elevations/material.json +0 -0
  119. /package/{json → dist/json}/typography/presets/typescales.json +0 -0
  120. /package/{tailwind → dist/tailwind}/themes/base.css +0 -0
  121. /package/{tailwind → dist/tailwind}/themes/dark/bootstrap.css +0 -0
  122. /package/{tailwind → dist/tailwind}/themes/dark/fluent.css +0 -0
  123. /package/{tailwind → dist/tailwind}/themes/dark/indigo.css +0 -0
  124. /package/{tailwind → dist/tailwind}/themes/dark/material.css +0 -0
  125. /package/{tailwind → dist/tailwind}/themes/light/bootstrap.css +0 -0
  126. /package/{tailwind → dist/tailwind}/themes/light/fluent.css +0 -0
  127. /package/{tailwind → dist/tailwind}/themes/light/indigo.css +0 -0
  128. /package/{tailwind → dist/tailwind}/themes/light/material.css +0 -0
@@ -1,24 +1,63 @@
1
- import * as path from "node:path";
2
- import { fileURLToPath } from "node:url";
1
+ import { themingImporter } from "./theming-resolve.js";
3
2
  import * as sass from "sass-embedded";
4
- const __filename$1 = fileURLToPath(import.meta.url);
5
- const __dirname$1 = path.dirname(__filename$1);
6
- const PACKAGE_ROOT = path.resolve(__dirname$1, "..", "..", "..");
7
- const LUMINANCE_THRESHOLD = 0.5;
8
- const DEFAULT_MINIMUM_CONTRAST_RATIO = 3;
9
- const SUGGESTED_COLORS = {
10
- light: {
11
- surface: ["white", "#ffffff", "#f8f9fa", "#fafafa", "#f5f5f5"],
12
- gray: ["black", "#000000", "#333333", "#212121", "#424242"]
13
- },
14
- dark: {
15
- surface: ["#222222", "#1a1a1a", "#121212", "#181818", "#2d2d2d"],
16
- gray: ["white", "#ffffff", "#e5e5e5", "#f5f5f5", "#eeeeee"]
17
- }
3
+ //#region src/utils/color.ts
4
+ /**
5
+ * Color analysis utilities using Sass-embedded.
6
+ * Calls the actual Sass luminance() and contrast() functions for accurate validation.
7
+ */
8
+ /**
9
+ * Luminance threshold for determining light vs dark colors.
10
+ * Colors with luminance > 0.5 are considered "light".
11
+ * Colors with luminance <= 0.5 are considered "dark".
12
+ */
13
+ var LUMINANCE_THRESHOLD = .5;
14
+ /**
15
+ * Suggested colors for different variants.
16
+ */
17
+ var SUGGESTED_COLORS = {
18
+ light: {
19
+ surface: [
20
+ "white",
21
+ "#ffffff",
22
+ "#f8f9fa",
23
+ "#fafafa",
24
+ "#f5f5f5"
25
+ ],
26
+ gray: [
27
+ "black",
28
+ "#000000",
29
+ "#333333",
30
+ "#212121",
31
+ "#424242"
32
+ ]
33
+ },
34
+ dark: {
35
+ surface: [
36
+ "#222222",
37
+ "#1a1a1a",
38
+ "#121212",
39
+ "#181818",
40
+ "#2d2d2d"
41
+ ],
42
+ gray: [
43
+ "white",
44
+ "#ffffff",
45
+ "#e5e5e5",
46
+ "#f5f5f5",
47
+ "#eeeeee"
48
+ ]
49
+ }
18
50
  };
51
+ /**
52
+ * Analyze a single color using Sass luminance() function.
53
+ *
54
+ * @param color - CSS color value (hex, rgb, hsl, or named color)
55
+ * @returns Color analysis with luminance and isLight flag
56
+ * @throws Error if Sass compilation fails or color is invalid
57
+ */
19
58
  async function analyzeColor(color) {
20
- const sassCode = `
21
- @use 'sass/color' as color;
59
+ const sassCode = `
60
+ @use 'igniteui-theming/sass/color' as color;
22
61
 
23
62
  $lum: color.luminance(${color});
24
63
 
@@ -26,237 +65,236 @@ $lum: color.luminance(${color});
26
65
  --luminance: #{$lum};
27
66
  }
28
67
  `;
29
- try {
30
- const result = await sass.compileStringAsync(sassCode, {
31
- loadPaths: [PACKAGE_ROOT]
32
- });
33
- const luminanceMatch = result.css.match(/--luminance:\s*([\d.]+)/);
34
- if (!luminanceMatch) {
35
- throw new Error(
36
- `Could not parse luminance from Sass output for color: ${color}`
37
- );
38
- }
39
- const luminance = Number.parseFloat(luminanceMatch[1]);
40
- return {
41
- color,
42
- luminance,
43
- isLight: luminance > LUMINANCE_THRESHOLD
44
- };
45
- } catch (error) {
46
- const message = error instanceof Error ? error.message : String(error);
47
- throw new Error(`Failed to analyze color "${color}": ${message}`);
48
- }
68
+ try {
69
+ const luminanceMatch = (await sass.compileStringAsync(sassCode, { importers: [themingImporter] })).css.match(/--luminance:\s*([\d.]+)/);
70
+ if (!luminanceMatch) throw new Error(`Could not parse luminance from Sass output for color: ${color}`);
71
+ const luminance = Number.parseFloat(luminanceMatch[1]);
72
+ return {
73
+ color,
74
+ luminance,
75
+ isLight: luminance > LUMINANCE_THRESHOLD
76
+ };
77
+ } catch (error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ throw new Error(`Failed to analyze color "${color}": ${message}`);
80
+ }
49
81
  }
82
+ /**
83
+ * Analyze surface and gray colors together in a single Sass compilation.
84
+ * This is more efficient than calling analyzeColor twice.
85
+ *
86
+ * @param params - Object containing surface and/or gray colors
87
+ * @returns Combined analysis results
88
+ */
50
89
  async function analyzeSurfaceGrayColors(params) {
51
- const { surface, gray } = params;
52
- if (!surface && !gray) {
53
- return {};
54
- }
55
- const sassLines = [`@use 'sass/color' as color;`, ""];
56
- if (surface) {
57
- sassLines.push(`$surface-lum: color.luminance(${surface});`);
58
- }
59
- if (gray) {
60
- sassLines.push(`$gray-lum: color.luminance(${gray});`);
61
- }
62
- if (surface && gray) {
63
- sassLines.push(`$contrast: color.contrast(${surface}, ${gray});`);
64
- }
65
- sassLines.push("", ":root {");
66
- if (surface) {
67
- sassLines.push(" --surface-luminance: #{$surface-lum};");
68
- }
69
- if (gray) {
70
- sassLines.push(" --gray-luminance: #{$gray-lum};");
71
- }
72
- if (surface && gray) {
73
- sassLines.push(" --contrast-ratio: #{$contrast};");
74
- }
75
- sassLines.push("}");
76
- const sassCode = sassLines.join("\n");
77
- try {
78
- const result = await sass.compileStringAsync(sassCode, {
79
- loadPaths: [PACKAGE_ROOT]
80
- });
81
- const analysis = {};
82
- if (surface) {
83
- const surfaceMatch = result.css.match(/--surface-luminance:\s*([\d.]+)/);
84
- if (surfaceMatch) {
85
- const luminance = Number.parseFloat(surfaceMatch[1]);
86
- analysis.surface = {
87
- color: surface,
88
- luminance,
89
- isLight: luminance > LUMINANCE_THRESHOLD
90
- };
91
- }
92
- }
93
- if (gray) {
94
- const grayMatch = result.css.match(/--gray-luminance:\s*([\d.]+)/);
95
- if (grayMatch) {
96
- const luminance = Number.parseFloat(grayMatch[1]);
97
- analysis.gray = {
98
- color: gray,
99
- luminance,
100
- isLight: luminance > LUMINANCE_THRESHOLD
101
- };
102
- }
103
- }
104
- if (surface && gray) {
105
- const contrastMatch = result.css.match(/--contrast-ratio:\s*([\d.]+)/);
106
- if (contrastMatch) {
107
- analysis.contrastRatio = Number.parseFloat(contrastMatch[1]);
108
- }
109
- }
110
- return analysis;
111
- } catch (error) {
112
- const message = error instanceof Error ? error.message : String(error);
113
- throw new Error(`Failed to analyze surface/gray colors: ${message}`);
114
- }
90
+ const { surface, gray } = params;
91
+ if (!surface && !gray) return {};
92
+ const sassLines = [`@use 'igniteui-theming/sass/color' as color;`, ""];
93
+ if (surface) sassLines.push(`$surface-lum: color.luminance(${surface});`);
94
+ if (gray) sassLines.push(`$gray-lum: color.luminance(${gray});`);
95
+ if (surface && gray) sassLines.push(`$contrast: color.contrast(${surface}, ${gray});`);
96
+ sassLines.push("", ":root {");
97
+ if (surface) sassLines.push(" --surface-luminance: #{$surface-lum};");
98
+ if (gray) sassLines.push(" --gray-luminance: #{$gray-lum};");
99
+ if (surface && gray) sassLines.push(" --contrast-ratio: #{$contrast};");
100
+ sassLines.push("}");
101
+ const sassCode = sassLines.join("\n");
102
+ try {
103
+ const result = await sass.compileStringAsync(sassCode, { importers: [themingImporter] });
104
+ const analysis = {};
105
+ if (surface) {
106
+ const surfaceMatch = result.css.match(/--surface-luminance:\s*([\d.]+)/);
107
+ if (surfaceMatch) {
108
+ const luminance = Number.parseFloat(surfaceMatch[1]);
109
+ analysis.surface = {
110
+ color: surface,
111
+ luminance,
112
+ isLight: luminance > LUMINANCE_THRESHOLD
113
+ };
114
+ }
115
+ }
116
+ if (gray) {
117
+ const grayMatch = result.css.match(/--gray-luminance:\s*([\d.]+)/);
118
+ if (grayMatch) {
119
+ const luminance = Number.parseFloat(grayMatch[1]);
120
+ analysis.gray = {
121
+ color: gray,
122
+ luminance,
123
+ isLight: luminance > LUMINANCE_THRESHOLD
124
+ };
125
+ }
126
+ }
127
+ if (surface && gray) {
128
+ const contrastMatch = result.css.match(/--contrast-ratio:\s*([\d.]+)/);
129
+ if (contrastMatch) analysis.contrastRatio = Number.parseFloat(contrastMatch[1]);
130
+ }
131
+ return analysis;
132
+ } catch (error) {
133
+ const message = error instanceof Error ? error.message : String(error);
134
+ throw new Error(`Failed to analyze surface/gray colors: ${message}`);
135
+ }
115
136
  }
137
+ /**
138
+ * Check if a color is valid by attempting to analyze it.
139
+ *
140
+ * @param color - CSS color value to validate
141
+ * @returns true if the color is valid, false otherwise
142
+ */
116
143
  async function isValidColor(color) {
117
- try {
118
- await analyzeColor(color);
119
- return true;
120
- } catch {
121
- return false;
122
- }
144
+ try {
145
+ await analyzeColor(color);
146
+ return true;
147
+ } catch {
148
+ return false;
149
+ }
123
150
  }
151
+ /**
152
+ * Validate multiple colors in a single Sass compilation for efficiency.
153
+ * This is much faster than calling isValidColor() for each color individually.
154
+ *
155
+ * @param colors - Map of key names to color values
156
+ * @returns Map of key names to validation results (true = valid, false = invalid)
157
+ */
124
158
  async function validateColorsInBatch(colors) {
125
- const entries = Object.entries(colors);
126
- if (entries.length === 0) {
127
- return {};
128
- }
129
- const sassLines = [`@use 'sass:color';`, `@use 'sass:meta';`, ""];
130
- for (const [key, colorValue] of entries) {
131
- const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
132
- sassLines.push(
133
- `$${safeKey}-valid: meta.type-of(${colorValue}) == 'color';`
134
- );
135
- }
136
- sassLines.push("", ":root {");
137
- for (const [key] of entries) {
138
- const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
139
- sassLines.push(` --${safeKey}-valid: #{$${safeKey}-valid};`);
140
- }
141
- sassLines.push("}");
142
- const sassCode = sassLines.join("\n");
143
- try {
144
- const result = await sass.compileStringAsync(sassCode, {
145
- loadPaths: [PACKAGE_ROOT]
146
- });
147
- const validationResults = {};
148
- for (const [key] of entries) {
149
- const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
150
- const validMatch = result.css.match(
151
- new RegExp(`--${safeKey}-valid:\\s*(true|false)`)
152
- );
153
- validationResults[key] = validMatch ? validMatch[1] === "true" : false;
154
- }
155
- return validationResults;
156
- } catch {
157
- const validationResults = {};
158
- for (const [key, colorValue] of entries) {
159
- validationResults[key] = await isValidColor(colorValue);
160
- }
161
- return validationResults;
162
- }
159
+ const entries = Object.entries(colors);
160
+ if (entries.length === 0) return {};
161
+ const sassLines = [
162
+ `@use 'sass:color';`,
163
+ `@use 'sass:meta';`,
164
+ ""
165
+ ];
166
+ for (const [key, colorValue] of entries) {
167
+ const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
168
+ sassLines.push(`$${safeKey}-valid: meta.type-of(${colorValue}) == 'color';`);
169
+ }
170
+ sassLines.push("", ":root {");
171
+ for (const [key] of entries) {
172
+ const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
173
+ sassLines.push(` --${safeKey}-valid: #{$${safeKey}-valid};`);
174
+ }
175
+ sassLines.push("}");
176
+ const sassCode = sassLines.join("\n");
177
+ try {
178
+ const result = await sass.compileStringAsync(sassCode, { importers: [themingImporter] });
179
+ const validationResults = {};
180
+ for (const [key] of entries) {
181
+ const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
182
+ const validMatch = result.css.match(new RegExp(`--${safeKey}-valid:\\s*(true|false)`));
183
+ validationResults[key] = validMatch ? validMatch[1] === "true" : false;
184
+ }
185
+ return validationResults;
186
+ } catch {
187
+ const validationResults = {};
188
+ for (const [key, colorValue] of entries) validationResults[key] = await isValidColor(colorValue);
189
+ return validationResults;
190
+ }
163
191
  }
164
- const DEFAULT_HUE_TOLERANCE = 30;
165
- function huesAreClose(hue1, hue2, tolerance = DEFAULT_HUE_TOLERANCE) {
166
- const diff = Math.abs(hue1 - hue2);
167
- const circularDiff = Math.min(diff, 360 - diff);
168
- return circularDiff <= tolerance;
192
+ /**
193
+ * Check if two hue values are within tolerance of each other,
194
+ * accounting for the circular nature of hue ( = 360°).
195
+ *
196
+ * @param hue1 - First hue value (0-360)
197
+ * @param hue2 - Second hue value (0-360)
198
+ * @param tolerance - Maximum allowed difference in degrees (default: 30)
199
+ * @returns true if hues are within tolerance
200
+ */
201
+ function huesAreClose(hue1, hue2, tolerance = 30) {
202
+ const diff = Math.abs(hue1 - hue2);
203
+ return Math.min(diff, 360 - diff) <= tolerance;
169
204
  }
205
+ /**
206
+ * Analyze multiple colors in a single Sass compilation for efficiency.
207
+ * Returns luminance and hue for each color.
208
+ *
209
+ * @param colors - Map of key names to color values
210
+ * @returns Map of key names to analysis results
211
+ */
170
212
  async function analyzeColorsWithHue(colors) {
171
- const entries = Object.entries(colors);
172
- if (entries.length === 0) {
173
- return {};
174
- }
175
- const sassLines = [`@use 'sass/color' as igColor;`, `@use 'sass:color';`, ""];
176
- for (const [key, color] of entries) {
177
- const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
178
- sassLines.push(`$${safeKey}-lum: igColor.luminance(${color});`);
179
- sassLines.push(
180
- `$${safeKey}-hue: color.channel(${color}, "hue", $space: hsl);`
181
- );
182
- }
183
- sassLines.push("", ":root {");
184
- for (const [key] of entries) {
185
- const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
186
- sassLines.push(` --${safeKey}-luminance: #{$${safeKey}-lum};`);
187
- sassLines.push(` --${safeKey}-hue: #{$${safeKey}-hue};`);
188
- }
189
- sassLines.push("}");
190
- const sassCode = sassLines.join("\n");
191
- try {
192
- const result = await sass.compileStringAsync(sassCode, {
193
- loadPaths: [PACKAGE_ROOT]
194
- });
195
- const analysis = {};
196
- for (const [key] of entries) {
197
- const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
198
- const lumMatch = result.css.match(
199
- new RegExp(`--${safeKey}-luminance:\\s*([\\d.]+)`)
200
- );
201
- const hueMatch = result.css.match(
202
- new RegExp(`--${safeKey}-hue:\\s*([\\d.]+)`)
203
- );
204
- if (lumMatch && hueMatch) {
205
- analysis[key] = {
206
- luminance: Number.parseFloat(lumMatch[1]),
207
- hue: Number.parseFloat(hueMatch[1])
208
- };
209
- }
210
- }
211
- return analysis;
212
- } catch (error) {
213
- const message = error instanceof Error ? error.message : String(error);
214
- throw new Error(`Failed to analyze colors: ${message}`);
215
- }
213
+ const entries = Object.entries(colors);
214
+ if (entries.length === 0) return {};
215
+ const sassLines = [
216
+ `@use 'igniteui-theming/sass/color' as igColor;`,
217
+ `@use 'sass:color';`,
218
+ ""
219
+ ];
220
+ for (const [key, color] of entries) {
221
+ const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
222
+ sassLines.push(`$${safeKey}-lum: igColor.luminance(${color});`);
223
+ sassLines.push(`$${safeKey}-hue: color.channel(${color}, "hue", $space: hsl);`);
224
+ }
225
+ sassLines.push("", ":root {");
226
+ for (const [key] of entries) {
227
+ const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
228
+ sassLines.push(` --${safeKey}-luminance: #{$${safeKey}-lum};`);
229
+ sassLines.push(` --${safeKey}-hue: #{$${safeKey}-hue};`);
230
+ }
231
+ sassLines.push("}");
232
+ const sassCode = sassLines.join("\n");
233
+ try {
234
+ const result = await sass.compileStringAsync(sassCode, { importers: [themingImporter] });
235
+ const analysis = {};
236
+ for (const [key] of entries) {
237
+ const safeKey = key.replace(/[^a-zA-Z0-9]/g, "-");
238
+ const lumMatch = result.css.match(new RegExp(`--${safeKey}-luminance:\\s*([\\d.]+)`));
239
+ const hueMatch = result.css.match(new RegExp(`--${safeKey}-hue:\\s*([\\d.]+)`));
240
+ if (lumMatch && hueMatch) analysis[key] = {
241
+ luminance: Number.parseFloat(lumMatch[1]),
242
+ hue: Number.parseFloat(hueMatch[1])
243
+ };
244
+ }
245
+ return analysis;
246
+ } catch (error) {
247
+ const message = error instanceof Error ? error.message : String(error);
248
+ throw new Error(`Failed to analyze colors: ${message}`);
249
+ }
216
250
  }
217
- const PALETTE_LUMINANCE_THRESHOLDS = {
218
- /** Below this luminance, lighter shades (50-200) will lack contrast range */
219
- TOO_DARK: 0.05,
220
- /** Above this luminance, darker shades (600-900) will appear washed out */
221
- TOO_LIGHT: 0.45
251
+ /**
252
+ * Luminance thresholds for palette shade generation suitability.
253
+ * Colors outside this range may produce poor automatic shade generation results.
254
+ *
255
+ * Note: This is different from LUMINANCE_THRESHOLD (0.5) which determines
256
+ * if a color is "light" or "dark" for variant matching. These thresholds
257
+ * determine if a color can produce a good range of shades when used with
258
+ * the palette() function.
259
+ *
260
+ * Based on color theory research:
261
+ * - Optimal base color tone: 35-65 L* (CIELAB)
262
+ * - Too light (L* > 70-75): darker shades compress together
263
+ * - Too dark (L* < 25-30): lighter shades compress together
264
+ */
265
+ var PALETTE_LUMINANCE_THRESHOLDS = {
266
+ TOO_DARK: .05,
267
+ TOO_LIGHT: .45
222
268
  };
269
+ /**
270
+ * Analyze whether a color is suitable for automatic shade generation.
271
+ * Colors with extreme luminance (very light or very dark) may produce
272
+ * poor results when using the palette() function's automatic shade generation.
273
+ *
274
+ * @param color - CSS color value (hex, rgb, hsl, or named color)
275
+ * @returns Analysis result indicating suitability and any issues
276
+ */
223
277
  async function analyzeColorForPalette(color) {
224
- const analysis = await analyzeColor(color);
225
- if (analysis.luminance > PALETTE_LUMINANCE_THRESHOLDS.TOO_LIGHT) {
226
- return {
227
- color,
228
- luminance: analysis.luminance,
229
- suitable: false,
230
- issue: "too-light",
231
- description: `Luminance ${analysis.luminance.toFixed(2)} exceeds ${PALETTE_LUMINANCE_THRESHOLDS.TOO_LIGHT} - darker shades (600-900) will appear washed out`
232
- };
233
- }
234
- if (analysis.luminance < PALETTE_LUMINANCE_THRESHOLDS.TOO_DARK) {
235
- return {
236
- color,
237
- luminance: analysis.luminance,
238
- suitable: false,
239
- issue: "too-dark",
240
- description: `Luminance ${analysis.luminance.toFixed(2)} is below ${PALETTE_LUMINANCE_THRESHOLDS.TOO_DARK} - lighter shades (50-200) will lack contrast range`
241
- };
242
- }
243
- return {
244
- color,
245
- luminance: analysis.luminance,
246
- suitable: true
247
- };
278
+ const analysis = await analyzeColor(color);
279
+ if (analysis.luminance > PALETTE_LUMINANCE_THRESHOLDS.TOO_LIGHT) return {
280
+ color,
281
+ luminance: analysis.luminance,
282
+ suitable: false,
283
+ issue: "too-light",
284
+ description: `Luminance ${analysis.luminance.toFixed(2)} exceeds ${PALETTE_LUMINANCE_THRESHOLDS.TOO_LIGHT} - darker shades (600-900) will appear washed out`
285
+ };
286
+ if (analysis.luminance < PALETTE_LUMINANCE_THRESHOLDS.TOO_DARK) return {
287
+ color,
288
+ luminance: analysis.luminance,
289
+ suitable: false,
290
+ issue: "too-dark",
291
+ description: `Luminance ${analysis.luminance.toFixed(2)} is below ${PALETTE_LUMINANCE_THRESHOLDS.TOO_DARK} - lighter shades (50-200) will lack contrast range`
292
+ };
293
+ return {
294
+ color,
295
+ luminance: analysis.luminance,
296
+ suitable: true
297
+ };
248
298
  }
249
- export {
250
- DEFAULT_HUE_TOLERANCE,
251
- DEFAULT_MINIMUM_CONTRAST_RATIO,
252
- LUMINANCE_THRESHOLD,
253
- PALETTE_LUMINANCE_THRESHOLDS,
254
- SUGGESTED_COLORS,
255
- analyzeColor,
256
- analyzeColorForPalette,
257
- analyzeColorsWithHue,
258
- analyzeSurfaceGrayColors,
259
- huesAreClose,
260
- isValidColor,
261
- validateColorsInBatch
262
- };
299
+ //#endregion
300
+ export { LUMINANCE_THRESHOLD, SUGGESTED_COLORS, analyzeColorForPalette, analyzeColorsWithHue, analyzeSurfaceGrayColors, huesAreClose, validateColorsInBatch };
@@ -1,34 +1,61 @@
1
+ //#region src/utils/preprocessing.ts
2
+ /**
3
+ * Recursively parse JSON strings in a value.
4
+ *
5
+ * This function handles cases where nested objects are passed as JSON strings
6
+ * (e.g., from MCP Inspector text fields or MCP clients that send nested objects as strings).
7
+ * It only attempts to parse strings that look like JSON objects or arrays (starting with `{` or `[`).
8
+ *
9
+ * @param value - The value to process
10
+ * @returns The value with any JSON strings parsed into objects/arrays
11
+ *
12
+ * @example
13
+ * // String that looks like JSON is parsed
14
+ * deepParseJsonStrings('{"mode": "shades"}') // => { mode: 'shades' }
15
+ *
16
+ * // Regular strings are left as-is
17
+ * deepParseJsonStrings('hello') // => 'hello'
18
+ *
19
+ * // Nested objects are processed recursively
20
+ * deepParseJsonStrings({ primary: '{"mode": "shades"}' })
21
+ * // => { primary: { mode: 'shades' } }
22
+ */
1
23
  function deepParseJsonStrings(value) {
2
- if (typeof value === "string") {
3
- const trimmed = value.trim();
4
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
5
- try {
6
- const parsed = JSON.parse(trimmed);
7
- return deepParseJsonStrings(parsed);
8
- } catch {
9
- return value;
10
- }
11
- }
12
- return value;
13
- }
14
- if (Array.isArray(value)) {
15
- return value.map(deepParseJsonStrings);
16
- }
17
- if (value !== null && typeof value === "object") {
18
- return Object.fromEntries(
19
- Object.entries(value).map(([k, v]) => [k, deepParseJsonStrings(v)])
20
- );
21
- }
22
- return value;
24
+ if (typeof value === "string") {
25
+ const trimmed = value.trim();
26
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) try {
27
+ return deepParseJsonStrings(JSON.parse(trimmed));
28
+ } catch {
29
+ return value;
30
+ }
31
+ return value;
32
+ }
33
+ if (Array.isArray(value)) return value.map(deepParseJsonStrings);
34
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deepParseJsonStrings(v)]));
35
+ return value;
23
36
  }
37
+ /**
38
+ * Create a tool handler with automatic JSON string preprocessing.
39
+ *
40
+ * This wrapper ensures that nested objects passed as JSON strings (common when
41
+ * using MCP Inspector) are properly parsed before schema validation.
42
+ *
43
+ * @param schema - The Zod schema for validating the tool's parameters
44
+ * @param handler - The tool handler function
45
+ * @returns A wrapped handler that preprocesses inputs before validation
46
+ *
47
+ * @example
48
+ * server.registerTool(
49
+ * 'create_custom_palette',
50
+ * { ... },
51
+ * withPreprocessing(createCustomPaletteSchema, handleCreateCustomPalette)
52
+ * );
53
+ */
24
54
  function withPreprocessing(schema, handler) {
25
- return async (rawParams) => {
26
- const preprocessed = deepParseJsonStrings(rawParams);
27
- const validated = schema.parse(preprocessed);
28
- return handler(validated);
29
- };
55
+ return async (rawParams) => {
56
+ const preprocessed = deepParseJsonStrings(rawParams);
57
+ return handler(schema.parse(preprocessed));
58
+ };
30
59
  }
31
- export {
32
- deepParseJsonStrings,
33
- withPreprocessing
34
- };
60
+ //#endregion
61
+ export { withPreprocessing };