colbrush 1.5.0 → 1.7.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.
@@ -0,0 +1,104 @@
1
+ // src/react/ThemeProvider.tsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useState
8
+ } from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ var THEME_KEYS = [
11
+ "default",
12
+ "protanopia",
13
+ "deuteranopia",
14
+ "tritanopia"
15
+ ];
16
+ var THEME_LABEL = {
17
+ English: {
18
+ default: "default",
19
+ protanopia: "protanopia",
20
+ deuteranopia: "deuteranopia",
21
+ tritanopia: "tritanopia"
22
+ },
23
+ Korean: {
24
+ default: "\uAE30\uBCF8",
25
+ protanopia: "\uC801\uC0C9\uB9F9",
26
+ deuteranopia: "\uB179\uC0C9\uB9F9",
27
+ tritanopia: "\uCCAD\uC0C9\uB9F9"
28
+ }
29
+ };
30
+ var getThemeOptions = (lang) => THEME_KEYS.map((key) => ({ key, label: THEME_LABEL[lang][key] }));
31
+ var KEY = "theme";
32
+ var LANG_KEY = "theme_lang";
33
+ var ThemeContext = createContext({
34
+ theme: "default",
35
+ language: "English",
36
+ updateTheme: () => {
37
+ },
38
+ updateLanguage: () => {
39
+ },
40
+ simulationFilter: "none",
41
+ setSimulationFilter: () => {
42
+ }
43
+ });
44
+ var useTheme = () => useContext(ThemeContext);
45
+ function normalizeToKey(value) {
46
+ if (!value) return "default";
47
+ if (THEME_KEYS.includes(value))
48
+ return value;
49
+ const reverse = {};
50
+ ["English", "Korean"].forEach((lang) => {
51
+ Object.entries(THEME_LABEL[lang]).forEach(
52
+ ([k, label]) => {
53
+ reverse[label] = k;
54
+ }
55
+ );
56
+ });
57
+ return reverse[value] ?? "default";
58
+ }
59
+ function ThemeProvider({ children }) {
60
+ const [theme, setTheme] = useState("default");
61
+ const [simulationFilter, setSimulationFilter] = useState("none");
62
+ const [language, setLanguage] = useState("English");
63
+ useEffect(() => {
64
+ if (typeof window === "undefined") return;
65
+ const storedTheme = normalizeToKey(localStorage.getItem(KEY));
66
+ const storedLang = localStorage.getItem(LANG_KEY) || "English";
67
+ setTheme(storedTheme);
68
+ setLanguage(storedLang);
69
+ document.documentElement.setAttribute("data-theme", storedTheme);
70
+ }, []);
71
+ const updateTheme = (k) => {
72
+ setTheme(k);
73
+ if (typeof window !== "undefined") {
74
+ localStorage.setItem(KEY, k);
75
+ document.documentElement.setAttribute("data-theme", k);
76
+ }
77
+ };
78
+ const updateLanguage = (t) => {
79
+ setLanguage(t);
80
+ if (typeof window !== "undefined") {
81
+ localStorage.setItem(LANG_KEY, t);
82
+ }
83
+ };
84
+ const value = useMemo(
85
+ () => ({
86
+ theme,
87
+ language,
88
+ updateTheme,
89
+ updateLanguage,
90
+ simulationFilter,
91
+ setSimulationFilter
92
+ }),
93
+ [theme, language, simulationFilter]
94
+ );
95
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
96
+ }
97
+ var THEMES = THEME_LABEL;
98
+
99
+ export {
100
+ getThemeOptions,
101
+ useTheme,
102
+ ThemeProvider,
103
+ THEMES
104
+ };
package/dist/cli.cjs CHANGED
@@ -75,16 +75,17 @@ var DEFAULT_SCALE = {
75
75
 
76
76
  // src/core/utils/colorScale.ts
77
77
  var CLAMP01 = (x) => Math.max(0, Math.min(1, x));
78
+ var Color = import_colorjs.default.default ?? import_colorjs.default;
78
79
  function hexToOKLCH(hex) {
79
- const c = new import_colorjs.default(hex);
80
+ const c = new Color(hex);
80
81
  const o = c.to("oklch");
81
82
  return { l: o.l, c: o.c, h: o.h ?? 0 };
82
83
  }
83
84
  function oklchToHex(l, c, h) {
84
- const color = new import_colorjs.default("oklch", [l, c, h]);
85
+ const color = new Color("oklch", [l, c, h]);
85
86
  const srgb = color.to("srgb");
86
87
  const r = CLAMP01(srgb.r), g = CLAMP01(srgb.g), b = CLAMP01(srgb.b);
87
- return new import_colorjs.default("srgb", [r, g, b]).toString({ format: "hex" });
88
+ return new Color("srgb", [r, g, b]).toString({ format: "hex" });
88
89
  }
89
90
  function buildScaleFromBaseHex(baseHex, opts) {
90
91
  const keys = opts?.keys ?? DEFAULT_KEYS;
@@ -190,72 +191,48 @@ function inferRich(varName, baseHex) {
190
191
 
191
192
  // src/cli/colorTransform.ts
192
193
  var import_chroma_js = __toESM(require("chroma-js"), 1);
193
- var import_color_blind = __toESM(require("color-blind"), 1);
194
- var THRESHOLD = 10;
195
- var CANDIDATE_COUNT = 10;
196
- var HUE_STEP = 180;
197
- function generateCandidates(hex) {
198
- const [baseHue] = (0, import_chroma_js.default)(hex).hcl();
199
- const candidates = [];
200
- candidates[0] = hex;
201
- for (let i = 1; i <= CANDIDATE_COUNT / 2; i++) {
202
- candidates[i * 2 - 1] = (0, import_chroma_js.default)(hex).set("hcl.h", (baseHue + HUE_STEP / CANDIDATE_COUNT * i) % 360).hex();
203
- candidates[i * 2] = (0, import_chroma_js.default)(hex).set(
204
- "hcl.h",
205
- (baseHue - HUE_STEP / CANDIDATE_COUNT * i + 360) % 360
206
- ).hex();
207
- }
208
- return candidates;
209
- }
210
- function findOptimalColorCombination(colorKeys, baseColorsArray, candidateList, vision) {
211
- const n = baseColorsArray.length;
212
- let bestColors = null;
213
- let minDeltaSum = Infinity;
214
- let blind;
215
- switch (vision) {
216
- // 적색맹
217
- case "protanopia":
218
- blind = import_color_blind.default.protanopia;
219
- break;
220
- // 녹색맹
221
- case "deuteranopia":
222
- blind = import_color_blind.default.deuteranopia;
223
- break;
224
- // 청색맹
225
- case "tritanopia":
226
- blind = import_color_blind.default.tritanopia;
227
- break;
228
- default:
229
- throw new Error("Invalid color blindness option");
230
- }
231
- function dfs(index, currentColors, totalDelta) {
232
- if (index === n) {
233
- if (isValidColors(currentColors, blind)) {
234
- if (totalDelta < minDeltaSum) {
235
- minDeltaSum = totalDelta;
236
- bestColors = [...currentColors];
237
- }
238
- }
239
- return;
194
+ var ALPHA = 1.5;
195
+ function colorTranslate(colorKeys, baseColorsArray, vision) {
196
+ const labArray = baseColorsArray.map((hex) => (0, import_chroma_js.default)(hex).lab());
197
+ const newLabArray = labArray.map((lab) => {
198
+ const newLab = [...lab];
199
+ switch (vision) {
200
+ case "protanopia":
201
+ newLab[2] += ALPHA * lab[1];
202
+ break;
203
+ case "deuteranopia":
204
+ newLab[2] += ALPHA * lab[1];
205
+ break;
206
+ case "tritanopia":
207
+ newLab[1] += ALPHA * 0.2 * lab[2];
208
+ break;
240
209
  }
241
- for (const candidate of candidateList[index]) {
242
- const delta = import_chroma_js.default.deltaE(baseColorsArray[index], candidate);
243
- if (totalDelta + delta > minDeltaSum) continue;
244
- currentColors.push(candidate);
245
- dfs(index + 1, currentColors, totalDelta + delta);
246
- currentColors.pop();
247
- }
248
- }
249
- dfs(0, [], 0);
250
- const finalColors = bestColors ?? [...baseColorsArray];
210
+ return newLab;
211
+ });
212
+ const newColorArray = newLabArray.map((lab) => import_chroma_js.default.lab(lab[0], lab[1], lab[2]).hex());
251
213
  return colorKeys.reduce(
252
214
  (acc, key, idx) => {
253
- acc[key] = finalColors[idx];
215
+ acc[key] = newColorArray[idx];
254
216
  return acc;
255
217
  },
256
218
  {}
257
219
  );
258
220
  }
221
+ function buildThemeForVision(colorKeys, baseColorsArray, vision) {
222
+ const colors = colorTranslate(
223
+ colorKeys,
224
+ baseColorsArray,
225
+ vision
226
+ );
227
+ const variables = Object.entries(colors).reduce(
228
+ (acc, [varName, baseHex]) => {
229
+ acc[varName] = inferRich(varName, baseHex);
230
+ return acc;
231
+ },
232
+ {}
233
+ );
234
+ return { vision, variables };
235
+ }
259
236
  function prepareCandidates(variables, progress) {
260
237
  const scaleGroups = {};
261
238
  const filteredVariables = {};
@@ -282,30 +259,7 @@ function prepareCandidates(variables, progress) {
282
259
  }
283
260
  const colorKeys = Object.keys(filteredVariables);
284
261
  const baseColorsArray = Object.values(filteredVariables).map((v) => v.base);
285
- progress?.startSection("Generate candidates");
286
- const candidateList = [];
287
- for (let i = 0; i < baseColorsArray.length; i++) {
288
- candidateList[i] = generateCandidates(baseColorsArray[i]);
289
- progress?.update((i + 1) / baseColorsArray.length * 100);
290
- }
291
- progress?.finishSection("Done");
292
- return { colorKeys, baseColorsArray, candidateList };
293
- }
294
- function buildThemeForVision(colorKeys, baseColorsArray, candidateList, vision) {
295
- const colors = findOptimalColorCombination(
296
- colorKeys,
297
- baseColorsArray,
298
- candidateList,
299
- vision
300
- );
301
- const variables = Object.entries(colors).reduce(
302
- (acc, [varName, baseHex]) => {
303
- acc[varName] = inferRich(varName, baseHex);
304
- return acc;
305
- },
306
- {}
307
- );
308
- return { vision, variables };
262
+ return { colorKeys, baseColorsArray };
309
263
  }
310
264
  function getMiddleScaleKey(keys) {
311
265
  const scaleNumbers = keys.map((k) => {
@@ -319,15 +273,6 @@ function getMiddleScaleKey(keys) {
319
273
  const middleKey = keys.find((k) => k.endsWith(midNumber.toString())) || null;
320
274
  return middleKey;
321
275
  }
322
- function isValidColors(colors, blind) {
323
- for (let i = 0; i < colors.length; i++) {
324
- for (let j = i + 1; j < colors.length; j++) {
325
- if (import_chroma_js.default.deltaE(blind(colors[i]), blind(colors[j])) < THRESHOLD)
326
- return false;
327
- }
328
- }
329
- return true;
330
- }
331
276
 
332
277
  // src/cli/runThemeApply.ts
333
278
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -338,7 +283,7 @@ function removeExistingThemeBlocks(content) {
338
283
  let cleaned = content;
339
284
  for (const vision of visions) {
340
285
  const pattern = new RegExp(
341
- `\\/\\*\\s*${vision} theme start\\s*\\*\\/[^]*?\\/\\*\\s*${vision} theme end\\s*\\*\\/`,
286
+ `\\[data-theme=['"]${vision}['"]\\][^}]*}\\s*`,
342
287
  "gm"
343
288
  );
344
289
  cleaned = cleaned.replace(pattern, "");
@@ -418,7 +363,7 @@ async function runThemeApply(cssPath, progress) {
418
363
  progress?.finishSection("Done");
419
364
  const visions = ["deuteranopia", "protanopia", "tritanopia"];
420
365
  try {
421
- const { colorKeys, baseColorsArray, candidateList } = prepareCandidates(
366
+ const { colorKeys, baseColorsArray } = prepareCandidates(
422
367
  variables,
423
368
  progress
424
369
  );
@@ -429,7 +374,6 @@ async function runThemeApply(cssPath, progress) {
429
374
  const themeData = buildThemeForVision(
430
375
  colorKeys,
431
376
  baseColorsArray,
432
- candidateList,
433
377
  vision
434
378
  );
435
379
  progress?.update(70, "Applying CSS...");
package/dist/client.cjs CHANGED
@@ -60,6 +60,9 @@ var ThemeContext = (0, import_react.createContext)({
60
60
  updateTheme: () => {
61
61
  },
62
62
  updateLanguage: () => {
63
+ },
64
+ simulationFilter: "none",
65
+ setSimulationFilter: () => {
63
66
  }
64
67
  });
65
68
  var useTheme = () => (0, import_react.useContext)(ThemeContext);
@@ -79,6 +82,7 @@ function normalizeToKey(value) {
79
82
  }
80
83
  function ThemeProvider({ children }) {
81
84
  const [theme, setTheme] = (0, import_react.useState)("default");
85
+ const [simulationFilter, setSimulationFilter] = (0, import_react.useState)("none");
82
86
  const [language, setLanguage] = (0, import_react.useState)("English");
83
87
  (0, import_react.useEffect)(() => {
84
88
  if (typeof window === "undefined") return;
@@ -102,8 +106,15 @@ function ThemeProvider({ children }) {
102
106
  }
103
107
  };
104
108
  const value = (0, import_react.useMemo)(
105
- () => ({ theme, language, updateTheme, updateLanguage }),
106
- [theme, language]
109
+ () => ({
110
+ theme,
111
+ language,
112
+ updateTheme,
113
+ updateLanguage,
114
+ simulationFilter,
115
+ setSimulationFilter
116
+ }),
117
+ [theme, language, simulationFilter]
107
118
  );
108
119
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeContext.Provider, { value, children });
109
120
  }
package/dist/client.d.cts CHANGED
@@ -3,12 +3,16 @@ import { ReactNode } from 'react';
3
3
 
4
4
  type TLanguage = 'English' | 'Korean';
5
5
  declare const THEME_KEYS: readonly ["default", "protanopia", "deuteranopia", "tritanopia"];
6
+ declare const SIMULATION_KEYS: readonly ["none", "deuteranopia", "protanopia", "tritanopia"];
6
7
  type ThemeKey = (typeof THEME_KEYS)[number];
8
+ type SimulationKey = (typeof SIMULATION_KEYS)[number];
7
9
  type ThemeContextType = {
8
10
  theme: ThemeKey;
9
11
  language: TLanguage;
10
12
  updateTheme: (k: ThemeKey) => void;
11
13
  updateLanguage: (t: TLanguage) => void;
14
+ simulationFilter: SimulationKey;
15
+ setSimulationFilter: (value: SimulationKey) => void;
12
16
  };
13
17
  declare const useTheme: () => ThemeContextType;
14
18
  type ThemeProviderProps = {
@@ -16,7 +20,7 @@ type ThemeProviderProps = {
16
20
  };
17
21
  declare function ThemeProvider({ children }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
18
22
  type ThemeType = ThemeKey;
19
- declare const THEMES: Record<TLanguage, Record<"default" | "protanopia" | "deuteranopia" | "tritanopia", string>>;
23
+ declare const THEMES: Record<TLanguage, Record<"deuteranopia" | "protanopia" | "tritanopia" | "default", string>>;
20
24
 
21
25
  type Props = {
22
26
  options?: {
package/dist/client.d.ts CHANGED
@@ -3,12 +3,16 @@ import { ReactNode } from 'react';
3
3
 
4
4
  type TLanguage = 'English' | 'Korean';
5
5
  declare const THEME_KEYS: readonly ["default", "protanopia", "deuteranopia", "tritanopia"];
6
+ declare const SIMULATION_KEYS: readonly ["none", "deuteranopia", "protanopia", "tritanopia"];
6
7
  type ThemeKey = (typeof THEME_KEYS)[number];
8
+ type SimulationKey = (typeof SIMULATION_KEYS)[number];
7
9
  type ThemeContextType = {
8
10
  theme: ThemeKey;
9
11
  language: TLanguage;
10
12
  updateTheme: (k: ThemeKey) => void;
11
13
  updateLanguage: (t: TLanguage) => void;
14
+ simulationFilter: SimulationKey;
15
+ setSimulationFilter: (value: SimulationKey) => void;
12
16
  };
13
17
  declare const useTheme: () => ThemeContextType;
14
18
  type ThemeProviderProps = {
@@ -16,7 +20,7 @@ type ThemeProviderProps = {
16
20
  };
17
21
  declare function ThemeProvider({ children }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
18
22
  type ThemeType = ThemeKey;
19
- declare const THEMES: Record<TLanguage, Record<"default" | "protanopia" | "deuteranopia" | "tritanopia", string>>;
23
+ declare const THEMES: Record<TLanguage, Record<"deuteranopia" | "protanopia" | "tritanopia" | "default", string>>;
20
24
 
21
25
  type Props = {
22
26
  options?: {