| #!/usr/bin/env node | ||
| export {}; |
+79
| #!/usr/bin/env node | ||
| import { Command } from 'commander'; | ||
| import { toDarkModeColor } from './utils/toDarkModeColor.js'; | ||
| import { toFilterColor } from './utils/toFilterColor.js'; | ||
| const program = new Command(); | ||
| program | ||
| .command('toDark <colors...>') | ||
| .description('Convert hex colors to dark mode equivalent colors.') | ||
| .action((colors) => { | ||
| colors.forEach(color => { | ||
| try { | ||
| const darkModeColor = toDarkModeColor(color); | ||
| console.log(`#${darkModeColor}`); | ||
| } | ||
| catch (error) { | ||
| const message = error instanceof Error ? error.message : 'An unknown error occurred'; | ||
| console.error(`Error processing color "${color}": ${message}`); | ||
| } | ||
| }); | ||
| }); | ||
| program | ||
| .command('toFilter <colors...>') | ||
| .description('Calculate the CSS filter to match given hex colors.') | ||
| .action((colors) => { | ||
| colors.forEach((color, index) => { | ||
| try { | ||
| if (index > 0) | ||
| console.log('---'); // Add a separator | ||
| console.log(`Input Color: #${color}`); | ||
| const { filter, loss } = toFilterColor(color); | ||
| console.log(`Filter: ${filter}`); | ||
| console.log(`Loss: ${loss.toFixed(2)}`); | ||
| if (loss > 10) { | ||
| console.warn('Warning: The color is somewhat off. The result may not be perfect.'); | ||
| } | ||
| } | ||
| catch (error) { | ||
| const message = error instanceof Error ? error.message : 'An unknown error occurred'; | ||
| console.error(`Error processing color "${color}": ${message}`); | ||
| } | ||
| }); | ||
| }); | ||
| program | ||
| .command('toDarkFilter <colors...>') | ||
| .description('Pipeline to convert a color to dark mode and then to a CSS filter.') | ||
| .action((colors) => { | ||
| colors.forEach((color, index) => { | ||
| try { | ||
| if (index > 0) | ||
| console.log('---'); // Add a separator | ||
| console.log(`Input Color: #${color}`); | ||
| // Calculate filter for original color (Light Filter) | ||
| const { filter: lightFilter, loss: lightLoss } = toFilterColor(color); | ||
| console.log(`Light Filter: ${lightFilter}`); | ||
| console.log(`Light Filter Loss: ${lightLoss.toFixed(2)}`); | ||
| if (lightLoss > 10) { | ||
| console.warn('Warning (Light Filter): The color is somewhat off. The result may not be perfect.'); | ||
| } | ||
| // Convert to dark mode | ||
| const darkModeColor = toDarkModeColor(color); | ||
| console.log(`Dark Mode Color: #${darkModeColor}`); | ||
| // Calculate filter for dark mode color (Dark Filter) | ||
| const { filter: darkFilter, loss: darkLoss } = toFilterColor(darkModeColor); | ||
| console.log(`Dark Filter: ${darkFilter}`); | ||
| console.log(`Dark Filter Loss: ${darkLoss.toFixed(2)}`); | ||
| if (darkLoss > 10) { | ||
| console.warn('Warning (Dark Filter): The color is somewhat off. The result may not be perfect.'); | ||
| } | ||
| } | ||
| catch (error) { | ||
| const message = error instanceof Error ? error.message : 'An unknown error occurred'; | ||
| console.error(`Error processing color "${color}": ${message}`); | ||
| } | ||
| }); | ||
| }); | ||
| program.parse(process.argv); | ||
| if (!program.args.length) { | ||
| program.help(); | ||
| } |
| export declare const TRANSITION_CLASS = "theme-transition"; | ||
| export declare const TRANSITION_STYLE_ID = "theme-transition-style"; |
| export const TRANSITION_CLASS = "theme-transition"; | ||
| export const TRANSITION_STYLE_ID = "theme-transition-style"; |
| import React from "react"; | ||
| import { Theme } from "../types"; | ||
| interface ThemeContextType { | ||
| theme: Theme; | ||
| setTheme: (theme: Theme) => void; | ||
| effectiveTheme: "light" | "dark"; | ||
| isHydrated: boolean; | ||
| } | ||
| export declare function ThemeProvider({ children }: { | ||
| children: React.ReactNode; | ||
| }): import("react/jsx-runtime").JSX.Element; | ||
| export declare function useTheme(): ThemeContextType; | ||
| export {}; |
| import { jsx as _jsx } from "react/jsx-runtime"; | ||
| import { createContext, useContext, useState } from "react"; | ||
| import { useEffectiveTheme } from "./useEffectiveTheme"; | ||
| import { useThemeDOMEffect } from "./useThemeDOMEffect"; | ||
| import { useThemeHydration } from "./useThemeHydration"; | ||
| import { useThemeState } from "./useThemeState"; | ||
| import { useThemeTransitionStyle } from "./useThemeTransitionStyle"; | ||
| const ThemeContext = createContext(undefined); | ||
| export function ThemeProvider({ children }) { | ||
| const { theme, setTheme, setThemeState } = useThemeState(); | ||
| const [effectiveTheme, setEffectiveTheme] = useState("light"); | ||
| const [isHydrated, setIsHydrated] = useState(false); | ||
| useThemeHydration(setThemeState, setEffectiveTheme, setIsHydrated); | ||
| useThemeTransitionStyle(); | ||
| useEffectiveTheme(theme, isHydrated, setEffectiveTheme); | ||
| useThemeDOMEffect(theme, effectiveTheme, isHydrated); | ||
| return (_jsx(ThemeContext.Provider, { value: { theme, setTheme, effectiveTheme, isHydrated }, children: children })); | ||
| } | ||
| export function useTheme() { | ||
| const context = useContext(ThemeContext); | ||
| if (context === undefined) { | ||
| throw new Error("useTheme must be used within a ThemeProvider"); | ||
| } | ||
| return context; | ||
| } |
| export declare function createThemeScript(): string; |
| // FOUC 방지를 위한 인라인 스크립트 (Neato Theme) | ||
| // 이 함수는 컴포넌트가 아닌 순수 문자열 생성용입니다. | ||
| export function createThemeScript() { | ||
| return ` | ||
| (function () { | ||
| try { | ||
| var theme = localStorage.getItem('theme'); | ||
| var isDark = false; | ||
| if (theme === 'dark') { | ||
| isDark = true; | ||
| } else if (theme === 'light') { | ||
| isDark = false; | ||
| } else { | ||
| // 시스템 테마 사용 (theme이 null이거나 'system'인 경우) | ||
| isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | ||
| } | ||
| if (isDark) { | ||
| document.documentElement.classList.add('dark'); | ||
| } | ||
| } catch (e) { | ||
| // 에러가 발생하면 시스템 테마 사용 | ||
| try { | ||
| if (window.matchMedia('(prefers-color-scheme: dark)').matches) { | ||
| document.documentElement.classList.add('dark'); | ||
| } | ||
| } catch (e2) { | ||
| // 완전히 실패하면 아무것도 하지 않음 | ||
| } | ||
| } | ||
| })(); | ||
| `.trim(); | ||
| } |
| import { Theme } from "../types"; | ||
| export declare function useEffectiveTheme(theme: Theme, isHydrated: boolean, setEffectiveTheme: (theme: "light" | "dark") => void): void; |
| import { useEffect } from "react"; | ||
| export function useEffectiveTheme(theme, isHydrated, setEffectiveTheme) { | ||
| useEffect(() => { | ||
| if (!isHydrated) | ||
| return; | ||
| const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); | ||
| const updateEffectiveTheme = () => { | ||
| if (theme === "system") { | ||
| setEffectiveTheme(mediaQuery.matches ? "dark" : "light"); | ||
| } | ||
| else { | ||
| setEffectiveTheme(theme); | ||
| } | ||
| }; | ||
| // 초기 설정 | ||
| updateEffectiveTheme(); | ||
| // 시스템 테마 변경 감지 | ||
| const handler = () => { | ||
| if (theme === "system") { | ||
| updateEffectiveTheme(); | ||
| } | ||
| }; | ||
| mediaQuery.addEventListener("change", handler); | ||
| return () => mediaQuery.removeEventListener("change", handler); | ||
| }, [theme, isHydrated]); | ||
| } |
| import { Theme } from "../types"; | ||
| export declare function useThemeDOMEffect(theme: Theme, effectiveTheme: "light" | "dark", isHydrated: boolean): void; |
| import { useEffect } from "react"; | ||
| export function useThemeDOMEffect(theme, effectiveTheme, isHydrated) { | ||
| useEffect(() => { | ||
| if (!isHydrated) | ||
| return; | ||
| // 현재 DOM 상태 확인 | ||
| const htmlElement = document.documentElement; | ||
| const currentlyDark = htmlElement.classList.contains("dark"); | ||
| const shouldBeDark = effectiveTheme === "dark"; | ||
| // 상태가 다를 때만 변경 | ||
| if (shouldBeDark && !currentlyDark) { | ||
| htmlElement.classList.add("dark"); | ||
| } | ||
| else if (!shouldBeDark && currentlyDark) { | ||
| htmlElement.classList.remove("dark"); | ||
| } | ||
| // localStorage에 저장 | ||
| if (theme === "system") { | ||
| localStorage.removeItem("theme"); | ||
| } | ||
| else { | ||
| localStorage.setItem("theme", theme); | ||
| } | ||
| }, [theme, effectiveTheme, isHydrated]); | ||
| } |
| import { Theme } from "../types"; | ||
| export declare function useThemeHydration(setTheme: (theme: Theme) => void, setEffectiveTheme: (theme: "light" | "dark") => void, setIsHydrated: (isHydrated: boolean) => void): void; |
| import { useEffect } from "react"; | ||
| export function useThemeHydration(setTheme, setEffectiveTheme, setIsHydrated) { | ||
| useEffect(() => { | ||
| setIsHydrated(true); | ||
| // localStorage에서 테마 값 읽기 | ||
| const saved = localStorage.getItem("theme"); | ||
| if (saved === "light" || saved === "dark" || saved === "system") { | ||
| setTheme(saved); | ||
| } | ||
| // 현재 DOM 상태를 기준으로 초기 effectiveTheme 설정 | ||
| const currentlyDark = document.documentElement.classList.contains("dark"); | ||
| setEffectiveTheme(currentlyDark ? "dark" : "light"); | ||
| }, []); | ||
| } |
| import { Theme } from "../types"; | ||
| export declare function useThemeState(): { | ||
| theme: Theme; | ||
| setTheme: (newTheme: Theme) => void; | ||
| setThemeState: import("react").Dispatch<import("react").SetStateAction<Theme>>; | ||
| }; |
| import { useState, useRef } from "react"; | ||
| import { TRANSITION_CLASS } from "./constants"; | ||
| export function useThemeState() { | ||
| const [theme, setThemeState] = useState("system"); | ||
| const transitionTimeoutRef = useRef(null); | ||
| const setTheme = (newTheme) => { | ||
| try { | ||
| const html = document.documentElement; | ||
| // transition 클래스 추가 | ||
| html.classList.add(TRANSITION_CLASS); | ||
| // 이전 타이머가 있으면 제거 | ||
| if (transitionTimeoutRef.current) { | ||
| window.clearTimeout(transitionTimeoutRef.current); | ||
| } | ||
| // 250ms 이후에 transition 클래스 제거 | ||
| transitionTimeoutRef.current = window.setTimeout(() => { | ||
| html.classList.remove(TRANSITION_CLASS); | ||
| transitionTimeoutRef.current = null; | ||
| }, 250); | ||
| } | ||
| catch (e) { | ||
| // DOM 접근 실패 시 그냥 계속 | ||
| } | ||
| setThemeState(newTheme); | ||
| }; | ||
| return { theme, setTheme, setThemeState }; | ||
| } |
| export declare function useThemeTransitionStyle(): void; |
| import { useEffect } from "react"; | ||
| import { TRANSITION_CLASS, TRANSITION_STYLE_ID } from "./constants"; | ||
| export function useThemeTransitionStyle() { | ||
| useEffect(() => { | ||
| try { | ||
| if (typeof document === "undefined") | ||
| return; | ||
| if (!document.getElementById(TRANSITION_STYLE_ID)) { | ||
| const style = document.createElement("style"); | ||
| style.id = TRANSITION_STYLE_ID; | ||
| // 최소한의 안전한 속성만 전환합니다. 너무 많은 속성을 전환하면 성능에 영향이 있습니다. | ||
| style.innerHTML = `html.${TRANSITION_CLASS}, html.${TRANSITION_CLASS} * { transition: background-color 200ms linear, color 200ms linear, border-color 200ms linear, box-shadow 200ms linear, fill 200ms linear, stroke 200ms linear !important; }`; | ||
| document.head.appendChild(style); | ||
| } | ||
| } | ||
| catch (e) { | ||
| // 삽입 실패 시 무시 | ||
| } | ||
| }, []); | ||
| } |
| export declare function toDarkModeColor(hex: string): string; |
| function hexToHsl(hex) { | ||
| let r = 0, g = 0, b = 0; | ||
| if (hex.startsWith("#")) | ||
| hex = hex.slice(1); | ||
| if (hex.length === 3) { | ||
| r = parseInt(hex[0] + hex[0], 16); | ||
| g = parseInt(hex[1] + hex[1], 16); | ||
| b = parseInt(hex[2] + hex[2], 16); | ||
| } | ||
| else if (hex.length === 6) { | ||
| r = parseInt(hex.slice(0, 2), 16); | ||
| g = parseInt(hex.slice(2, 4), 16); | ||
| b = parseInt(hex.slice(4, 6), 16); | ||
| } | ||
| r /= 255; | ||
| g /= 255; | ||
| b /= 255; | ||
| const max = Math.max(r, g, b), min = Math.min(r, g, b); | ||
| let h = 0, s = 0, l = (max + min) / 2; | ||
| if (max !== min) { | ||
| const d = max - min; | ||
| s = l > 0.5 ? d / (2 - max - min) : d / (max + min); | ||
| switch (max) { | ||
| case r: | ||
| h = (g - b) / d + (g < b ? 6 : 0); | ||
| break; | ||
| case g: | ||
| h = (b - r) / d + 2; | ||
| break; | ||
| case b: | ||
| h = (r - g) / d + 4; | ||
| break; | ||
| } | ||
| h /= 6; | ||
| } | ||
| return [h * 360, s * 100, l * 100]; | ||
| } | ||
| function hslToHex(h, s, l) { | ||
| s /= 100; | ||
| l /= 100; | ||
| const c = (1 - Math.abs(2 * l - 1)) * s; | ||
| const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); | ||
| const m = l - c / 2; | ||
| let r = 0, g = 0, b = 0; | ||
| if (h < 60) { | ||
| r = c; | ||
| g = x; | ||
| b = 0; | ||
| } | ||
| else if (h < 120) { | ||
| r = x; | ||
| g = c; | ||
| b = 0; | ||
| } | ||
| else if (h < 180) { | ||
| r = 0; | ||
| g = c; | ||
| b = x; | ||
| } | ||
| else if (h < 240) { | ||
| r = 0; | ||
| g = x; | ||
| b = c; | ||
| } | ||
| else if (h < 300) { | ||
| r = x; | ||
| g = 0; | ||
| b = c; | ||
| } | ||
| else { | ||
| r = c; | ||
| g = 0; | ||
| b = x; | ||
| } | ||
| r = Math.round((r + m) * 255); | ||
| g = Math.round((g + m) * 255); | ||
| b = Math.round((b + m) * 255); | ||
| return [r, g, b].map(x => x.toString(16).padStart(2, "0")).join(""); | ||
| } | ||
| export function toDarkModeColor(hex) { | ||
| let [h, s, l] = hexToHsl(hex); | ||
| // 채도 줄이기 | ||
| s = Math.max(0, s - 15); | ||
| // 명도 반전 + 보정 | ||
| l = 100 - l; | ||
| if (l < 20) | ||
| l = 20; // 너무 어둡지 않게 | ||
| if (l > 80) | ||
| l = 80; // 너무 밝지 않게 | ||
| return hslToHex(h, s, l); | ||
| } |
| export declare function toFilterColor(hex: string): { | ||
| filter: string; | ||
| loss: number; | ||
| }; |
| /* | ||
| This script is based on the work of Barrett Sonntag, | ||
| who published it on GitHub under an MIT license. | ||
| Original source: https://github.com/element-plus/element-plus/blob/dev/internal/build/src/tasks/color.ts | ||
| */ | ||
| class Color { | ||
| constructor(r, g, b) { | ||
| this.set(r, g, b); | ||
| } | ||
| toString() { | ||
| return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; | ||
| } | ||
| set(r, g, b) { | ||
| this.r = this.clamp(r); | ||
| this.g = this.clamp(g); | ||
| this.b = this.clamp(b); | ||
| } | ||
| hueRotate(angle = 0) { | ||
| angle = (angle / 180) * Math.PI; | ||
| const sin = Math.sin(angle); | ||
| const cos = Math.cos(angle); | ||
| this.multiply([ | ||
| 0.213 + cos * 0.787 - sin * 0.213, | ||
| 0.715 - cos * 0.715 - sin * 0.715, | ||
| 0.072 - cos * 0.072 + sin * 0.928, | ||
| 0.213 - cos * 0.213 + sin * 0.143, | ||
| 0.715 + cos * 0.285 + sin * 0.14, | ||
| 0.072 - cos * 0.072 - sin * 0.283, | ||
| 0.213 - cos * 0.213 - sin * 0.787, | ||
| 0.715 - cos * 0.715 + sin * 0.715, | ||
| 0.072 + cos * 0.928 + sin * 0.072, | ||
| ]); | ||
| } | ||
| grayscale(value = 1) { | ||
| this.multiply([ | ||
| 0.2126 + 0.7874 * (1 - value), | ||
| 0.7152 - 0.7152 * (1 - value), | ||
| 0.0722 - 0.0722 * (1 - value), | ||
| 0.2126 - 0.2126 * (1 - value), | ||
| 0.7152 + 0.2848 * (1 - value), | ||
| 0.0722 - 0.0722 * (1 - value), | ||
| 0.2126 - 0.2126 * (1 - value), | ||
| 0.7152 - 0.7152 * (1 - value), | ||
| 0.0722 + 0.9278 * (1 - value), | ||
| ]); | ||
| } | ||
| sepia(value = 1) { | ||
| this.multiply([ | ||
| 0.393 + 0.607 * (1 - value), | ||
| 0.769 - 0.769 * (1 - value), | ||
| 0.189 - 0.189 * (1 - value), | ||
| 0.349 - 0.349 * (1 - value), | ||
| 0.686 + 0.314 * (1 - value), | ||
| 0.168 - 0.168 * (1 - value), | ||
| 0.272 - 0.272 * (1 - value), | ||
| 0.534 - 0.534 * (1 - value), | ||
| 0.131 + 0.869 * (1 - value), | ||
| ]); | ||
| } | ||
| saturate(value = 1) { | ||
| this.multiply([ | ||
| 0.213 + 0.787 * value, | ||
| 0.715 - 0.715 * value, | ||
| 0.072 - 0.072 * value, | ||
| 0.213 - 0.213 * value, | ||
| 0.715 + 0.285 * value, | ||
| 0.072 - 0.072 * value, | ||
| 0.213 - 0.213 * value, | ||
| 0.715 - 0.715 * value, | ||
| 0.072 + 0.928 * value, | ||
| ]); | ||
| } | ||
| multiply(matrix) { | ||
| const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]); | ||
| const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]); | ||
| const newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]); | ||
| this.r = newR; | ||
| this.g = newG; | ||
| this.b = newB; | ||
| } | ||
| brightness(value = 1) { | ||
| this.linear(value); | ||
| } | ||
| contrast(value = 1) { | ||
| this.linear(value, -(0.5 * value) + 0.5); | ||
| } | ||
| linear(slope = 1, intercept = 0) { | ||
| this.r = this.clamp(this.r * slope + intercept * 255); | ||
| this.g = this.clamp(this.g * slope + intercept * 255); | ||
| this.b = this.clamp(this.b * slope + intercept * 255); | ||
| } | ||
| invert(value = 1) { | ||
| this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255); | ||
| this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255); | ||
| this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255); | ||
| } | ||
| hsl() { | ||
| const r = this.r / 255; | ||
| const g = this.g / 255; | ||
| const b = this.b / 255; | ||
| const max = Math.max(r, g, b); | ||
| const min = Math.min(r, g, b); | ||
| let h = 0, s = 0; | ||
| const l = (max + min) / 2; | ||
| if (max !== min) { | ||
| const d = max - min; | ||
| s = l > 0.5 ? d / (2 - max - min) : d / (max + min); | ||
| switch (max) { | ||
| case r: | ||
| h = (g - b) / d + (g < b ? 6 : 0); | ||
| break; | ||
| case g: | ||
| h = (b - r) / d + 2; | ||
| break; | ||
| case b: | ||
| h = (r - g) / d + 4; | ||
| break; | ||
| } | ||
| h /= 6; | ||
| } | ||
| return { | ||
| h: h * 100, | ||
| s: s * 100, | ||
| l: l * 100, | ||
| }; | ||
| } | ||
| clamp(value) { | ||
| if (value > 255) { | ||
| return 255; | ||
| } | ||
| if (value < 0) { | ||
| return 0; | ||
| } | ||
| return value; | ||
| } | ||
| } | ||
| class Solver { | ||
| constructor(target) { | ||
| this.target = target; | ||
| this.targetHSL = target.hsl(); | ||
| this.reusedColor = new Color(0, 0, 0); | ||
| } | ||
| solve() { | ||
| let bestResult = { | ||
| values: [], | ||
| loss: Infinity, | ||
| filter: '', | ||
| }; | ||
| let attempts = 0; | ||
| while (bestResult.loss >= 3.0) { | ||
| const wideResult = this.solveWide(); | ||
| const narrowResult = this.solveNarrow(wideResult); | ||
| const currentResult = { | ||
| values: narrowResult.values, | ||
| loss: narrowResult.loss, | ||
| filter: this.css(narrowResult.values), | ||
| }; | ||
| if (currentResult.loss < bestResult.loss) { | ||
| bestResult = currentResult; | ||
| } | ||
| attempts++; | ||
| } | ||
| return bestResult; | ||
| } | ||
| solveWide() { | ||
| const A = 5; | ||
| const c = 15; | ||
| const a = [60, 180, 18000, 600, 1.2, 1.2]; | ||
| let best = { loss: Infinity, values: [] }; | ||
| for (let i = 0; best.loss > 25 && i < 3; i++) { | ||
| const initial = [50, 20, 3750, 50, 100, 100]; | ||
| const result = this.spsa(A, a, c, initial, 1000); | ||
| if (result.loss < best.loss) { | ||
| best = result; | ||
| } | ||
| } | ||
| return best; | ||
| } | ||
| solveNarrow(wide) { | ||
| const A = wide.loss; | ||
| const c = 2; | ||
| const A1 = A + 1; | ||
| const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1]; | ||
| return this.spsa(A, a, c, wide.values, 500); | ||
| } | ||
| spsa(A, a, c, values, iters) { | ||
| const alpha = 1; | ||
| const gamma = 0.16666666666666666; | ||
| const best = values.slice(0); | ||
| let bestLoss = this.loss(values); | ||
| const deltas = new Array(6); | ||
| const highArgs = new Array(6); | ||
| const lowArgs = new Array(6); | ||
| for (let k = 0; k < iters; k++) { | ||
| const ck = c / Math.pow(k + 1, gamma); | ||
| for (let i = 0; i < 6; i++) { | ||
| deltas[i] = Math.random() > 0.5 ? 1 : -1; | ||
| highArgs[i] = values[i] + ck * deltas[i]; | ||
| lowArgs[i] = values[i] - ck * deltas[i]; | ||
| } | ||
| const lossDiff = this.loss(highArgs) - this.loss(lowArgs); | ||
| for (let i = 0; i < 6; i++) { | ||
| const g = (lossDiff / (2 * ck)) * deltas[i]; | ||
| const ak = a[i] / Math.pow(A + k + 1, alpha); | ||
| values[i] = fix(values[i] - ak * g, i); | ||
| } | ||
| const loss = this.loss(values); | ||
| if (loss < bestLoss) { | ||
| best.splice(0, best.length, ...values); | ||
| bestLoss = loss; | ||
| } | ||
| } | ||
| return { values: best, loss: bestLoss }; | ||
| function fix(value, idx) { | ||
| let max = 100; | ||
| if (idx === 2) { // saturate | ||
| max = 7500; | ||
| } | ||
| else if (idx === 4 || idx === 5) { // brightness || contrast | ||
| max = 200; | ||
| } | ||
| if (idx === 3) { // hue-rotate | ||
| if (value > max) { | ||
| value %= max; | ||
| } | ||
| else if (value < 0) { | ||
| value = max + (value % max); | ||
| } | ||
| } | ||
| else if (value < 0) { | ||
| value = 0; | ||
| } | ||
| else if (value > max) { | ||
| value = max; | ||
| } | ||
| return value; | ||
| } | ||
| } | ||
| loss(filters) { | ||
| const color = this.reusedColor; | ||
| color.set(0, 0, 0); | ||
| color.invert(filters[0] / 100); | ||
| color.sepia(filters[1] / 100); | ||
| color.saturate(filters[2] / 100); | ||
| color.hueRotate(filters[3] * 3.6); | ||
| color.brightness(filters[4] / 100); | ||
| color.contrast(filters[5] / 100); | ||
| const colorHSL = color.hsl(); | ||
| return (Math.abs(color.r - this.target.r) + | ||
| Math.abs(color.g - this.target.g) + | ||
| Math.abs(color.b - this.target.b) + | ||
| Math.abs(colorHSL.h - this.targetHSL.h) + | ||
| Math.abs(colorHSL.s - this.targetHSL.s) + | ||
| Math.abs(colorHSL.l - this.targetHSL.l)); | ||
| } | ||
| css(filters) { | ||
| const fmt = (idx, multiplier = 1) => Math.round(filters[idx] * multiplier); | ||
| return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`; | ||
| } | ||
| } | ||
| function hexToRgb(hex) { | ||
| hex = hex.replace(/^#/, ''); | ||
| if (hex.length === 3) { | ||
| hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; | ||
| } | ||
| const num = parseInt(hex, 16); | ||
| if (isNaN(num) || hex.length !== 6) { | ||
| return null; | ||
| } | ||
| return [(num >> 16), (num >> 8) & 0xFF, num & 0xFF]; | ||
| } | ||
| export function toFilterColor(hex) { | ||
| const rgb = hexToRgb(hex); | ||
| if (rgb === null) { | ||
| throw new Error('Invalid hex color format'); | ||
| } | ||
| const color = new Color(rgb[0], rgb[1], rgb[2]); | ||
| const solver = new Solver(color); | ||
| const result = solver.solve(); | ||
| return { | ||
| filter: result.filter, | ||
| loss: result.loss, | ||
| }; | ||
| } |
+1
-0
@@ -14,1 +14,2 @@ export type VariantProps = Record<string, string | number | boolean | undefined>; | ||
| } | ||
| export type Theme = "light" | "dark" | "system"; |
+7
-7
| { | ||
| "name": "neato", | ||
| "version": "0.0.15", | ||
| "version": "0.0.16", | ||
| "description": "A powerful utility library for efficient CSS class management in React applications", | ||
@@ -36,8 +36,8 @@ "keywords": [ | ||
| "./theme": { | ||
| "import": "./dist/theme.js", | ||
| "types": "./dist/theme.d.ts" | ||
| "import": "./dist/theme/index.js", | ||
| "types": "./dist/theme/index.d.ts" | ||
| }, | ||
| "./theme-script": { | ||
| "import": "./dist/theme-script.js", | ||
| "types": "./dist/theme-script.d.ts" | ||
| "./theme/script": { | ||
| "import": "./dist/theme/theme-script.js", | ||
| "types": "./dist/theme/theme-script.d.ts" | ||
| } | ||
@@ -50,3 +50,3 @@ }, | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "build": "rm -rf dist && tsc", | ||
| "dev": "tsc --watch" | ||
@@ -53,0 +53,0 @@ }, |
| export declare function createNeatoThemeScript(): string; |
| // FOUC 방지를 위한 인라인 스크립트 (Neato Theme) | ||
| // 이 함수는 컴포넌트가 아닌 순수 문자열 생성용입니다. | ||
| export function createNeatoThemeScript() { | ||
| return ` | ||
| (function () { | ||
| try { | ||
| var theme = localStorage.getItem('theme'); | ||
| var isDark = false; | ||
| if (theme === 'dark') { | ||
| isDark = true; | ||
| } else if (theme === 'light') { | ||
| isDark = false; | ||
| } else { | ||
| // 시스템 테마 사용 (theme이 null이거나 'system'인 경우) | ||
| isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | ||
| } | ||
| if (isDark) { | ||
| document.documentElement.classList.add('dark'); | ||
| } | ||
| } catch (e) { | ||
| // 에러가 발생하면 시스템 테마 사용 | ||
| try { | ||
| if (window.matchMedia('(prefers-color-scheme: dark)').matches) { | ||
| document.documentElement.classList.add('dark'); | ||
| } | ||
| } catch (e2) { | ||
| // 완전히 실패하면 아무것도 하지 않음 | ||
| } | ||
| } | ||
| })(); | ||
| `.trim(); | ||
| } |
| import React from "react"; | ||
| export type NeatoTheme = "light" | "dark" | "system"; | ||
| interface NeatoThemeContextType { | ||
| theme: NeatoTheme; | ||
| setTheme: (theme: NeatoTheme) => void; | ||
| effectiveTheme: "light" | "dark"; | ||
| isHydrated: boolean; | ||
| } | ||
| export declare function NeatoThemeProvider({ children }: { | ||
| children: React.ReactNode; | ||
| }): import("react/jsx-runtime").JSX.Element; | ||
| export declare function useNeatoTheme(): NeatoThemeContextType; | ||
| export {}; |
| "use client"; | ||
| import { jsx as _jsx } from "react/jsx-runtime"; | ||
| import { createContext, useContext, useEffect, useState } from "react"; | ||
| const NeatoThemeContext = createContext(undefined); | ||
| export function NeatoThemeProvider({ children }) { | ||
| const [theme, setTheme] = useState("system"); | ||
| const [effectiveTheme, setEffectiveTheme] = useState("light"); | ||
| const [isHydrated, setIsHydrated] = useState(false); | ||
| // 클라이언트 하이드레이션 완료 후 실행 | ||
| useEffect(() => { | ||
| setIsHydrated(true); | ||
| // localStorage에서 테마 값 읽기 | ||
| const saved = localStorage.getItem("theme"); | ||
| if (saved === "light" || saved === "dark" || saved === "system") { | ||
| setTheme(saved); | ||
| } | ||
| // 현재 DOM 상태를 기준으로 초기 effectiveTheme 설정 | ||
| const currentlyDark = document.documentElement.classList.contains("dark"); | ||
| setEffectiveTheme(currentlyDark ? "dark" : "light"); | ||
| }, []); // 시스템 다크모드 감지 및 테마 적용 | ||
| useEffect(() => { | ||
| if (!isHydrated) | ||
| return; | ||
| const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); | ||
| const updateEffectiveTheme = () => { | ||
| if (theme === "system") { | ||
| setEffectiveTheme(mediaQuery.matches ? "dark" : "light"); | ||
| } | ||
| else { | ||
| setEffectiveTheme(theme); | ||
| } | ||
| }; | ||
| // 초기 설정 | ||
| updateEffectiveTheme(); | ||
| // 시스템 테마 변경 감지 | ||
| const handler = () => { | ||
| if (theme === "system") { | ||
| updateEffectiveTheme(); | ||
| } | ||
| }; | ||
| mediaQuery.addEventListener("change", handler); | ||
| return () => mediaQuery.removeEventListener("change", handler); | ||
| }, [theme, isHydrated]); | ||
| // DOM에 다크모드 클래스 적용 | ||
| useEffect(() => { | ||
| if (!isHydrated) | ||
| return; | ||
| // 현재 DOM 상태 확인 | ||
| const htmlElement = document.documentElement; | ||
| const currentlyDark = htmlElement.classList.contains("dark"); | ||
| const shouldBeDark = effectiveTheme === "dark"; | ||
| // 상태가 다를 때만 변경 | ||
| if (shouldBeDark && !currentlyDark) { | ||
| htmlElement.classList.add("dark"); | ||
| } | ||
| else if (!shouldBeDark && currentlyDark) { | ||
| htmlElement.classList.remove("dark"); | ||
| } | ||
| // localStorage에 저장 | ||
| if (theme === "system") { | ||
| localStorage.removeItem("theme"); | ||
| } | ||
| else { | ||
| localStorage.setItem("theme", theme); | ||
| } | ||
| }, [theme, effectiveTheme, isHydrated]); | ||
| return (_jsx(NeatoThemeContext.Provider, { value: { theme, setTheme, effectiveTheme, isHydrated }, children: children })); | ||
| } | ||
| export function useNeatoTheme() { | ||
| const context = useContext(NeatoThemeContext); | ||
| if (context === undefined) { | ||
| throw new Error("useNeatoTheme must be used within a NeatoThemeProvider"); | ||
| } | ||
| return context; | ||
| } |
| export type ClassValue = string | number | boolean | undefined | null | ClassValue[] | Record<string, boolean | undefined | null>; | ||
| export declare function mergeClasses(...inputs: ClassValue[]): string; | ||
| export declare function resolveTailwindConflicts(classNames: string): string; | ||
| /** @deprecated Use resolveTailwindConflicts instead */ | ||
| export declare function getBorderConflictKey(className: string): string; |
-509
| export function mergeClasses(...inputs) { | ||
| const classes = []; | ||
| for (const input of inputs) { | ||
| if (!input) | ||
| continue; | ||
| if (typeof input === 'string' || typeof input === 'number') { | ||
| classes.push(String(input)); | ||
| } | ||
| else if (Array.isArray(input)) { | ||
| const result = mergeClasses(...input); | ||
| if (result) | ||
| classes.push(result); | ||
| } | ||
| else if (typeof input === 'object') { | ||
| for (const [key, value] of Object.entries(input)) { | ||
| if (value) | ||
| classes.push(key); | ||
| } | ||
| } | ||
| } | ||
| return classes.join(' '); | ||
| } | ||
| // 더 정확한 충돌 그룹 정의 - 계층적 구조로 관리 | ||
| const TAILWIND_CONFLICT_GROUPS = { | ||
| // Spacing - Padding | ||
| padding: { | ||
| all: /^p-/, | ||
| x: /^px-/, | ||
| y: /^py-/, | ||
| top: /^pt-/, | ||
| right: /^pr-/, | ||
| bottom: /^pb-/, | ||
| left: /^pl-/, | ||
| start: /^ps-/, | ||
| end: /^pe-/, | ||
| }, | ||
| // Spacing - Margin | ||
| margin: { | ||
| all: /^m-/, | ||
| x: /^mx-/, | ||
| y: /^my-/, | ||
| top: /^mt-/, | ||
| right: /^mr-/, | ||
| bottom: /^mb-/, | ||
| left: /^ml-/, | ||
| start: /^ms-/, | ||
| end: /^me-/, | ||
| }, | ||
| // Spacing - Space and Gap | ||
| space: { | ||
| x: /^space-x-/, | ||
| y: /^space-y-/, | ||
| }, | ||
| gap: { | ||
| all: /^gap-/, | ||
| x: /^gap-x-/, | ||
| y: /^gap-y-/, | ||
| }, | ||
| // Sizing | ||
| width: /^w-/, | ||
| height: /^h-/, | ||
| minWidth: /^min-w-/, | ||
| minHeight: /^min-h-/, | ||
| maxWidth: /^max-w-/, | ||
| maxHeight: /^max-h-/, | ||
| size: /^size-/, | ||
| // Typography | ||
| fontSize: /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/, | ||
| textColor: /^text-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| fontWeight: /^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/, | ||
| fontFamily: /^font-(sans|serif|mono)$/, | ||
| fontStyle: /^(italic|not-italic)$/, | ||
| textDecoration: /^(underline|overline|line-through|no-underline)$/, | ||
| textAlign: /^text-(left|center|right|justify|start|end)$/, | ||
| textTransform: /^(uppercase|lowercase|capitalize|normal-case)$/, | ||
| textOverflow: /^(truncate|text-ellipsis|text-clip)$/, | ||
| textWrap: /^text-(wrap|nowrap|balance|pretty)$/, | ||
| textIndent: /^indent-/, | ||
| lineHeight: /^leading-/, | ||
| letterSpacing: /^tracking-/, | ||
| // Background | ||
| backgroundColor: /^bg-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| backgroundImage: /^bg-(none|gradient-to-[trbl]|gradient-to-[trbl][trbl])$/, | ||
| backgroundPosition: /^bg-(bottom|center|left|left-bottom|left-top|right|right-bottom|right-top|top)$/, | ||
| backgroundRepeat: /^bg-(repeat|no-repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/, | ||
| backgroundSize: /^bg-(auto|cover|contain)$/, | ||
| backgroundAttachment: /^bg-(fixed|local|scroll)$/, | ||
| backgroundClip: /^bg-clip-(border|padding|content|text)$/, | ||
| backgroundOrigin: /^bg-origin-(border|padding|content)$/, | ||
| // Layout - Display | ||
| display: /^(block|inline-block|inline|flex|inline-flex|table|inline-table|table-caption|table-cell|table-column|table-column-group|table-footer-group|table-header-group|table-row-group|table-row|flow-root|grid|inline-grid|contents|list-item|hidden)$/, | ||
| // Layout - Position | ||
| position: /^(static|fixed|absolute|relative|sticky)$/, | ||
| // Layout - Inset | ||
| inset: { | ||
| all: /^inset-/, | ||
| x: /^inset-x-/, | ||
| y: /^inset-y-/, | ||
| top: /^top-/, | ||
| right: /^right-/, | ||
| bottom: /^bottom-/, | ||
| left: /^left-/, | ||
| start: /^start-/, | ||
| end: /^end-/, | ||
| }, | ||
| // Layout - Overflow | ||
| overflow: /^overflow-(auto|hidden|clip|visible|scroll)$/, | ||
| overflowX: /^overflow-x-(auto|hidden|clip|visible|scroll)$/, | ||
| overflowY: /^overflow-y-(auto|hidden|clip|visible|scroll)$/, | ||
| // Layout - Z-index | ||
| zIndex: /^z-/, | ||
| // Layout - Visibility | ||
| visibility: /^(visible|invisible|collapse)$/, | ||
| // Flexbox | ||
| flexDirection: /^flex-(row|row-reverse|col|col-reverse)$/, | ||
| flexWrap: /^flex-(wrap|wrap-reverse|nowrap)$/, | ||
| flex: /^flex-(1|auto|initial|none)$/, | ||
| flexGrow: /^grow(-0)?$/, | ||
| flexShrink: /^shrink(-0)?$/, | ||
| flexBasis: /^basis-/, | ||
| // Flexbox & Grid - Alignment | ||
| justifyContent: /^justify-(normal|start|end|center|between|around|evenly|stretch)$/, | ||
| justifyItems: /^justify-items-(start|end|center|stretch)$/, | ||
| justifySelf: /^justify-self-(auto|start|end|center|stretch)$/, | ||
| alignContent: /^content-(normal|center|start|end|between|around|evenly|baseline|stretch)$/, | ||
| alignItems: /^items-(start|end|center|baseline|stretch)$/, | ||
| alignSelf: /^self-(auto|start|end|center|stretch|baseline)$/, | ||
| placeContent: /^place-content-(center|start|end|between|around|evenly|baseline|stretch)$/, | ||
| placeItems: /^place-items-(start|end|center|baseline|stretch)$/, | ||
| placeSelf: /^place-self-(auto|start|end|center|stretch)$/, | ||
| // Grid | ||
| gridTemplateColumns: /^grid-cols-/, | ||
| gridTemplateRows: /^grid-rows-/, | ||
| gridColumn: /^col-(auto|span-\d+|start-\d+|end-\d+)$/, | ||
| gridRow: /^row-(auto|span-\d+|start-\d+|end-\d+)$/, | ||
| gridAutoFlow: /^grid-flow-(row|col|dense|row-dense|col-dense)$/, | ||
| gridAutoColumns: /^auto-cols-/, | ||
| gridAutoRows: /^auto-rows-/, | ||
| // Borders - 세분화된 구조 (속성별로 분리) | ||
| borderWidth: { | ||
| all: /^border(-\d+)?$/, | ||
| x: /^border-x(-\d+)?$/, | ||
| y: /^border-y(-\d+)?$/, | ||
| top: /^border-t(-\d+)?$/, | ||
| right: /^border-r(-\d+)?$/, | ||
| bottom: /^border-b(-\d+)?$/, | ||
| left: /^border-l(-\d+)?$/, | ||
| start: /^border-s(-\d+)?$/, | ||
| end: /^border-e(-\d+)?$/, | ||
| }, | ||
| borderStyle: { | ||
| all: /^border-(solid|dashed|dotted|double|hidden|none)$/, | ||
| x: /^border-x-(solid|dashed|dotted|double|hidden|none)$/, | ||
| y: /^border-y-(solid|dashed|dotted|double|hidden|none)$/, | ||
| top: /^border-t-(solid|dashed|dotted|double|hidden|none)$/, | ||
| right: /^border-r-(solid|dashed|dotted|double|hidden|none)$/, | ||
| bottom: /^border-b-(solid|dashed|dotted|double|hidden|none)$/, | ||
| left: /^border-l-(solid|dashed|dotted|double|hidden|none)$/, | ||
| start: /^border-s-(solid|dashed|dotted|double|hidden|none)$/, | ||
| end: /^border-e-(solid|dashed|dotted|double|hidden|none)$/, | ||
| }, | ||
| borderColor: { | ||
| all: /^border-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| x: /^border-x-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| y: /^border-y-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| top: /^border-t-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| right: /^border-r-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| bottom: /^border-b-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| left: /^border-l-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| start: /^border-s-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| end: /^border-e-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| }, | ||
| // Border Radius | ||
| borderRadius: { | ||
| all: /^rounded(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| top: /^rounded-t(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| right: /^rounded-r(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| bottom: /^rounded-b(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| left: /^rounded-l(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| start: /^rounded-s(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| end: /^rounded-e(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| topLeft: /^rounded-tl(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| topRight: /^rounded-tr(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| bottomRight: /^rounded-br(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| bottomLeft: /^rounded-bl(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| startStart: /^rounded-ss(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| startEnd: /^rounded-se(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| endEnd: /^rounded-ee(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| endStart: /^rounded-es(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/, | ||
| }, | ||
| // Outline | ||
| outlineWidth: /^outline(-\d+)?$/, | ||
| outlineStyle: /^outline-(none|solid|dashed|dotted|double)$/, | ||
| outlineColor: /^outline-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| outlineOffset: /^outline-offset-/, | ||
| // Ring | ||
| ringWidth: /^ring(-\d+)?$/, | ||
| ringColor: /^ring-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| ringOpacity: /^ring-opacity-/, | ||
| ringOffsetWidth: /^ring-offset-/, | ||
| ringOffsetColor: /^ring-offset-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| // Effects | ||
| boxShadow: /^shadow(-sm|-md|-lg|-xl|-2xl|-inner|-none)?$/, | ||
| boxShadowColor: /^shadow-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?$/, | ||
| opacity: /^opacity-/, | ||
| mixBlendMode: /^mix-blend-/, | ||
| backgroundBlendMode: /^bg-blend-/, | ||
| // Transforms | ||
| scale: /^scale-/, | ||
| scaleX: /^scale-x-/, | ||
| scaleY: /^scale-y-/, | ||
| rotate: /^-?rotate-/, | ||
| translateX: /^-?translate-x-/, | ||
| translateY: /^-?translate-y-/, | ||
| skewX: /^-?skew-x-/, | ||
| skewY: /^-?skew-y-/, | ||
| transformOrigin: /^origin-/, | ||
| // Transitions & Animations | ||
| transitionProperty: /^transition(-none|-all|-colors|-opacity|-shadow|-transform)?$/, | ||
| transitionDuration: /^duration-/, | ||
| transitionTimingFunction: /^ease-(linear|in|out|in-out)$/, | ||
| transitionDelay: /^delay-/, | ||
| animation: /^animate-/, | ||
| // Filters | ||
| blur: /^blur(-none|-sm|-md|-lg|-xl|-2xl|-3xl)?$/, | ||
| brightness: /^brightness-/, | ||
| contrast: /^contrast-/, | ||
| dropShadow: /^drop-shadow(-sm|-md|-lg|-xl|-2xl|-none)?$/, | ||
| grayscale: /^grayscale(-0)?$/, | ||
| hueRotate: /^-?hue-rotate-/, | ||
| invert: /^invert(-0)?$/, | ||
| saturate: /^saturate-/, | ||
| sepia: /^sepia(-0)?$/, | ||
| // Backdrop Filters | ||
| backdropBlur: /^backdrop-blur(-none|-sm|-md|-lg|-xl|-2xl|-3xl)?$/, | ||
| backdropBrightness: /^backdrop-brightness-/, | ||
| backdropContrast: /^backdrop-contrast-/, | ||
| backdropGrayscale: /^backdrop-grayscale(-0)?$/, | ||
| backdropHueRotate: /^-?backdrop-hue-rotate-/, | ||
| backdropInvert: /^backdrop-invert(-0)?$/, | ||
| backdropOpacity: /^backdrop-opacity-/, | ||
| backdropSaturate: /^backdrop-saturate-/, | ||
| backdropSepia: /^backdrop-sepia(-0)?$/, | ||
| // SVG | ||
| fill: /^fill-(none|current|(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?)$/, | ||
| stroke: /^stroke-(none|current|(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|inherit|current|transparent|black|white)(-\d+)?)$/, | ||
| strokeWidth: /^stroke-/, | ||
| // Interactivity | ||
| cursor: /^cursor-/, | ||
| pointerEvents: /^pointer-events-(none|auto)$/, | ||
| resize: /^resize(-none|-x|-y)?$/, | ||
| scrollBehavior: /^scroll-(auto|smooth)$/, | ||
| scrollMargin: /^scroll-m[xytblr]?-/, | ||
| scrollPadding: /^scroll-p[xytblr]?-/, | ||
| userSelect: /^select-(none|text|all|auto)$/, | ||
| willChange: /^will-change-/, | ||
| // Table | ||
| borderCollapse: /^border-(collapse|separate)$/, | ||
| borderSpacing: /^border-spacing-/, | ||
| tableLayout: /^table-(auto|fixed)$/, | ||
| captionSide: /^caption-(top|bottom)$/, | ||
| // Lists | ||
| listStyleType: /^list-(none|disc|decimal)$/, | ||
| listStylePosition: /^list-(inside|outside)$/, | ||
| listStyleImage: /^list-image-/, | ||
| // Columns | ||
| columns: /^columns-/, | ||
| columnSpan: /^col-span-/, | ||
| // Break | ||
| breakAfter: /^break-after-/, | ||
| breakBefore: /^break-before-/, | ||
| breakInside: /^break-inside-/, | ||
| // Box Decoration Break | ||
| boxDecorationBreak: /^box-decoration-(clone|slice)$/, | ||
| // Content | ||
| content: /^content-/, | ||
| }; | ||
| // 충돌 감지를 위한 헬퍼 함수들 | ||
| function getConflictKey(utility) { | ||
| // 중첩된 객체를 평탄화하여 검색 | ||
| function searchInGroup(group, path = '') { | ||
| if (group instanceof RegExp) { | ||
| return group.test(utility) ? path : null; | ||
| } | ||
| if (typeof group === 'object' && group !== null) { | ||
| for (const [key, value] of Object.entries(group)) { | ||
| const currentPath = path ? `${path}.${key}` : key; | ||
| const result = searchInGroup(value, currentPath); | ||
| if (result) | ||
| return result; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| // 모든 충돌 그룹을 순회하여 매칭되는 항목 찾기 | ||
| for (const [groupName, group] of Object.entries(TAILWIND_CONFLICT_GROUPS)) { | ||
| const result = searchInGroup(group, groupName); | ||
| if (result) | ||
| return result; | ||
| } | ||
| // 매칭되는 그룹이 없으면 유틸리티 자체를 키로 사용 | ||
| return utility; | ||
| } | ||
| // 특수한 케이스들을 처리하는 함수 | ||
| function handleSpecialCases(utility) { | ||
| // Arbitrary values: [값] 형태의 임의 값들 | ||
| const arbitraryMatch = utility.match(/^([a-z-]+)-\[.+\]$/); | ||
| if (arbitraryMatch) { | ||
| return getConflictKey(arbitraryMatch[1] + '-arbitrary'); | ||
| } | ||
| // Important modifier: !유틸리티 | ||
| if (utility.startsWith('!')) { | ||
| return '!' + getConflictKey(utility.slice(1)); | ||
| } | ||
| return getConflictKey(utility); | ||
| } | ||
| // Modifier별로 우선순위를 정하는 함수 | ||
| function getModifierPriority(modifiers) { | ||
| let priority = 0; | ||
| // 반응형 > 상태 > 기타 순으로 우선순위 부여 | ||
| for (const modifier of modifiers) { | ||
| if (['sm', 'md', 'lg', 'xl', '2xl'].includes(modifier)) { | ||
| priority += 1000; // 반응형이 가장 높은 우선순위 | ||
| } | ||
| else if (['hover', 'focus', 'active', 'visited', 'focus-within', 'focus-visible'].includes(modifier)) { | ||
| priority += 100; // 상태 modifier | ||
| } | ||
| else if (['dark', 'light'].includes(modifier)) { | ||
| priority += 50; // 테마 modifier | ||
| } | ||
| else if (['first', 'last', 'odd', 'even', 'group-hover', 'peer-hover'].includes(modifier)) { | ||
| priority += 10; // 기타 modifier | ||
| } | ||
| else { | ||
| priority += 1; // 알려지지 않은 modifier | ||
| } | ||
| } | ||
| return priority; | ||
| } | ||
| // 계층적 충돌 검사 함수 - 더 스마트한 충돌 감지 | ||
| function hasConflict(key1, key2) { | ||
| // 정확히 같은 키면 충돌 | ||
| if (key1 === key2) | ||
| return true; | ||
| // 계층적 충돌 검사 | ||
| const parts1 = key1.split('.'); | ||
| const parts2 = key2.split('.'); | ||
| // 최상위 그룹이 다르면 충돌하지 않음 | ||
| if (parts1[0] !== parts2[0]) | ||
| return false; | ||
| const groupName = parts1[0]; | ||
| // Padding의 경우: px는 left,right와 충돌, py는 top,bottom과 충돌 | ||
| if (groupName === 'padding') { | ||
| const subGroup1 = parts1[1]; | ||
| const subGroup2 = parts2[1]; | ||
| if (subGroup1 === 'all' || subGroup2 === 'all') | ||
| return true; | ||
| if ((subGroup1 === 'x' && ['left', 'right'].includes(subGroup2)) || | ||
| (subGroup2 === 'x' && ['left', 'right'].includes(subGroup1))) | ||
| return true; | ||
| if ((subGroup1 === 'y' && ['top', 'bottom'].includes(subGroup2)) || | ||
| (subGroup2 === 'y' && ['top', 'bottom'].includes(subGroup1))) | ||
| return true; | ||
| return subGroup1 === subGroup2; // 같은 방향이면 충돌 | ||
| } | ||
| // Margin도 동일한 로직 | ||
| if (groupName === 'margin') { | ||
| const subGroup1 = parts1[1]; | ||
| const subGroup2 = parts2[1]; | ||
| if (subGroup1 === 'all' || subGroup2 === 'all') | ||
| return true; | ||
| if ((subGroup1 === 'x' && ['left', 'right'].includes(subGroup2)) || | ||
| (subGroup2 === 'x' && ['left', 'right'].includes(subGroup1))) | ||
| return true; | ||
| if ((subGroup1 === 'y' && ['top', 'bottom'].includes(subGroup2)) || | ||
| (subGroup2 === 'y' && ['top', 'bottom'].includes(subGroup1))) | ||
| return true; | ||
| return subGroup1 === subGroup2; // 같은 방향이면 충돌 | ||
| } | ||
| // Inset도 동일한 로직 | ||
| if (groupName === 'inset') { | ||
| const subGroup1 = parts1[1]; | ||
| const subGroup2 = parts2[1]; | ||
| if (subGroup1 === 'all' || subGroup2 === 'all') | ||
| return true; | ||
| if ((subGroup1 === 'x' && ['left', 'right'].includes(subGroup2)) || | ||
| (subGroup2 === 'x' && ['left', 'right'].includes(subGroup1))) | ||
| return true; | ||
| if ((subGroup1 === 'y' && ['top', 'bottom'].includes(subGroup2)) || | ||
| (subGroup2 === 'y' && ['top', 'bottom'].includes(subGroup1))) | ||
| return true; | ||
| return subGroup1 === subGroup2; // 같은 방향이면 충돌 | ||
| } | ||
| // Border의 경우: 더 정교한 로직 | ||
| // 개발자 의도를 고려한 스마트 충돌 감지 | ||
| if (['borderWidth', 'borderStyle', 'borderColor'].includes(groupName)) { | ||
| const subGroup1 = parts1[1] || 'all'; | ||
| const subGroup2 = parts2[1] || 'all'; | ||
| // 둘 다 'all'이면 충돌 | ||
| if (subGroup1 === 'all' && subGroup2 === 'all') | ||
| return true; | ||
| // 하나는 specific direction, 하나는 'all'인 경우 | ||
| // 더 구체적인 것(specific direction)이 우선되도록 함 | ||
| if (subGroup1 === 'all' || subGroup2 === 'all') { | ||
| // 'all'은 덜 구체적이므로, specific한 것과는 충돌하지 않는 것으로 처리 | ||
| // 이렇게 하면 border-t-transparent와 border-blue-500가 공존할 수 있음 | ||
| return false; | ||
| } | ||
| // 둘 다 구체적인 방향이면서 축이 겹치는지 확인 | ||
| if ((subGroup1 === 'x' && ['left', 'right'].includes(subGroup2)) || | ||
| (subGroup2 === 'x' && ['left', 'right'].includes(subGroup1))) | ||
| return true; | ||
| if ((subGroup1 === 'y' && ['top', 'bottom'].includes(subGroup2)) || | ||
| (subGroup2 === 'y' && ['top', 'bottom'].includes(subGroup1))) | ||
| return true; | ||
| // 정확히 같은 방향이면 충돌 | ||
| return subGroup1 === subGroup2; | ||
| } | ||
| // Border Radius 처리 | ||
| if (groupName === 'borderRadius') { | ||
| const dir1 = parts1[1] || 'all'; | ||
| const dir2 = parts2[1] || 'all'; | ||
| if (dir1 === 'all' || dir2 === 'all') | ||
| return true; | ||
| // top과 topLeft/topRight의 충돌 등 | ||
| if ((dir1 === 'top' && ['topLeft', 'topRight'].includes(dir2)) || | ||
| (dir2 === 'top' && ['topLeft', 'topRight'].includes(dir1))) | ||
| return true; | ||
| if ((dir1 === 'bottom' && ['bottomLeft', 'bottomRight'].includes(dir2)) || | ||
| (dir2 === 'bottom' && ['bottomLeft', 'bottomRight'].includes(dir1))) | ||
| return true; | ||
| if ((dir1 === 'left' && ['topLeft', 'bottomLeft'].includes(dir2)) || | ||
| (dir2 === 'left' && ['topLeft', 'bottomLeft'].includes(dir1))) | ||
| return true; | ||
| if ((dir1 === 'right' && ['topRight', 'bottomRight'].includes(dir2)) || | ||
| (dir2 === 'right' && ['topRight', 'bottomRight'].includes(dir1))) | ||
| return true; | ||
| return dir1 === dir2; // 같은 방향이면 충돌 | ||
| } | ||
| // Gap 처리 | ||
| if (groupName === 'gap') { | ||
| const subGroup1 = parts1[1] || 'all'; | ||
| const subGroup2 = parts2[1] || 'all'; | ||
| if (subGroup1 === 'all' || subGroup2 === 'all') | ||
| return true; | ||
| return subGroup1 === subGroup2; // 같은 방향이면 충돌 | ||
| } | ||
| // Space 처리 | ||
| if (groupName === 'space') { | ||
| const subGroup1 = parts1[1] || 'all'; | ||
| const subGroup2 = parts2[1] || 'all'; | ||
| return subGroup1 === subGroup2; // x와 y는 서로 다른 축이므로 충돌하지 않음 | ||
| } | ||
| // 단일 속성들 (fontSize, textColor, backgroundColor 등)은 정확히 같은 키일 때만 충돌 | ||
| return false; | ||
| } | ||
| export function resolveTailwindConflicts(classNames) { | ||
| if (!classNames) | ||
| return ''; | ||
| const classes = classNames.split(/\s+/).filter(Boolean); | ||
| const result = []; | ||
| classes.forEach((className, index) => { | ||
| // modifier와 utility 분리 (예: "sm:hover:p-4" -> ["sm", "hover"], "p-4") | ||
| const parts = className.split(':'); | ||
| const utility = parts[parts.length - 1]; | ||
| const modifiers = parts.slice(0, -1); | ||
| // 충돌 키 생성 | ||
| const baseConflictKey = handleSpecialCases(utility); | ||
| const modifierPrefix = modifiers.join(':'); | ||
| const fullConflictKey = modifierPrefix ? `${modifierPrefix}:${baseConflictKey}` : baseConflictKey; | ||
| // 우선순위 계산 (modifier 우선순위 + 선언 순서) | ||
| const modifierPriority = getModifierPriority(modifiers); | ||
| const priority = modifierPriority + index; // 나중에 선언된 것이 더 높은 우선순위 | ||
| result.push({ className, conflictKey: fullConflictKey, modifiers, priority, index }); | ||
| }); | ||
| // 충돌 제거 | ||
| const finalResult = []; | ||
| for (const current of result) { | ||
| let shouldAdd = true; | ||
| // 이미 추가된 클래스들과 충돌 검사 | ||
| for (let i = finalResult.length - 1; i >= 0; i--) { | ||
| const existing = finalResult[i]; | ||
| if (hasConflict(current.conflictKey, existing.conflictKey)) { | ||
| if (current.priority > existing.priority) { | ||
| // 현재 것이 우선순위가 높으면 기존 것을 제거 | ||
| finalResult.splice(i, 1); | ||
| } | ||
| else { | ||
| // 기존 것이 우선순위가 높으면 현재 것을 추가하지 않음 | ||
| shouldAdd = false; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (shouldAdd) { | ||
| finalResult.push(current); | ||
| } | ||
| } | ||
| // 원래 순서를 유지하면서 반환 | ||
| return finalResult | ||
| .sort((a, b) => a.index - b.index) | ||
| .map(item => item.className) | ||
| .join(' '); | ||
| } | ||
| // 하위 호환성을 위한 레거시 함수 (deprecated) | ||
| /** @deprecated Use resolveTailwindConflicts instead */ | ||
| export function getBorderConflictKey(className) { | ||
| return getConflictKey(className); | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
32
100%740
3.79%44327
-10.8%