@bytechain.cn/colamd-themes 0.5.0 → 0.5.2

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 (71) hide show
  1. package/README.md +27 -13
  2. package/cordis.patch.yml +9 -0
  3. package/dist/cli.js +3 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/dsh-plugin.d.ts +44 -0
  6. package/dist/dsh-plugin.js +72 -0
  7. package/dist/dsh-plugin.js.map +1 -0
  8. package/dist/generator.d.ts +1 -1
  9. package/dist/generator.js +2 -2
  10. package/dist/generator.js.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +2 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/models.d.ts +1 -1
  15. package/dist/models.js +1 -1
  16. package/dist/models.js.map +1 -1
  17. package/dist/renderer.d.ts +6 -5
  18. package/dist/renderer.js +19 -38
  19. package/dist/renderer.js.map +1 -1
  20. package/dist/theme-store.js +4 -4
  21. package/dist/theme-store.js.map +1 -1
  22. package/dist/validators.d.ts +2 -2
  23. package/dist/validators.js +4 -4
  24. package/dist/validators.js.map +1 -1
  25. package/package.json +22 -2
  26. package/skills/build-skills.sh +5 -5
  27. package/skills/colamd-themes/SKILL.md +5 -5
  28. package/.claude/skills/colamd-themes-preview.skill.md +0 -333
  29. package/.claude/skills/colamd-themes.skill.md +0 -10
  30. package/.dsh/skills/colamd-themes/SKILL.md +0 -14
  31. package/.github/supported-topics.yml +0 -17
  32. package/.hermes/skills/tools/colamd-themes/SKILL.md +0 -14
  33. package/.openclaw/skills/colamd-themes/SKILL.md +0 -21
  34. package/.opencode/skills/colamd-themes/SKILL.md +0 -17
  35. package/.trae/rules/colamd-themes.md +0 -5
  36. package/CHANGELOG.md +0 -318
  37. package/DSH_SUPPORT_SUMMARY.md +0 -87
  38. package/README_CN.md +0 -412
  39. package/RELEASENOTE.md +0 -541
  40. package/bytechain.cn-colamd-themes-0.5.0.tgz +0 -0
  41. package/mcp-server/dist/index.d.ts +0 -13
  42. package/mcp-server/dist/index.d.ts.map +0 -1
  43. package/mcp-server/dist/index.js.map +0 -1
  44. package/mcp-server/package-lock.json +0 -1759
  45. package/mcp-server/package.json +0 -24
  46. package/mcp-server/src/index.ts +0 -0
  47. package/mcp-server/tsconfig.json +0 -19
  48. package/src/cli.ts +0 -474
  49. package/src/color-systems.ts +0 -289
  50. package/src/color-utils.ts +0 -137
  51. package/src/contrast.ts +0 -66
  52. package/src/docx-style-paradigm.ts +0 -811
  53. package/src/export-cli.ts +0 -356
  54. package/src/extractors/base.ts +0 -8
  55. package/src/extractors/docx-extractor.ts +0 -417
  56. package/src/extractors/pdf-extractor.ts +0 -533
  57. package/src/extractors/url-extractor.ts +0 -846
  58. package/src/generator.ts +0 -197
  59. package/src/index.ts +0 -29
  60. package/src/models.ts +0 -212
  61. package/src/renderer.ts +0 -332
  62. package/src/templates/template.css +0 -220
  63. package/src/templates/theme-paradigm.md +0 -530
  64. package/src/templates/theme-template.hbs +0 -256
  65. package/src/theme-store.ts +0 -185
  66. package/src/types/pdf-parse.d.ts +0 -12
  67. package/src/utils/cli-utils.ts +0 -84
  68. package/src/utils/document-paradigm.ts +0 -118
  69. package/src/validators.ts +0 -298
  70. package/themes-preview/demo-content.md +0 -187
  71. package/tsconfig.json +0 -18
@@ -1,289 +0,0 @@
1
- /**
2
- * Local color system library — predefined seed palettes for popular color schemes.
3
- * Each scheme provides a complete SeedPalette (5 seed colors + variants).
4
- */
5
-
6
- import type { SeedPalette } from "./validators.js";
7
- import { hslHue, hslSaturation, hslLightness } from "./contrast.js";
8
-
9
- export interface ColorSystem {
10
- id: string;
11
- name: string;
12
- nameCN: string;
13
- description: string;
14
- mode: "light" | "dark";
15
- palette: SeedPalette;
16
- }
17
-
18
- // ═══════════════════════════════════════════════════════════
19
- // 莫兰迪 (Morandi) — 低饱和度灰调
20
- // ═══════════════════════════════════════════════════════════
21
-
22
- const morandiPink: ColorSystem = {
23
- id: "morandi-pink",
24
- name: "Morandi Pink",
25
- nameCN: "莫兰迪 灰粉",
26
- description: "低饱和度灰粉基调,柔和慵懒",
27
- mode: "light",
28
- palette: {
29
- accent: "#b08a82", accentLight: "#e8ddd8", accentDark: "#8b6b63",
30
- surface: "#f5f2f0", panel: "#ece6e2", panelAlt: "#e2dad5",
31
- ink: "#3a3533", inkMuted: "#8a807c", inkDim: "#b0a8a4",
32
- border: "#d8d2ce", borderStrong: "#c0b8b4",
33
- },
34
- };
35
-
36
- const morandiGreen: ColorSystem = {
37
- id: "morandi-green",
38
- name: "Morandi Green",
39
- nameCN: "莫兰迪 灰绿",
40
- description: "低饱和度灰绿调,沉静淡雅",
41
- mode: "light",
42
- palette: {
43
- accent: "#8a9b7a", accentLight: "#e2e8dc", accentDark: "#6b7a5e",
44
- surface: "#f4f5f2", panel: "#e8ebe4", panelAlt: "#dce0d5",
45
- ink: "#3a3d35", inkMuted: "#8a8d82", inkDim: "#b0b4a8",
46
- border: "#d4d8ce", borderStrong: "#bcc0b4",
47
- },
48
- };
49
-
50
- const morandiBlue: ColorSystem = {
51
- id: "morandi-blue",
52
- name: "Morandi Blue",
53
- nameCN: "莫兰迪 灰蓝",
54
- description: "低饱和度灰蓝调,冷静克制",
55
- mode: "light",
56
- palette: {
57
- accent: "#7a8fa0", accentLight: "#dce4e8", accentDark: "#5e7080",
58
- surface: "#f2f4f6", panel: "#e6eaee", panelAlt: "#dae0e6",
59
- ink: "#353a3f", inkMuted: "#828a92", inkDim: "#a8b0b8",
60
- border: "#ced4dc", borderStrong: "#b4bcc4",
61
- },
62
- };
63
-
64
- // ═══════════════════════════════════════════════════════════
65
- // 马卡龙 (Macaron) — 粉彩糖果色
66
- // ═══════════════════════════════════════════════════════════
67
-
68
- const macaronStrawberry: ColorSystem = {
69
- id: "macaron-strawberry",
70
- name: "Macaron Strawberry",
71
- nameCN: "马卡龙 草莓",
72
- description: "柔粉奶油色,甜美活泼",
73
- mode: "light",
74
- palette: {
75
- accent: "#e893a6", accentLight: "#fde8ec", accentDark: "#c47082",
76
- surface: "#fefafb", panel: "#faf0f2", panelAlt: "#f4e4e8",
77
- ink: "#4a3035", inkMuted: "#9a8086", inkDim: "#c0a8ae",
78
- border: "#eed4d8", borderStrong: "#dab8be",
79
- },
80
- };
81
-
82
- const macaronMint: ColorSystem = {
83
- id: "macaron-mint",
84
- name: "Macaron Mint",
85
- nameCN: "马卡龙 薄荷",
86
- description: "清新薄荷绿,轻盈爽朗",
87
- mode: "light",
88
- palette: {
89
- accent: "#8cc9a8", accentLight: "#e6f6ee", accentDark: "#6aaa88",
90
- surface: "#f8fcfa", panel: "#eef6f2", panelAlt: "#e2f0e8",
91
- ink: "#304538", inkMuted: "#809588", inkDim: "#a8c0b0",
92
- border: "#cce8d6", borderStrong: "#b0d4bc",
93
- },
94
- };
95
-
96
- const macaronBlueberry: ColorSystem = {
97
- id: "macaron-blueberry",
98
- name: "Macaron Blueberry",
99
- nameCN: "马卡龙 蓝莓",
100
- description: "淡紫蓝调,温柔梦幻",
101
- mode: "light",
102
- palette: {
103
- accent: "#9aa8e0", accentLight: "#eaecf8", accentDark: "#7888c0",
104
- surface: "#f8f8fc", panel: "#f0f2f8", panelAlt: "#e6e8f4",
105
- ink: "#303548", inkMuted: "#808898", inkDim: "#a8b0c0",
106
- border: "#d0d4e8", borderStrong: "#b8bcd4",
107
- },
108
- };
109
-
110
- // ═══════════════════════════════════════════════════════════
111
- // 北欧 (Nordic) — 极简自然色
112
- // ═══════════════════════════════════════════════════════════
113
-
114
- const nordicWood: ColorSystem = {
115
- id: "nordic-wood",
116
- name: "Nordic Wood",
117
- nameCN: "北欧 原木",
118
- description: "浅木色与暖白搭配,自然温馨",
119
- mode: "light",
120
- palette: {
121
- accent: "#8b7355", accentLight: "#e8e0d4", accentDark: "#6b5a40",
122
- surface: "#faf8f4", panel: "#f2eeea", panelAlt: "#e8e2dc",
123
- ink: "#3a3530", inkMuted: "#8a8078", inkDim: "#b0a89e",
124
- border: "#d8d0c4", borderStrong: "#c0b8aa",
125
- },
126
- };
127
-
128
- const nordicMinimal: ColorSystem = {
129
- id: "nordic-minimal",
130
- name: "Nordic Minimal",
131
- nameCN: "北欧 极简白",
132
- description: "纯白基调,冷蓝点缀,极致简洁",
133
- mode: "light",
134
- palette: {
135
- accent: "#4a7c96", accentLight: "#dce6ec", accentDark: "#3a6078",
136
- surface: "#fefefe", panel: "#f6f8fa", panelAlt: "#eef2f6",
137
- ink: "#2a2a2a", inkMuted: "#7a7a7a", inkDim: "#aaaaaa",
138
- border: "#e0e0e0", borderStrong: "#c8c8c8",
139
- },
140
- };
141
-
142
- const nordicGray: ColorSystem = {
143
- id: "nordic-gray",
144
- name: "Nordic Gray",
145
- nameCN: "北欧 灰调",
146
- description: "中灰基调,森林绿点缀,沉稳自然",
147
- mode: "light",
148
- palette: {
149
- accent: "#6b8a7a", accentLight: "#dce6e0", accentDark: "#4e6b5c",
150
- surface: "#f6f5f4", panel: "#edebe8", panelAlt: "#e2e0dc",
151
- ink: "#3a3a3a", inkMuted: "#8a8a8a", inkDim: "#b0b0b0",
152
- border: "#d8d6d2", borderStrong: "#c0beb8",
153
- },
154
- };
155
-
156
- // ═══════════════════════════════════════════════════════════
157
- // 复古 (Vintage) — 暖棕岁月感
158
- // ═══════════════════════════════════════════════════════════
159
-
160
- const vintageWarmBrown: ColorSystem = {
161
- id: "vintage-brown",
162
- name: "Vintage Warm Brown",
163
- nameCN: "复古 暖棕",
164
- description: "暖棕纸色,旧时光氛围",
165
- mode: "light",
166
- palette: {
167
- accent: "#8b5e3c", accentLight: "#e8dcd0", accentDark: "#6b4428",
168
- surface: "#f8f2ea", panel: "#f0e8dc", panelAlt: "#e6dcc8",
169
- ink: "#3a3028", inkMuted: "#8a7a6a", inkDim: "#b0a090",
170
- border: "#d4c8b8", borderStrong: "#bca898",
171
- },
172
- };
173
-
174
- const vintageNewspaper: ColorSystem = {
175
- id: "vintage-newspaper",
176
- name: "Vintage Newspaper",
177
- nameCN: "复古 旧报纸",
178
- description: "泛黄纸色,怀旧阅读感",
179
- mode: "light",
180
- palette: {
181
- accent: "#7a5a3a", accentLight: "#e8dcc8", accentDark: "#5a4028",
182
- surface: "#f4f0e0", panel: "#ece4d0", panelAlt: "#e2d8c0",
183
- ink: "#3a3528", inkMuted: "#8a8070", inkDim: "#b0a898",
184
- border: "#c8c0a8", borderStrong: "#b0a890",
185
- },
186
- };
187
-
188
- const vintageWine: ColorSystem = {
189
- id: "vintage-wine",
190
- name: "Vintage Wine",
191
- nameCN: "复古 酒红",
192
- description: "暗酒红点缀,浓郁优雅",
193
- mode: "light",
194
- palette: {
195
- accent: "#8b3a3a", accentLight: "#e8d4d4", accentDark: "#6b2828",
196
- surface: "#f8f2f2", panel: "#f0e8e8", panelAlt: "#e6dcdc",
197
- ink: "#3a2a2a", inkMuted: "#8a7070", inkDim: "#b09898",
198
- border: "#d4c0c0", borderStrong: "#bca8a8",
199
- },
200
- };
201
-
202
- // ═══════════════════════════════════════════════════════════
203
- // 薄荷绿 (Mint) — 清新自然
204
- // ═══════════════════════════════════════════════════════════
205
-
206
- const mintFresh: ColorSystem = {
207
- id: "mint-fresh",
208
- name: "Mint Fresh",
209
- nameCN: "薄荷绿 清新",
210
- description: "鲜嫩薄荷叶色,清爽宜人",
211
- mode: "light",
212
- palette: {
213
- accent: "#5aaa80", accentLight: "#dceee4", accentDark: "#408860",
214
- surface: "#f8fcfa", panel: "#eef6f2", panelAlt: "#e2f0e8",
215
- ink: "#2a3a30", inkMuted: "#708a78", inkDim: "#98b0a0",
216
- border: "#c8e4d4", borderStrong: "#a8ccb8",
217
- },
218
- };
219
-
220
- const mintDeep: ColorSystem = {
221
- id: "mint-deep",
222
- name: "Mint Deep",
223
- nameCN: "薄荷绿 深薄荷",
224
- description: "深邃薄荷绿,沉稳清新",
225
- mode: "light",
226
- palette: {
227
- accent: "#3a8a60", accentLight: "#d4e8dc", accentDark: "#286a48",
228
- surface: "#f4f8f6", panel: "#e8f0ec", panelAlt: "#dce8e2",
229
- ink: "#283830", inkMuted: "#708878", inkDim: "#98b0a0",
230
- border: "#b8dcc8", borderStrong: "#98c4ac",
231
- },
232
- };
233
-
234
- const mintDew: ColorSystem = {
235
- id: "mint-dew",
236
- name: "Mint Dew",
237
- nameCN: "薄荷绿 晨露",
238
- description: "晨露般清透浅绿,轻柔温和",
239
- mode: "light",
240
- palette: {
241
- accent: "#7ab898", accentLight: "#e0f0e8", accentDark: "#5a9878",
242
- surface: "#fafdfc", panel: "#f2f8f4", panelAlt: "#e8f2ec",
243
- ink: "#2c3832", inkMuted: "#788880", inkDim: "#a0b0a8",
244
- border: "#d0e8dc", borderStrong: "#b8d4c4",
245
- },
246
- };
247
-
248
- // ── Registry ──
249
-
250
- export const COLOR_SYSTEMS: ColorSystem[] = [
251
- morandiPink, morandiGreen, morandiBlue,
252
- macaronStrawberry, macaronMint, macaronBlueberry,
253
- nordicWood, nordicMinimal, nordicGray,
254
- vintageWarmBrown, vintageNewspaper, vintageWine,
255
- mintFresh, mintDeep, mintDew,
256
- ];
257
-
258
- export function getColorSystem(id: string): ColorSystem | undefined {
259
- return COLOR_SYSTEMS.find((cs) => cs.id === id);
260
- }
261
-
262
- /** Match extracted accent/surface to the nearest color system by hue + saturation distance. */
263
- export function matchColorSystem(accent: string, surface: string): ColorSystem {
264
- const hAccent = hslHue(accent);
265
- const sAccent = hslSaturation(accent);
266
- const lSurface = hslLightness(surface);
267
-
268
- let best = COLOR_SYSTEMS[0];
269
- let bestDist = Infinity;
270
-
271
- for (const cs of COLOR_SYSTEMS) {
272
- const hCs = hslHue(cs.palette.accent);
273
- const sCs = hslSaturation(cs.palette.accent);
274
- const lCs = hslLightness(cs.palette.surface);
275
-
276
- // Weighted distance: hue 40%, saturation 30%, lightness 30%
277
- const dH = Math.abs(hAccent - hCs);
278
- const hueDist = Math.min(dH, 360 - dH) / 360; // wrap around
279
- const satDist = Math.abs(sAccent - sCs);
280
- const lumDist = Math.abs(lSurface - lCs);
281
- const dist = hueDist * 0.4 + satDist * 0.3 + lumDist * 0.3;
282
-
283
- if (dist < bestDist) {
284
- bestDist = dist;
285
- best = cs;
286
- }
287
- }
288
- return best;
289
- }
@@ -1,137 +0,0 @@
1
- /** Color manipulation utilities. */
2
-
3
- export function hexToRgb(hex: string): [number, number, number] {
4
- const trimmed = hex.replace(/^#/, "").trim();
5
- if (!/^[0-9a-fA-F]{6}$/.test(trimmed) && !/^[0-9a-fA-F]{3}$/.test(trimmed)) {
6
- throw new Error(`Invalid hex color: "${hex}"`);
7
- }
8
- hex = trimmed;
9
- if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
10
- const r = parseInt(hex.substring(0, 2), 16);
11
- const g = parseInt(hex.substring(2, 4), 16);
12
- const b = parseInt(hex.substring(4, 6), 16);
13
- if ([r, g, b].some((v) => Number.isNaN(v))) {
14
- throw new Error(`Invalid hex color: "${hex}"`);
15
- }
16
- return [r, g, b];
17
- }
18
-
19
- export function rgbToHex(r: number, g: number, b: number): string {
20
- const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v)));
21
- return `#${clamp(r).toString(16).padStart(2, "0")}${clamp(g).toString(16).padStart(2, "0")}${clamp(b).toString(16).padStart(2, "0")}`;
22
- }
23
-
24
- export function lighten(hex: string, factor: number = 0.3): string {
25
- const [r, g, b] = hexToRgb(hex);
26
- return rgbToHex(
27
- r + (255 - r) * factor,
28
- g + (255 - g) * factor,
29
- b + (255 - b) * factor
30
- );
31
- }
32
-
33
- export function darken(hex: string, factor: number = 0.15): string {
34
- const [r, g, b] = hexToRgb(hex);
35
- return rgbToHex(r * (1 - factor), g * (1 - factor), b * (1 - factor));
36
- }
37
-
38
- export function toRgba(hex: string, alpha: number = 0.18): string {
39
- const [r, g, b] = hexToRgb(hex);
40
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
41
- }
42
-
43
- export function rgbToHsl(
44
- r: number, g: number, b: number
45
- ): [number, number, number] {
46
- r /= 255; g /= 255; b /= 255;
47
- const max = Math.max(r, g, b);
48
- const min = Math.min(r, g, b);
49
- let h = 0, s: number;
50
- const l = (max + min) / 2;
51
-
52
- if (max === min) {
53
- h = s = 0;
54
- } else {
55
- const d = max - min;
56
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
57
- switch (max) {
58
- case r:
59
- h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
60
- break;
61
- case g:
62
- h = ((b - r) / d + 2) / 6;
63
- break;
64
- case b:
65
- h = ((r - g) / d + 4) / 6;
66
- break;
67
- }
68
- }
69
- return [h, s, l];
70
- }
71
-
72
- export function deriveSyntaxColors(
73
- baseHex: string
74
- ): Record<string, string> {
75
- const [r, g, b] = hexToRgb(baseHex);
76
- const [, , l] = rgbToHsl(r, g, b);
77
- const isDark = l < 0.5;
78
-
79
- if (isDark) {
80
- return {
81
- comment: lighten(baseHex, 0.4),
82
- punctuation: lighten(baseHex, 0.2),
83
- keyword: "#569cd6",
84
- booleanBuiltinTag: "#569cd6",
85
- string: "#ce9178",
86
- number: "#b5cea8",
87
- functionClass: "#dcdcaa",
88
- operator: lighten(baseHex, 0.2),
89
- variable: lighten(baseHex, 0.2),
90
- regex: "#d16969",
91
- };
92
- } else {
93
- return {
94
- comment: darken(baseHex, 0.4),
95
- punctuation: baseHex,
96
- keyword: "#0000ff",
97
- booleanBuiltinTag: "#0000ff",
98
- string: "#a31515",
99
- number: "#098658",
100
- functionClass: "#795e26",
101
- operator: baseHex,
102
- variable: baseHex,
103
- regex: "#811f3f",
104
- };
105
- }
106
- }
107
-
108
- export function sanitizeCssString(value: string): string {
109
- // Remove HTML-injection vectors while preserving CSS syntax (quotes, spaces, commas)
110
- return value.replace(/[<>]/g, "");
111
- }
112
-
113
- export function normalizeThemeStyle(style: import("./models.js").ThemeStyle): void {
114
- const p = style.palette;
115
- const t = style.typography;
116
- const s = style.syntax;
117
-
118
- if (!p.headingColor) p.headingColor = p.textColor;
119
- if (!p.codeColor) p.codeColor = p.textColor;
120
- if (!p.blockquoteBorder) p.blockquoteBorder = p.accentColor;
121
- if (!p.selectionBg) p.selectionBg = toRgba(p.accentColor);
122
- if (!t.headingFontFamily) t.headingFontFamily = t.bodyFontFamily;
123
-
124
- // Sanitize font stacks to prevent CSS injection via crafted input
125
- if (t.bodyFontFamily) t.bodyFontFamily = sanitizeCssString(t.bodyFontFamily);
126
- if (t.headingFontFamily) t.headingFontFamily = sanitizeCssString(t.headingFontFamily);
127
- if (t.codeFontFamily) t.codeFontFamily = sanitizeCssString(t.codeFontFamily);
128
- if (style.seedTypography.fontMermaid) style.seedTypography.fontMermaid = sanitizeCssString(style.seedTypography.fontMermaid);
129
-
130
- if (!s.comment) {
131
- const derived = deriveSyntaxColors(p.codeBlockText);
132
- for (const [key, value] of Object.entries(derived)) {
133
- const k = key as keyof typeof s;
134
- if (!s[k]) (s as unknown as Record<string, string>)[key] = value;
135
- }
136
- }
137
- }
package/src/contrast.ts DELETED
@@ -1,66 +0,0 @@
1
- /** WCAG 2.1 relative luminance and contrast ratio calculations. */
2
-
3
- import { hexToRgb } from "./color-utils.js";
4
-
5
- /** Linearize an sRGB channel value (0-255) → (0-1). */
6
- function linearize(channel: number): number {
7
- const c = channel / 255;
8
- return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
9
- }
10
-
11
- /** WCAG 2.1 relative luminance of a hex color. */
12
- export function relativeLuminance(hex: string): number {
13
- const [r, g, b] = hexToRgb(hex);
14
- return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
15
- }
16
-
17
- /** WCAG 2.1 contrast ratio between two hex colors. Range: 1-21. */
18
- export function contrastRatio(hex1: string, hex2: string): number {
19
- const L1 = relativeLuminance(hex1);
20
- const L2 = relativeLuminance(hex2);
21
- const lighter = Math.max(L1, L2);
22
- const darker = Math.min(L1, L2);
23
- return (lighter + 0.05) / (darker + 0.05);
24
- }
25
-
26
- /** Get HSL lightness (0-1) from a hex color. */
27
- export function hslLightness(hex: string): number {
28
- const [r, g, b] = hexToRgb(hex);
29
- const max = Math.max(r, g, b) / 255;
30
- const min = Math.min(r, g, b) / 255;
31
- return (max + min) / 2;
32
- }
33
-
34
- /** Get HSL saturation (0-1) from a hex color. */
35
- export function hslSaturation(hex: string): number {
36
- const [r, g, b] = hexToRgb(hex);
37
- const max = Math.max(r, g, b) / 255;
38
- const min = Math.min(r, g, b) / 255;
39
- const l = (max + min) / 2;
40
- if (max === min) return 0;
41
- return l > 0.5 ? (max - min) / (2 - max - min) : (max - min) / (max + min);
42
- }
43
-
44
- /** Get HSL hue (0-360) from a hex color. */
45
- export function hslHue(hex: string): number {
46
- const [r, g, b] = hexToRgb(hex);
47
- const max = Math.max(r, g, b);
48
- const min = Math.min(r, g, b);
49
- if (max === min) return 0;
50
- const d = max - min;
51
- let h = 0;
52
- if (max === r) h = ((g - b) / d) % 6;
53
- else if (max === g) h = (b - r) / d + 2;
54
- else h = (r - g) / d + 4;
55
- return Math.round(h * 60);
56
- }
57
-
58
- /** Check if two colors are on the same hue axis (within tolerance degrees). */
59
- export function sameHueAxis(h1: string, h2: string, tolerance: number = 5): boolean {
60
- return Math.abs(hslHue(h1) - hslHue(h2)) <= tolerance;
61
- }
62
-
63
- /** Determine if a palette is light mode (bg luminance > 0.5). */
64
- export function isLightMode(bgHex: string): boolean {
65
- return hslLightness(bgHex) > 0.5;
66
- }