| 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); | ||
| } |
+1
-1
| { | ||
| "name": "neato", | ||
| "version": "0.0.14", | ||
| "version": "0.0.15", | ||
| "description": "A powerful utility library for efficient CSS class management in React applications", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
+80
-34
@@ -19,2 +19,3 @@ # neato | ||
| - 🌓 **테마 시스템** - Light/Dark/System 테마를 지원하는 완전한 테마 관리 | ||
| - 🎨 **색상 유틸리티** - 다크모드 색상 변환 및 CSS 필터 생성 CLI 도구 | ||
| - 🚀 **TypeScript 우선** - 완전한 타입 안전성과 IntelliSense | ||
@@ -28,13 +29,53 @@ - 📦 **경량** - 런타임 오버헤드 없이 최소 번들 크기 | ||
| npm install neato | ||
| yarn add neato | ||
| pnpm add neato | ||
| ``` | ||
| ## 🎨 CLI 색상 유틸리티 | ||
| neato는 색상 변환을 위한 강력한 CLI 도구를 포함하고 있습니다. | ||
| ### 다크모드 색상 변환 | ||
| 라이트모드 색상을 다크모드에 적합한 색상으로 자동 변환: | ||
| ```bash | ||
| yarn add neato | ||
| npx neato toDark 3b82f6 ef4444 10b981 | ||
| # 출력: | ||
| # #4d5fb8 | ||
| # #b83c3c | ||
| # #0d8a5f | ||
| ``` | ||
| ### CSS 필터 생성 | ||
| 원하는 색상을 구현하는 CSS 필터를 계산: | ||
| ```bash | ||
| pnpm add neato | ||
| npx neato toFilter 3b82f6 | ||
| # 출력: | ||
| # Input Color: #3b82f6 | ||
| # Filter: filter: invert(32%) sepia(77%) saturate(2815%) hue-rotate(217deg) brightness(101%) contrast(101%); | ||
| # Loss: 0.89 | ||
| ``` | ||
| ### 다크모드 + 필터 변환 파이프라인 | ||
| 색상을 다크모드로 변환한 후 CSS 필터까지 생성: | ||
| ```bash | ||
| npx neato toDarkFilter 3b82f6 | ||
| # 출력: | ||
| # Input Color: #3b82f6 | ||
| # Light Filter: filter: invert(32%) sepia(77%) saturate(2815%) hue-rotate(217deg) brightness(101%) contrast(101%); | ||
| # Light Filter Loss: 0.89 | ||
| # Dark Mode Color: #4d5fb8 | ||
| # Dark Filter: filter: invert(36%) sepia(41%) saturate(1042%) hue-rotate(211deg) brightness(95%) contrast(96%); | ||
| # Dark Filter Loss: 1.23 | ||
| ``` | ||
| 이러한 CLI 도구들은 디자인 시스템에서 일관된 색상 팔레트를 구축하고, 아이콘이나 SVG 요소에 동적 색상을 적용할 때 특히 유용합니다. | ||
| ``` | ||
| ## 🛠️ Tailwind CSS IntelliSense 연동 | ||
@@ -132,4 +173,4 @@ | ||
| ```typescript | ||
| import { NeatoThemeProvider } from 'neato/theme'; | ||
| import { createNeatoThemeScript } from 'neato/theme-script'; | ||
| import { ThemeProvider } from 'neato/theme'; | ||
| import { createThemeScript } from 'neato/theme-script'; | ||
@@ -139,5 +180,5 @@ // 1. 앱 최상단에 Provider 설정 | ||
| return ( | ||
| <NeatoThemeProvider> | ||
| <ThemeProvider> | ||
| <YourComponents /> | ||
| </NeatoThemeProvider> | ||
| </ThemeProvider> | ||
| ); | ||
@@ -153,3 +194,3 @@ } | ||
| dangerouslySetInnerHTML={{ | ||
| __html: createNeatoThemeScript() | ||
| __html: createThemeScript() | ||
| }} | ||
@@ -159,5 +200,5 @@ /> | ||
| <body> | ||
| <NeatoThemeProvider> | ||
| <ThemeProvider> | ||
| {children} | ||
| </NeatoThemeProvider> | ||
| </ThemeProvider> | ||
| </body> | ||
@@ -206,6 +247,6 @@ </html> | ||
| ```typescript | ||
| import { useNeatoTheme } from 'neato/theme'; | ||
| import { useTheme } from 'neato/theme'; | ||
| function ThemeToggle() { | ||
| const { theme, setTheme, effectiveTheme, isHydrated } = useNeatoTheme(); | ||
| const { theme, setTheme, effectiveTheme, isHydrated } = useTheme(); | ||
@@ -257,6 +298,6 @@ return ( | ||
| ```typescript | ||
| import { useNeatoTheme } from 'neato/theme'; | ||
| import { useTheme } from 'neato/theme'; | ||
| function AdvancedThemeToggle() { | ||
| const { theme, setTheme, effectiveTheme } = useNeatoTheme(); | ||
| const { theme, setTheme, effectiveTheme } = useTheme(); | ||
@@ -293,3 +334,3 @@ const cycleTheme = () => { | ||
| #### `useNeatoTheme()` | ||
| #### `useTheme()` | ||
@@ -321,3 +362,3 @@ 테마 상태와 제어 함수를 반환합니다. | ||
| function ThemeStatus() { | ||
| const { theme, setTheme, effectiveTheme, isHydrated } = useNeatoTheme(); | ||
| const { theme, setTheme, effectiveTheme, isHydrated } = useTheme(); | ||
@@ -342,3 +383,3 @@ if (!isHydrated) { | ||
| #### `createNeatoThemeScript()` | ||
| #### `createThemeScript()` | ||
@@ -406,10 +447,20 @@ FOUC(Flash of Unstyled Content) 방지를 위한 인라인 스크립트 문자열을 생성합니다. | ||
| ```typescript | ||
| const multi = neatoVariants({ | ||
| icon: { | ||
| base: 'w-4 h-4', | ||
| variants: { color: { red: 'text-red-500', blue: 'text-blue-500' } } | ||
| const cardStyles = neatoVariants({ | ||
| container: { | ||
| base: 'rounded-lg border bg-white shadow-sm', | ||
| variants: { | ||
| size: { sm: 'p-4', md: 'p-6', lg: 'p-8' } | ||
| } | ||
| }, | ||
| label: { | ||
| base: 'font-bold', | ||
| variants: { size: { sm: 'text-sm', lg: 'text-lg' } } | ||
| header: { | ||
| base: 'border-b pb-4 mb-4', | ||
| variants: { | ||
| align: { left: 'text-left', center: 'text-center', right: 'text-right' } | ||
| } | ||
| }, | ||
| content: { | ||
| base: 'text-gray-700', | ||
| variants: { | ||
| spacing: { tight: 'space-y-2', normal: 'space-y-4', loose: 'space-y-6' } | ||
| } | ||
| } | ||
@@ -419,4 +470,5 @@ }); | ||
| // 각 슬롯별로 함수로 접근 | ||
| multi.icon({ color: 'red' }); // "w-4 h-4 text-red-500" | ||
| multi.label({ size: 'lg', className: 'underline' }); // "font-bold text-lg underline" | ||
| cardStyles.container({ size: 'lg' }); // "rounded-lg border bg-white shadow-sm p-8" | ||
| cardStyles.header({ align: 'center', className: 'font-bold' }); // "border-b pb-4 mb-4 text-center font-bold" | ||
| cardStyles.content({ spacing: 'loose' }); // "text-gray-700 space-y-6" | ||
| ``` | ||
@@ -498,14 +550,8 @@ 이제 멀티 슬롯 컴포넌트에서 각 부분별 스타일을 독립적으로 사용할 수 있습니다. | ||
| function Card({ size, headerAlign, contentSpacing, title, children }) { | ||
| const styles = cardStyles({ | ||
| container: { size }, | ||
| header: { align: headerAlign }, | ||
| content: { spacing: contentSpacing } | ||
| }); | ||
| return ( | ||
| <div className={styles.container}> | ||
| <header className={styles.header}> | ||
| <div className={cardStyles.container({ size })}> | ||
| <header className={cardStyles.header({ align: headerAlign })}> | ||
| <h3>{title}</h3> | ||
| </header> | ||
| <div className={styles.content}> | ||
| <div className={cardStyles.content({ spacing: contentSpacing })}> | ||
| {children} | ||
@@ -512,0 +558,0 @@ </div> |
| #!/usr/bin/env node | ||
| export {}; |
-72
| #!/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}`); | ||
| // 1. Convert to dark mode | ||
| const darkModeColor = toDarkModeColor(color); | ||
| console.log(`Dark Mode Color: #${darkModeColor}`); | ||
| // 2. Convert dark mode color to filter | ||
| const { filter, loss } = toFilterColor(darkModeColor); // Remove # from hex | ||
| 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.parse(process.argv); | ||
| if (!program.args.length) { | ||
| program.help(); | ||
| } |
| 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: '', | ||
| }; | ||
| const MAX_ATTEMPTS = 20; // Define a maximum number of attempts to prevent infinite loops | ||
| let attempts = 0; | ||
| while (bestResult.loss >= 5.0 && attempts < MAX_ATTEMPTS) { | ||
| 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, | ||
| }; | ||
| } |
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.
49695
28.66%713
9.02%640
7.74%16
-20%