🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

neato

Package Overview
Dependencies
Maintainers
1
Versions
95
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

neato - npm Package Compare versions

Comparing version
0.0.14
to
0.0.15
+5
dist/utils.d.ts
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;
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 {};
#!/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,
};
}