colbrush 1.7.0 → 1.9.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 colbrush
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -29,7 +29,7 @@ npm install colbrush
29
29
  ---
30
30
  ## Usage
31
31
  ### 1. Define CSS variables (index.css or global CSS)
32
- ```
32
+ ```css
33
33
  @theme {
34
34
  --color-primary-500: #7fe4c1;
35
35
  --color-secondary-yellow: #fdfa91;
@@ -102,6 +102,31 @@ export default function TestPage() {
102
102
  );
103
103
  }
104
104
  ```
105
+ ### 6. Apply SimulationFilter for vision simulation
106
+ ```
107
+ import { SimulationFilter } from 'colbrush/devtools';
108
+
109
+ function App() {
110
+ return (
111
+ <ThemeProvider>
112
+ <SimulationFilter
113
+ initialMode="normal"
114
+ toolbarPosition="left-bottom"
115
+ ...>
116
+ <YourApp />
117
+ </SimulationFilter>
118
+ </ThemeProvider>
119
+ );
120
+ }
121
+ ```
122
+ | **SimulationFilterProp** | **Type** | **Default** | **Description** |
123
+ | ----------------- | ----------------------------------------------------------------- | ------------- | ------------------------- |
124
+ | `initialMode?` | `"normal"` / `"protanopia"` / `"deuteranopia"` / `"tritanopia"` | `"normal"` | initial simulation mode |
125
+ | `toolbarPosition?` | `"top-left"` / `"top-right"` / `"bottom-left"` / `"bottom-right"` | `"top-right"` | toolbar position |
126
+ | `shortcut?` | `boolean` | `true` | enable keyboard shortcuts (⌘/Ctrl + Alt + D) |
127
+ | `productionGuard?` | `boolean` | `false` | block usage in production |
128
+
129
+
105
130
  ## Supported Vision Types
106
131
  | **Vision Type** | **설명** |
107
132
  | --------------- | ------ |
@@ -119,12 +144,27 @@ export default function TestPage() {
119
144
 
120
145
  # 📜 License
121
146
 
122
- Copyright (c) 2025 Team Colbrush
147
+ MIT License
148
+
149
+ Copyright (c) 2025 colbrush
150
+
151
+ Permission is hereby granted, free of charge, to any person obtaining a copy
152
+ of this software and associated documentation files (the "Software"), to deal
153
+ in the Software without restriction, including without limitation the rights
154
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
155
+ copies of the Software, and to permit persons to whom the Software is
156
+ furnished to do so, subject to the following conditions:
123
157
 
124
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
158
+ The above copyright notice and this permission notice shall be included in all
159
+ copies or substantial portions of the Software.
125
160
 
126
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
161
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
162
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
163
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
164
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
165
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
166
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
167
+ SOFTWARE.
127
168
 
128
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
129
169
 
130
170
 
@@ -0,0 +1,168 @@
1
+ // src/react/ThemeProvider.tsx
2
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
3
+
4
+ // src/core/constants/modes.ts
5
+ var SIMULATION_MODES = [
6
+ "deuteranopia",
7
+ "protanopia",
8
+ "tritanopia"
9
+ ];
10
+ var THEME_MODES = [
11
+ "default",
12
+ ...SIMULATION_MODES
13
+ ];
14
+ var VISION_MODES = ["none", ...SIMULATION_MODES];
15
+ var MODE_LABELS = {
16
+ English: {
17
+ none: "default",
18
+ protanopia: "protanopia",
19
+ deuteranopia: "deuteranopia",
20
+ tritanopia: "tritanopia"
21
+ },
22
+ Korean: {
23
+ none: "\uAEBC\uC9D0",
24
+ protanopia: "\uC801\uC0C9\uB9F9",
25
+ deuteranopia: "\uB179\uC0C9\uB9F9",
26
+ tritanopia: "\uCCAD\uC0C9\uB9F9"
27
+ }
28
+ };
29
+ var THEME_LABEL = {
30
+ English: {
31
+ default: "default",
32
+ protanopia: "protanopia",
33
+ deuteranopia: "deuteranopia",
34
+ tritanopia: "tritanopia"
35
+ },
36
+ Korean: {
37
+ default: "\uAE30\uBCF8",
38
+ protanopia: "\uC801\uC0C9\uB9F9",
39
+ deuteranopia: "\uB179\uC0C9\uB9F9",
40
+ tritanopia: "\uCCAD\uC0C9\uB9F9"
41
+ }
42
+ };
43
+
44
+ // src/core/constants/key.ts
45
+ var ThemeStorageKey = "colbrush-theme";
46
+ var LanguageStorageKey = "colbrush-language";
47
+ var SimulationStorageKey = "colbrush-filter";
48
+ var VISION_PORTAL_ID = "cb-vision-portal";
49
+ var FILTER_ID = "cb-vision-filter";
50
+ var FILTER_WRAPPER_ID = "cb-vision-filter-root";
51
+ var THEME_SWITCHER_PORTAL_ID = "theme-switcher-portal";
52
+
53
+ // src/react/ThemeProvider.tsx
54
+ import { jsx } from "react/jsx-runtime";
55
+ var getThemeOptions = (lang) => THEME_MODES.map((key) => ({ key, label: THEME_LABEL[lang][key] }));
56
+ var ThemeContext = createContext({
57
+ theme: "default",
58
+ language: "English",
59
+ updateTheme: () => {
60
+ },
61
+ updateLanguage: () => {
62
+ },
63
+ simulationFilter: "none",
64
+ setSimulationFilter: () => {
65
+ }
66
+ });
67
+ var useTheme = () => useContext(ThemeContext);
68
+ function normalizeToKey(value) {
69
+ if (!value) return "default";
70
+ if (THEME_MODES.includes(value))
71
+ return value;
72
+ const reverse = {};
73
+ ["English", "Korean"].forEach((lang) => {
74
+ Object.entries(THEME_LABEL[lang]).forEach(
75
+ ([k, label]) => {
76
+ reverse[label] = k;
77
+ }
78
+ );
79
+ });
80
+ return reverse[value] ?? "default";
81
+ }
82
+ function ThemeProvider({ children }) {
83
+ const [theme, setTheme] = useState("default");
84
+ const [simulationFilter, setSimulationFilter] = useState("none");
85
+ const [language, setLanguage] = useState("English");
86
+ useEffect(() => {
87
+ if (typeof window === "undefined") return;
88
+ const storedTheme = normalizeToKey(
89
+ localStorage.getItem(ThemeStorageKey)
90
+ );
91
+ const storedLang = localStorage.getItem(LanguageStorageKey) || "English";
92
+ const storedFilter = localStorage.getItem(SimulationStorageKey) || "none";
93
+ setSimulationFilter(storedFilter);
94
+ setTheme(storedTheme);
95
+ setLanguage(storedLang);
96
+ document.documentElement.setAttribute("data-theme", storedTheme);
97
+ }, []);
98
+ const updateTheme = (k) => {
99
+ setTheme(k);
100
+ if (typeof window !== "undefined") {
101
+ localStorage.setItem(ThemeStorageKey, k);
102
+ document.documentElement.setAttribute("data-theme", k);
103
+ }
104
+ };
105
+ const updateLanguage = (t) => {
106
+ setLanguage(t);
107
+ if (typeof window !== "undefined") {
108
+ localStorage.setItem(LanguageStorageKey, t);
109
+ }
110
+ };
111
+ const value = useMemo(
112
+ () => ({
113
+ theme,
114
+ language,
115
+ updateTheme,
116
+ updateLanguage,
117
+ simulationFilter,
118
+ setSimulationFilter
119
+ }),
120
+ [theme, language, simulationFilter]
121
+ );
122
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
123
+ }
124
+ var THEMES = THEME_LABEL;
125
+
126
+ // src/core/constants/position.ts
127
+ var Position = [
128
+ "left-bottom",
129
+ "right-bottom",
130
+ "left-top",
131
+ "right-top"
132
+ ];
133
+ var SWITCHER_POSITION = {
134
+ "left-bottom": "left-[16px] bottom-[16px]",
135
+ "right-bottom": "right-[16px] bottom-[16px]",
136
+ "left-top": "left-[16px] top-[16px]",
137
+ "right-top": "right-[16px] top-[16px]"
138
+ };
139
+ var SWITCHER_MENU_POSITION = {
140
+ "left-bottom": "left-[25px] bottom-[100px]",
141
+ "right-bottom": "right-[25px] bottom-[100px]",
142
+ "left-top": "left-[25px] top-[100px]",
143
+ "right-top": "right-[25px] top-[100px]"
144
+ };
145
+ var TOOLBAR_POSITION = {
146
+ "left-bottom": "left-[16px] bottom-[16px]",
147
+ "right-bottom": "right-[16px] bottom-[16px]",
148
+ "left-top": "left-[16px] top-[16px]",
149
+ "right-top": "right-[16px] top-[16px]"
150
+ };
151
+
152
+ export {
153
+ SIMULATION_MODES,
154
+ MODE_LABELS,
155
+ SimulationStorageKey,
156
+ VISION_PORTAL_ID,
157
+ FILTER_ID,
158
+ FILTER_WRAPPER_ID,
159
+ THEME_SWITCHER_PORTAL_ID,
160
+ getThemeOptions,
161
+ useTheme,
162
+ ThemeProvider,
163
+ THEMES,
164
+ Position,
165
+ SWITCHER_POSITION,
166
+ SWITCHER_MENU_POSITION,
167
+ TOOLBAR_POSITION
168
+ };
package/dist/client.cjs CHANGED
@@ -30,13 +30,18 @@ module.exports = __toCommonJS(client_exports);
30
30
 
31
31
  // src/react/ThemeProvider.tsx
32
32
  var import_react = require("react");
33
- var import_jsx_runtime = require("react/jsx-runtime");
34
- var THEME_KEYS = [
35
- "default",
36
- "protanopia",
33
+
34
+ // src/core/constants/modes.ts
35
+ var SIMULATION_MODES = [
37
36
  "deuteranopia",
37
+ "protanopia",
38
38
  "tritanopia"
39
39
  ];
40
+ var THEME_MODES = [
41
+ "default",
42
+ ...SIMULATION_MODES
43
+ ];
44
+ var VISION_MODES = ["none", ...SIMULATION_MODES];
40
45
  var THEME_LABEL = {
41
46
  English: {
42
47
  default: "default",
@@ -51,9 +56,16 @@ var THEME_LABEL = {
51
56
  tritanopia: "\uCCAD\uC0C9\uB9F9"
52
57
  }
53
58
  };
54
- var getThemeOptions = (lang) => THEME_KEYS.map((key) => ({ key, label: THEME_LABEL[lang][key] }));
55
- var KEY = "theme";
56
- var LANG_KEY = "theme_lang";
59
+
60
+ // src/core/constants/key.ts
61
+ var ThemeStorageKey = "colbrush-theme";
62
+ var LanguageStorageKey = "colbrush-language";
63
+ var SimulationStorageKey = "colbrush-filter";
64
+ var THEME_SWITCHER_PORTAL_ID = "theme-switcher-portal";
65
+
66
+ // src/react/ThemeProvider.tsx
67
+ var import_jsx_runtime = require("react/jsx-runtime");
68
+ var getThemeOptions = (lang) => THEME_MODES.map((key) => ({ key, label: THEME_LABEL[lang][key] }));
57
69
  var ThemeContext = (0, import_react.createContext)({
58
70
  theme: "default",
59
71
  language: "English",
@@ -68,7 +80,7 @@ var ThemeContext = (0, import_react.createContext)({
68
80
  var useTheme = () => (0, import_react.useContext)(ThemeContext);
69
81
  function normalizeToKey(value) {
70
82
  if (!value) return "default";
71
- if (THEME_KEYS.includes(value))
83
+ if (THEME_MODES.includes(value))
72
84
  return value;
73
85
  const reverse = {};
74
86
  ["English", "Korean"].forEach((lang) => {
@@ -86,8 +98,12 @@ function ThemeProvider({ children }) {
86
98
  const [language, setLanguage] = (0, import_react.useState)("English");
87
99
  (0, import_react.useEffect)(() => {
88
100
  if (typeof window === "undefined") return;
89
- const storedTheme = normalizeToKey(localStorage.getItem(KEY));
90
- const storedLang = localStorage.getItem(LANG_KEY) || "English";
101
+ const storedTheme = normalizeToKey(
102
+ localStorage.getItem(ThemeStorageKey)
103
+ );
104
+ const storedLang = localStorage.getItem(LanguageStorageKey) || "English";
105
+ const storedFilter = localStorage.getItem(SimulationStorageKey) || "none";
106
+ setSimulationFilter(storedFilter);
91
107
  setTheme(storedTheme);
92
108
  setLanguage(storedLang);
93
109
  document.documentElement.setAttribute("data-theme", storedTheme);
@@ -95,14 +111,14 @@ function ThemeProvider({ children }) {
95
111
  const updateTheme = (k) => {
96
112
  setTheme(k);
97
113
  if (typeof window !== "undefined") {
98
- localStorage.setItem(KEY, k);
114
+ localStorage.setItem(ThemeStorageKey, k);
99
115
  document.documentElement.setAttribute("data-theme", k);
100
116
  }
101
117
  };
102
118
  const updateLanguage = (t) => {
103
119
  setLanguage(t);
104
120
  if (typeof window !== "undefined") {
105
- localStorage.setItem(LANG_KEY, t);
121
+ localStorage.setItem(LanguageStorageKey, t);
106
122
  }
107
123
  };
108
124
  const value = (0, import_react.useMemo)(
@@ -577,14 +593,17 @@ var Deuteranopia_default = SvgDeuteranopia;
577
593
  // src/react/ThemeSwitcherPortal.tsx
578
594
  var import_react2 = require("react");
579
595
  var import_react_dom = require("react-dom");
580
- var PORTAL_ID = "theme-switcher-portal";
581
596
  var portalEl = null;
582
597
  var activeOwner = null;
583
598
  function ensurePortal() {
584
599
  if (typeof document === "undefined") throw new Error("No document");
585
600
  if (portalEl && document.body.contains(portalEl)) return portalEl;
586
- const existing = document.getElementById(PORTAL_ID);
587
- portalEl = existing ?? Object.assign(document.createElement("div"), { id: PORTAL_ID });
601
+ const existing = document.getElementById(
602
+ THEME_SWITCHER_PORTAL_ID
603
+ );
604
+ portalEl = existing ?? Object.assign(document.createElement("div"), {
605
+ id: THEME_SWITCHER_PORTAL_ID
606
+ });
588
607
  if (!existing) document.body.appendChild(portalEl);
589
608
  return portalEl;
590
609
  }
@@ -608,6 +627,20 @@ function ThemeSwitcherPortal({
608
627
  return (0, import_react_dom.createPortal)(isPrimary ? children : null, container);
609
628
  }
610
629
 
630
+ // src/core/constants/position.ts
631
+ var SWITCHER_POSITION = {
632
+ "left-bottom": "left-[16px] bottom-[16px]",
633
+ "right-bottom": "right-[16px] bottom-[16px]",
634
+ "left-top": "left-[16px] top-[16px]",
635
+ "right-top": "right-[16px] top-[16px]"
636
+ };
637
+ var SWITCHER_MENU_POSITION = {
638
+ "left-bottom": "left-[25px] bottom-[100px]",
639
+ "right-bottom": "right-[25px] bottom-[100px]",
640
+ "left-top": "left-[25px] top-[100px]",
641
+ "right-top": "right-[25px] top-[100px]"
642
+ };
643
+
611
644
  // src/react/ThemeSwitcher.tsx
612
645
  var import_jsx_runtime10 = require("react/jsx-runtime");
613
646
  var THEME_ICON = {
@@ -616,7 +649,7 @@ var THEME_ICON = {
616
649
  deuteranopia: Deuteranopia_default,
617
650
  tritanopia: Tritanopia_default
618
651
  };
619
- function ThemeSwitcher({ options }) {
652
+ function ThemeSwitcher({ options, position }) {
620
653
  const { theme, updateTheme, language, updateLanguage } = useTheme();
621
654
  const [hovered, setHovered] = (0, import_react3.useState)(null);
622
655
  const list = (0, import_react3.useMemo)(
@@ -625,6 +658,8 @@ function ThemeSwitcher({ options }) {
625
658
  );
626
659
  const [isOpen, setIsOpen] = (0, import_react3.useState)(false);
627
660
  const wrapperRef = (0, import_react3.useRef)(null);
661
+ const switcherClass = SWITCHER_POSITION[position ?? "right-bottom"];
662
+ const switcherMenuClass = SWITCHER_MENU_POSITION[position ?? "right-bottom"];
628
663
  (0, import_react3.useEffect)(() => {
629
664
  const handleClickOutside = (event) => {
630
665
  if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
@@ -646,7 +681,9 @@ function ThemeSwitcher({ options }) {
646
681
  "aria-haspopup": "menu",
647
682
  "aria-expanded": isOpen,
648
683
  onClick: toggle,
649
- className: "fixed right-[25px] bottom-[25px] w-[60px] h-[60px] p-[10px] bg-[#ffffff] rounded-full flex justify-center items-center shadow-[0_0_3px_0_rgba(0,0,0,0.17)]",
684
+ className: `fixed w-[60px] h-[60px] p-[10px] bg-[#ffffff] rounded-full flex justify-center items-center shadow-[0_0_3px_0_rgba(0,0,0,0.17)]
685
+ ${switcherClass}
686
+ `,
650
687
  children: isOpen ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(X_default, { className: "self-center" }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Logo_default, { className: "self-center" })
651
688
  }
652
689
  ),
@@ -655,7 +692,10 @@ function ThemeSwitcher({ options }) {
655
692
  {
656
693
  role: "menu",
657
694
  "aria-label": "Select theme",
658
- className: "fixed bottom-[100px] right-[25px] flex-col bg-[#ffffff] rounded-[18px] w-[220px] gap-[11px] filter drop-shadow-[0_0_1.3px_rgba(0,0,0,0.25)]",
695
+ className: `
696
+ fixed flex-col bg-[#ffffff] rounded-[18px] w-[220px] gap-[11px] filter drop-shadow-[0_0_1.3px_rgba(0,0,0,0.25)]
697
+ ${switcherMenuClass}
698
+ `,
659
699
  children: [
660
700
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { children: list.map((opt) => {
661
701
  const Icon = THEME_ICON[opt.key];
package/dist/client.d.cts CHANGED
@@ -1,11 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
+ import { T as TThemeKey, V as VisionMode, P as Position } from './simulationTypes-CenFTH7t.cjs';
3
4
 
4
5
  type TLanguage = 'English' | 'Korean';
5
- declare const THEME_KEYS: readonly ["default", "protanopia", "deuteranopia", "tritanopia"];
6
- declare const SIMULATION_KEYS: readonly ["none", "deuteranopia", "protanopia", "tritanopia"];
7
- type ThemeKey = (typeof THEME_KEYS)[number];
8
- type SimulationKey = (typeof SIMULATION_KEYS)[number];
6
+ type ThemeKey = TThemeKey[number];
7
+ type SimulationKey = VisionMode[number];
9
8
  type ThemeContextType = {
10
9
  theme: ThemeKey;
11
10
  language: TLanguage;
@@ -20,14 +19,15 @@ type ThemeProviderProps = {
20
19
  };
21
20
  declare function ThemeProvider({ children }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
22
21
  type ThemeType = ThemeKey;
23
- declare const THEMES: Record<TLanguage, Record<"deuteranopia" | "protanopia" | "tritanopia" | "default", string>>;
22
+ declare const THEMES: Record<TLanguage, Record<TThemeKey, string>>;
24
23
 
25
- type Props = {
24
+ type TThemeSwitcherProps = {
26
25
  options?: {
27
26
  key: ThemeKey;
28
27
  label: string;
29
28
  }[];
29
+ position?: Position;
30
30
  };
31
- declare function ThemeSwitcher({ options }: Props): react_jsx_runtime.JSX.Element;
31
+ declare function ThemeSwitcher({ options, position }: TThemeSwitcherProps): react_jsx_runtime.JSX.Element;
32
32
 
33
33
  export { THEMES, type TLanguage, ThemeProvider, ThemeSwitcher, type ThemeType, useTheme };
package/dist/client.d.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
+ import { T as TThemeKey, V as VisionMode, P as Position } from './simulationTypes-CenFTH7t.js';
3
4
 
4
5
  type TLanguage = 'English' | 'Korean';
5
- declare const THEME_KEYS: readonly ["default", "protanopia", "deuteranopia", "tritanopia"];
6
- declare const SIMULATION_KEYS: readonly ["none", "deuteranopia", "protanopia", "tritanopia"];
7
- type ThemeKey = (typeof THEME_KEYS)[number];
8
- type SimulationKey = (typeof SIMULATION_KEYS)[number];
6
+ type ThemeKey = TThemeKey[number];
7
+ type SimulationKey = VisionMode[number];
9
8
  type ThemeContextType = {
10
9
  theme: ThemeKey;
11
10
  language: TLanguage;
@@ -20,14 +19,15 @@ type ThemeProviderProps = {
20
19
  };
21
20
  declare function ThemeProvider({ children }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
22
21
  type ThemeType = ThemeKey;
23
- declare const THEMES: Record<TLanguage, Record<"deuteranopia" | "protanopia" | "tritanopia" | "default", string>>;
22
+ declare const THEMES: Record<TLanguage, Record<TThemeKey, string>>;
24
23
 
25
- type Props = {
24
+ type TThemeSwitcherProps = {
26
25
  options?: {
27
26
  key: ThemeKey;
28
27
  label: string;
29
28
  }[];
29
+ position?: Position;
30
30
  };
31
- declare function ThemeSwitcher({ options }: Props): react_jsx_runtime.JSX.Element;
31
+ declare function ThemeSwitcher({ options, position }: TThemeSwitcherProps): react_jsx_runtime.JSX.Element;
32
32
 
33
33
  export { THEMES, type TLanguage, ThemeProvider, ThemeSwitcher, type ThemeType, useTheme };
package/dist/client.js CHANGED
@@ -1,10 +1,13 @@
1
1
  "use client";
2
2
  import {
3
+ SWITCHER_MENU_POSITION,
4
+ SWITCHER_POSITION,
3
5
  THEMES,
6
+ THEME_SWITCHER_PORTAL_ID,
4
7
  ThemeProvider,
5
8
  getThemeOptions,
6
9
  useTheme
7
- } from "./chunk-EFIJAMPH.js";
10
+ } from "./chunk-WQEJ7FU2.js";
8
11
 
9
12
  // src/react/ThemeSwitcher.tsx
10
13
  import { useEffect as useEffect2, useRef as useRef2, useState as useState2, useMemo } from "react";
@@ -463,14 +466,17 @@ var Deuteranopia_default = SvgDeuteranopia;
463
466
  // src/react/ThemeSwitcherPortal.tsx
464
467
  import { useEffect, useRef, useState } from "react";
465
468
  import { createPortal } from "react-dom";
466
- var PORTAL_ID = "theme-switcher-portal";
467
469
  var portalEl = null;
468
470
  var activeOwner = null;
469
471
  function ensurePortal() {
470
472
  if (typeof document === "undefined") throw new Error("No document");
471
473
  if (portalEl && document.body.contains(portalEl)) return portalEl;
472
- const existing = document.getElementById(PORTAL_ID);
473
- portalEl = existing ?? Object.assign(document.createElement("div"), { id: PORTAL_ID });
474
+ const existing = document.getElementById(
475
+ THEME_SWITCHER_PORTAL_ID
476
+ );
477
+ portalEl = existing ?? Object.assign(document.createElement("div"), {
478
+ id: THEME_SWITCHER_PORTAL_ID
479
+ });
474
480
  if (!existing) document.body.appendChild(portalEl);
475
481
  return portalEl;
476
482
  }
@@ -502,7 +508,7 @@ var THEME_ICON = {
502
508
  deuteranopia: Deuteranopia_default,
503
509
  tritanopia: Tritanopia_default
504
510
  };
505
- function ThemeSwitcher({ options }) {
511
+ function ThemeSwitcher({ options, position }) {
506
512
  const { theme, updateTheme, language, updateLanguage } = useTheme();
507
513
  const [hovered, setHovered] = useState2(null);
508
514
  const list = useMemo(
@@ -511,6 +517,8 @@ function ThemeSwitcher({ options }) {
511
517
  );
512
518
  const [isOpen, setIsOpen] = useState2(false);
513
519
  const wrapperRef = useRef2(null);
520
+ const switcherClass = SWITCHER_POSITION[position ?? "right-bottom"];
521
+ const switcherMenuClass = SWITCHER_MENU_POSITION[position ?? "right-bottom"];
514
522
  useEffect2(() => {
515
523
  const handleClickOutside = (event) => {
516
524
  if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
@@ -532,7 +540,9 @@ function ThemeSwitcher({ options }) {
532
540
  "aria-haspopup": "menu",
533
541
  "aria-expanded": isOpen,
534
542
  onClick: toggle,
535
- className: "fixed right-[25px] bottom-[25px] w-[60px] h-[60px] p-[10px] bg-[#ffffff] rounded-full flex justify-center items-center shadow-[0_0_3px_0_rgba(0,0,0,0.17)]",
543
+ className: `fixed w-[60px] h-[60px] p-[10px] bg-[#ffffff] rounded-full flex justify-center items-center shadow-[0_0_3px_0_rgba(0,0,0,0.17)]
544
+ ${switcherClass}
545
+ `,
536
546
  children: isOpen ? /* @__PURE__ */ jsx9(X_default, { className: "self-center" }) : /* @__PURE__ */ jsx9(Logo_default, { className: "self-center" })
537
547
  }
538
548
  ),
@@ -541,7 +551,10 @@ function ThemeSwitcher({ options }) {
541
551
  {
542
552
  role: "menu",
543
553
  "aria-label": "Select theme",
544
- className: "fixed bottom-[100px] right-[25px] flex-col bg-[#ffffff] rounded-[18px] w-[220px] gap-[11px] filter drop-shadow-[0_0_1.3px_rgba(0,0,0,0.25)]",
554
+ className: `
555
+ fixed flex-col bg-[#ffffff] rounded-[18px] w-[220px] gap-[11px] filter drop-shadow-[0_0_1.3px_rgba(0,0,0,0.25)]
556
+ ${switcherMenuClass}
557
+ `,
545
558
  children: [
546
559
  /* @__PURE__ */ jsx9("div", { children: list.map((opt) => {
547
560
  const Icon = THEME_ICON[opt.key];
package/dist/devtools.cjs CHANGED
@@ -24,15 +24,31 @@ __export(devtools_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(devtools_exports);
26
26
 
27
- // src/core/constants/simulation.ts
27
+ // src/core/constants/key.ts
28
+ var SimulationStorageKey = "colbrush-filter";
29
+ var VISION_PORTAL_ID = "cb-vision-portal";
28
30
  var FILTER_ID = "cb-vision-filter";
29
31
  var FILTER_WRAPPER_ID = "cb-vision-filter-root";
32
+
33
+ // src/core/constants/position.ts
34
+ var Position = [
35
+ "left-bottom",
36
+ "right-bottom",
37
+ "left-top",
38
+ "right-top"
39
+ ];
40
+ var TOOLBAR_POSITION = {
41
+ "left-bottom": "left-[16px] bottom-[16px]",
42
+ "right-bottom": "right-[16px] bottom-[16px]",
43
+ "left-top": "left-[16px] top-[16px]",
44
+ "right-top": "right-[16px] top-[16px]"
45
+ };
46
+
47
+ // src/core/constants/simulation.ts
30
48
  var DEFAULT_OPTIONS = {
31
49
  defaultMode: "none",
32
- paramKey: "vision",
33
- storageKey: "colbrush:vision",
34
- toolbarPosition: "left-bottom",
35
- hotkey: true,
50
+ storageKey: SimulationStorageKey,
51
+ position: Position[0],
36
52
  allowInProd: false
37
53
  };
38
54
  var MATRICES = {
@@ -66,12 +82,17 @@ function getMatrixForMode(mode) {
66
82
  return MATRICES[mode];
67
83
  }
68
84
 
69
- // src/devtools/vision/modes.ts
85
+ // src/core/constants/modes.ts
70
86
  var SIMULATION_MODES = [
71
87
  "deuteranopia",
72
88
  "protanopia",
73
89
  "tritanopia"
74
90
  ];
91
+ var THEME_MODES = [
92
+ "default",
93
+ ...SIMULATION_MODES
94
+ ];
95
+ var VISION_MODES = ["none", ...SIMULATION_MODES];
75
96
  var MODE_LABELS = {
76
97
  English: {
77
98
  none: "default",
@@ -109,15 +130,16 @@ var useTheme = () => (0, import_react.useContext)(ThemeContext);
109
130
  // src/react/VisionPortal.tsx
110
131
  var import_react2 = require("react");
111
132
  var import_react_dom = require("react-dom");
112
- var PORTAL_ID = "cb-vision-portal";
113
133
  var portalEl = null;
114
134
  var activeOwner = null;
115
135
  function ensurePortal() {
116
136
  if (typeof document === "undefined")
117
137
  throw new Error("No document available");
118
138
  if (portalEl && document.body.contains(portalEl)) return portalEl;
119
- const existing = document.getElementById(PORTAL_ID);
120
- portalEl = existing ?? Object.assign(document.createElement("div"), { id: PORTAL_ID });
139
+ const existing = document.getElementById(
140
+ VISION_PORTAL_ID
141
+ );
142
+ portalEl = existing ?? Object.assign(document.createElement("div"), { id: VISION_PORTAL_ID });
121
143
  if (!existing) document.body.appendChild(portalEl);
122
144
  return portalEl;
123
145
  }
@@ -156,15 +178,11 @@ function VisionFilterPortal({
156
178
 
157
179
  // src/react/SimulationFilter.tsx
158
180
  var import_jsx_runtime2 = require("react/jsx-runtime");
159
- var import_meta = {};
160
181
  var originalFilterMap = /* @__PURE__ */ new WeakMap();
161
182
  var IS_DEV = (() => {
162
183
  if (typeof process !== "undefined" && true) {
163
184
  return true;
164
185
  }
165
- if (typeof import_meta !== "undefined" && typeof import_meta.env?.MODE === "string") {
166
- return import_meta.env.MODE !== "production";
167
- }
168
186
  if (typeof window !== "undefined") {
169
187
  const host = window.location.hostname;
170
188
  return host === "localhost" || host === "127.0.0.1";
@@ -176,27 +194,26 @@ function resolveOptions(props) {
176
194
  const merged = {
177
195
  ...DEFAULT_OPTIONS,
178
196
  ...options,
179
- toolbarPosition: options.toolbarPosition ?? DEFAULT_OPTIONS.toolbarPosition,
180
- hotkey: options.hotkey ?? DEFAULT_OPTIONS.hotkey,
197
+ storageKey: options.storageKey ?? DEFAULT_OPTIONS.storageKey,
198
+ position: options.position ?? DEFAULT_OPTIONS.position,
181
199
  allowInProd: options.allowInProd ?? DEFAULT_OPTIONS.allowInProd
182
200
  };
183
201
  return { config: merged, visible };
184
202
  }
185
203
  function SimulationFilter(props) {
186
204
  const { config, visible } = resolveOptions(props);
187
- const { toolbarPosition, allowInProd } = config;
205
+ const { position, allowInProd, defaultMode, storageKey } = config;
188
206
  const [open, setOpen] = (0, import_react3.useState)(false);
189
207
  const { simulationFilter, setSimulationFilter, language } = useTheme();
208
+ const initialized = (0, import_react3.useRef)(false);
190
209
  if (!visible) return null;
191
210
  if (!allowInProd && !IS_DEV) return null;
192
- const ANCHOR_CLASSES = {
193
- "left-bottom": "left-[16px] bottom-[16px]",
194
- "right-bottom": "right-[16px] bottom-[16px]",
195
- "left-top": "left-[16px] top-[16px]",
196
- "right-top": "right-[16px] top-[16px]"
197
- };
198
211
  const MODES = ["none", ...SIMULATION_MODES];
199
- const anchorClass = ANCHOR_CLASSES[toolbarPosition] ?? ANCHOR_CLASSES["left-bottom"];
212
+ const toolBarClass = TOOLBAR_POSITION[position] ?? TOOLBAR_POSITION["left-bottom"];
213
+ const updateSimulationFilter = (value) => {
214
+ setSimulationFilter(value);
215
+ localStorage.setItem(SimulationStorageKey, value);
216
+ };
200
217
  (0, import_react3.useEffect)(() => {
201
218
  if (typeof document === "undefined") return;
202
219
  const resolveTarget = () => {
@@ -206,7 +223,7 @@ function SimulationFilter(props) {
206
223
  document.body?.children ?? []
207
224
  );
208
225
  const fallback = bodyChildren.find(
209
- (child) => child.id !== PORTAL_ID
226
+ (child) => child.id !== VISION_PORTAL_ID
210
227
  );
211
228
  return fallback ?? document.body;
212
229
  };
@@ -261,7 +278,9 @@ function SimulationFilter(props) {
261
278
  "feColorMatrix",
262
279
  {
263
280
  type: "matrix",
264
- values: getMatrixForMode(simulationFilter)
281
+ values: getMatrixForMode(
282
+ simulationFilter
283
+ )
265
284
  }
266
285
  ) }) })
267
286
  }
@@ -269,7 +288,7 @@ function SimulationFilter(props) {
269
288
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
270
289
  "div",
271
290
  {
272
- className: `cb-vision-toolbar h-[36px] fixed z-[100] inline-flex items-center gap-[6px] rounded-[10px] bg-[rgba(17,17,17,0.85)] p-[6px_8px] text-[12px] text-white opacity-90 shadow-[0_6px_18px_rgba(0,0,0,0.25)] backdrop-blur-[6px] ${anchorClass}`,
291
+ className: `cb-vision-toolbar h-[36px] fixed z-[100] inline-flex items-center gap-[6px] rounded-[10px] bg-[rgba(17,17,17,0.85)] p-[6px_8px] text-[12px] text-white opacity-90 shadow-[0_6px_18px_rgba(0,0,0,0.25)] backdrop-blur-[6px] ${toolBarClass}`,
273
292
  children: [
274
293
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
275
294
  "span",
@@ -284,7 +303,7 @@ function SimulationFilter(props) {
284
303
  {
285
304
  type: "button",
286
305
  className: `rounded-[6px] px-[6px] py-[3px] ${simulationFilter === value ? "bg-white text-black" : "hover:bg-[rgba(255,255,255,0.2)]"}`,
287
- onClick: () => setSimulationFilter(value),
306
+ onClick: () => updateSimulationFilter(value),
288
307
  children: MODE_LABELS[language][value] ?? value
289
308
  },
290
309
  value
@@ -1,22 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
-
3
- type VisionMode = 'deuteranopia' | 'protanopia' | 'tritanopia' | 'none';
4
- interface VisionOptions {
5
- /** 기본 모드. 기본값 'none' */
6
- defaultMode?: VisionMode;
7
- /** URL 토글 파라미터 키 (예: ?vision=deut), 기본값 'vision' */
8
- paramKey?: string;
9
- /** localStorage 키, 기본값 'colbrush:vision' */
10
- storageKey?: string;
11
- /** 개발 호스트 허용(정규식) — 기본: localhost/127/192.168.x */
12
- devHostPattern?: RegExp;
13
- /** 툴바 위치, 기본 'left-bottom' */
14
- toolbarPosition?: 'left-bottom' | 'right-bottom' | 'left-top' | 'right-top';
15
- /** 단축키 활성 여부, 기본 true (⌘/Ctrl + Alt + D) */
16
- hotkey?: boolean;
17
- /** 프로덕션에서도 강제로 허용(디버깅용). 기본 false */
18
- allowInProd?: boolean;
19
- }
2
+ import { a as VisionOptions } from './simulationTypes-CenFTH7t.cjs';
20
3
 
21
4
  type SimulationFilterProps = VisionOptions & {
22
5
  visible?: boolean;
@@ -1,22 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
-
3
- type VisionMode = 'deuteranopia' | 'protanopia' | 'tritanopia' | 'none';
4
- interface VisionOptions {
5
- /** 기본 모드. 기본값 'none' */
6
- defaultMode?: VisionMode;
7
- /** URL 토글 파라미터 키 (예: ?vision=deut), 기본값 'vision' */
8
- paramKey?: string;
9
- /** localStorage 키, 기본값 'colbrush:vision' */
10
- storageKey?: string;
11
- /** 개발 호스트 허용(정규식) — 기본: localhost/127/192.168.x */
12
- devHostPattern?: RegExp;
13
- /** 툴바 위치, 기본 'left-bottom' */
14
- toolbarPosition?: 'left-bottom' | 'right-bottom' | 'left-top' | 'right-top';
15
- /** 단축키 활성 여부, 기본 true (⌘/Ctrl + Alt + D) */
16
- hotkey?: boolean;
17
- /** 프로덕션에서도 강제로 허용(디버깅용). 기본 false */
18
- allowInProd?: boolean;
19
- }
2
+ import { a as VisionOptions } from './simulationTypes-CenFTH7t.js';
20
3
 
21
4
  type SimulationFilterProps = VisionOptions & {
22
5
  visible?: boolean;
package/dist/devtools.js CHANGED
@@ -1,16 +1,20 @@
1
1
  import {
2
+ FILTER_ID,
3
+ FILTER_WRAPPER_ID,
4
+ MODE_LABELS,
5
+ Position,
6
+ SIMULATION_MODES,
7
+ SimulationStorageKey,
8
+ TOOLBAR_POSITION,
9
+ VISION_PORTAL_ID,
2
10
  useTheme
3
- } from "./chunk-EFIJAMPH.js";
11
+ } from "./chunk-WQEJ7FU2.js";
4
12
 
5
13
  // src/core/constants/simulation.ts
6
- var FILTER_ID = "cb-vision-filter";
7
- var FILTER_WRAPPER_ID = "cb-vision-filter-root";
8
14
  var DEFAULT_OPTIONS = {
9
15
  defaultMode: "none",
10
- paramKey: "vision",
11
- storageKey: "colbrush:vision",
12
- toolbarPosition: "left-bottom",
13
- hotkey: true,
16
+ storageKey: SimulationStorageKey,
17
+ position: Position[0],
14
18
  allowInProd: false
15
19
  };
16
20
  var MATRICES = {
@@ -44,42 +48,22 @@ function getMatrixForMode(mode) {
44
48
  return MATRICES[mode];
45
49
  }
46
50
 
47
- // src/devtools/vision/modes.ts
48
- var SIMULATION_MODES = [
49
- "deuteranopia",
50
- "protanopia",
51
- "tritanopia"
52
- ];
53
- var MODE_LABELS = {
54
- English: {
55
- none: "default",
56
- protanopia: "protanopia",
57
- deuteranopia: "deuteranopia",
58
- tritanopia: "tritanopia"
59
- },
60
- Korean: {
61
- none: "\uAEBC\uC9D0",
62
- protanopia: "\uC801\uC0C9\uB9F9",
63
- deuteranopia: "\uB179\uC0C9\uB9F9",
64
- tritanopia: "\uCCAD\uC0C9\uB9F9"
65
- }
66
- };
67
-
68
51
  // src/react/SimulationFilter.tsx
69
- import { useEffect as useEffect2, useState as useState2 } from "react";
52
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
70
53
 
71
54
  // src/react/VisionPortal.tsx
72
55
  import { useEffect, useRef, useState } from "react";
73
56
  import { createPortal } from "react-dom";
74
- var PORTAL_ID = "cb-vision-portal";
75
57
  var portalEl = null;
76
58
  var activeOwner = null;
77
59
  function ensurePortal() {
78
60
  if (typeof document === "undefined")
79
61
  throw new Error("No document available");
80
62
  if (portalEl && document.body.contains(portalEl)) return portalEl;
81
- const existing = document.getElementById(PORTAL_ID);
82
- portalEl = existing ?? Object.assign(document.createElement("div"), { id: PORTAL_ID });
63
+ const existing = document.getElementById(
64
+ VISION_PORTAL_ID
65
+ );
66
+ portalEl = existing ?? Object.assign(document.createElement("div"), { id: VISION_PORTAL_ID });
83
67
  if (!existing) document.body.appendChild(portalEl);
84
68
  return portalEl;
85
69
  }
@@ -123,9 +107,6 @@ var IS_DEV = (() => {
123
107
  if (typeof process !== "undefined" && true) {
124
108
  return true;
125
109
  }
126
- if (typeof import.meta !== "undefined" && typeof import.meta.env?.MODE === "string") {
127
- return import.meta.env.MODE !== "production";
128
- }
129
110
  if (typeof window !== "undefined") {
130
111
  const host = window.location.hostname;
131
112
  return host === "localhost" || host === "127.0.0.1";
@@ -137,27 +118,26 @@ function resolveOptions(props) {
137
118
  const merged = {
138
119
  ...DEFAULT_OPTIONS,
139
120
  ...options,
140
- toolbarPosition: options.toolbarPosition ?? DEFAULT_OPTIONS.toolbarPosition,
141
- hotkey: options.hotkey ?? DEFAULT_OPTIONS.hotkey,
121
+ storageKey: options.storageKey ?? DEFAULT_OPTIONS.storageKey,
122
+ position: options.position ?? DEFAULT_OPTIONS.position,
142
123
  allowInProd: options.allowInProd ?? DEFAULT_OPTIONS.allowInProd
143
124
  };
144
125
  return { config: merged, visible };
145
126
  }
146
127
  function SimulationFilter(props) {
147
128
  const { config, visible } = resolveOptions(props);
148
- const { toolbarPosition, allowInProd } = config;
129
+ const { position, allowInProd, defaultMode, storageKey } = config;
149
130
  const [open, setOpen] = useState2(false);
150
131
  const { simulationFilter, setSimulationFilter, language } = useTheme();
132
+ const initialized = useRef2(false);
151
133
  if (!visible) return null;
152
134
  if (!allowInProd && !IS_DEV) return null;
153
- const ANCHOR_CLASSES = {
154
- "left-bottom": "left-[16px] bottom-[16px]",
155
- "right-bottom": "right-[16px] bottom-[16px]",
156
- "left-top": "left-[16px] top-[16px]",
157
- "right-top": "right-[16px] top-[16px]"
158
- };
159
135
  const MODES = ["none", ...SIMULATION_MODES];
160
- const anchorClass = ANCHOR_CLASSES[toolbarPosition] ?? ANCHOR_CLASSES["left-bottom"];
136
+ const toolBarClass = TOOLBAR_POSITION[position] ?? TOOLBAR_POSITION["left-bottom"];
137
+ const updateSimulationFilter = (value) => {
138
+ setSimulationFilter(value);
139
+ localStorage.setItem(SimulationStorageKey, value);
140
+ };
161
141
  useEffect2(() => {
162
142
  if (typeof document === "undefined") return;
163
143
  const resolveTarget = () => {
@@ -167,7 +147,7 @@ function SimulationFilter(props) {
167
147
  document.body?.children ?? []
168
148
  );
169
149
  const fallback = bodyChildren.find(
170
- (child) => child.id !== PORTAL_ID
150
+ (child) => child.id !== VISION_PORTAL_ID
171
151
  );
172
152
  return fallback ?? document.body;
173
153
  };
@@ -222,7 +202,9 @@ function SimulationFilter(props) {
222
202
  "feColorMatrix",
223
203
  {
224
204
  type: "matrix",
225
- values: getMatrixForMode(simulationFilter)
205
+ values: getMatrixForMode(
206
+ simulationFilter
207
+ )
226
208
  }
227
209
  ) }) })
228
210
  }
@@ -230,7 +212,7 @@ function SimulationFilter(props) {
230
212
  /* @__PURE__ */ jsxs(
231
213
  "div",
232
214
  {
233
- className: `cb-vision-toolbar h-[36px] fixed z-[100] inline-flex items-center gap-[6px] rounded-[10px] bg-[rgba(17,17,17,0.85)] p-[6px_8px] text-[12px] text-white opacity-90 shadow-[0_6px_18px_rgba(0,0,0,0.25)] backdrop-blur-[6px] ${anchorClass}`,
215
+ className: `cb-vision-toolbar h-[36px] fixed z-[100] inline-flex items-center gap-[6px] rounded-[10px] bg-[rgba(17,17,17,0.85)] p-[6px_8px] text-[12px] text-white opacity-90 shadow-[0_6px_18px_rgba(0,0,0,0.25)] backdrop-blur-[6px] ${toolBarClass}`,
234
216
  children: [
235
217
  /* @__PURE__ */ jsx(
236
218
  "span",
@@ -245,7 +227,7 @@ function SimulationFilter(props) {
245
227
  {
246
228
  type: "button",
247
229
  className: `rounded-[6px] px-[6px] py-[3px] ${simulationFilter === value ? "bg-white text-black" : "hover:bg-[rgba(255,255,255,0.2)]"}`,
248
- onClick: () => setSimulationFilter(value),
230
+ onClick: () => updateSimulationFilter(value),
249
231
  children: MODE_LABELS[language][value] ?? value
250
232
  },
251
233
  value
@@ -0,0 +1,21 @@
1
+ type TPosition = 'left-bottom' | 'right-bottom' | 'left-top' | 'right-top';
2
+
3
+ type Vision = 'protanopia' | 'deuteranopia' | 'tritanopia';
4
+ type VisionMode = Vision | 'none';
5
+ type TThemeKey = Vision | 'default';
6
+
7
+ interface VisionOptions {
8
+ /** 기본 모드. 기본값 'none' */
9
+ defaultMode?: VisionMode;
10
+ /** localStorage 키, 기본값 'colbrush-filter' */
11
+ storageKey?: string;
12
+ /** 개발 호스트 허용(정규식) — 기본: localhost/127/192.168.x */
13
+ devHostPattern?: RegExp;
14
+ /** 툴바 위치, 기본 'left-bottom' */
15
+ position?: TPosition;
16
+ /** 프로덕션에서도 강제로 허용(디버깅용). 기본 false */
17
+ allowInProd?: boolean;
18
+ }
19
+ type Position = NonNullable<VisionOptions['position']>;
20
+
21
+ export type { Position as P, TThemeKey as T, VisionMode as V, VisionOptions as a };
@@ -0,0 +1,21 @@
1
+ type TPosition = 'left-bottom' | 'right-bottom' | 'left-top' | 'right-top';
2
+
3
+ type Vision = 'protanopia' | 'deuteranopia' | 'tritanopia';
4
+ type VisionMode = Vision | 'none';
5
+ type TThemeKey = Vision | 'default';
6
+
7
+ interface VisionOptions {
8
+ /** 기본 모드. 기본값 'none' */
9
+ defaultMode?: VisionMode;
10
+ /** localStorage 키, 기본값 'colbrush-filter' */
11
+ storageKey?: string;
12
+ /** 개발 호스트 허용(정규식) — 기본: localhost/127/192.168.x */
13
+ devHostPattern?: RegExp;
14
+ /** 툴바 위치, 기본 'left-bottom' */
15
+ position?: TPosition;
16
+ /** 프로덕션에서도 강제로 허용(디버깅용). 기본 false */
17
+ allowInProd?: boolean;
18
+ }
19
+ type Position = NonNullable<VisionOptions['position']>;
20
+
21
+ export type { Position as P, TThemeKey as T, VisionMode as V, VisionOptions as a };
package/dist/styles.css CHANGED
@@ -1 +1 @@
1
- @source "./src/**/*.{js,ts,jsx,tsx}";svg rect{fill:none!important}
1
+ @source "./src/**/*.{js,ts,jsx,tsx}";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "colbrush",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "A React theme switching library that makes it easy to apply color-blind accessible UI themes",
5
5
  "homepage": "https://colbrush.vercel.app",
6
6
  "repository": {
@@ -53,7 +53,6 @@
53
53
  "dependencies": {
54
54
  "chokidar": "^3.6.0",
55
55
  "chroma-js": "^3.1.2",
56
- "color-blind": "^0.1.3",
57
56
  "colorjs.io": "^0.5.2",
58
57
  "postcss": "^8.5.6",
59
58
  "postcss-safe-parser": "^7.0.1"
@@ -1,104 +0,0 @@
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
- };