Compare commits

4 Commits

Author SHA1 Message Date
Ilia Mashkov 16c2fda843 feat(shared): add cn utility for tailwind-aware class merging 2026-04-23 09:38:30 +03:00
Ilia Mashkov aea9fde9ff fix: workflow 2026-04-22 16:11:05 +03:00
Ilia Mashkov 7e847640ad test: add timeout to fail the test instead of OOM 2026-04-22 15:24:28 +03:00
Ilia Mashkov f97afc2425 fix(createVirtualizer): add window check to resolve the ReferenceError 2026-04-22 13:37:23 +03:00
7 changed files with 52 additions and 3 deletions
+2 -1
View File
@@ -47,7 +47,8 @@ jobs:
run: yarn test:unit
- name: Run Component Tests
run: yarn test:component
timeout-minutes: 5
run: yarn test:component --reporter=verbose --logHeapUsage
publish:
needs: build # Only runs if tests/lint pass
@@ -258,12 +258,13 @@ export function createVirtualizer<T>(
// Calculate initial offset ONCE
const getElementOffset = () => {
const rect = node.getBoundingClientRect();
return rect.top + window.scrollY;
const scrollY = typeof window !== 'undefined' ? window.scrollY : 0;
return rect.top + scrollY;
};
let cachedOffsetTop = 0;
let rafId: number | null = null;
containerHeight = window.innerHeight;
containerHeight = typeof window !== 'undefined' ? window.innerHeight : 0;
const handleScroll = () => {
if (rafId !== null) {
+1
View File
@@ -39,6 +39,7 @@ export {
export {
buildQueryString,
clampNumber,
cn,
debounce,
getDecimalPlaces,
roundToStepPrecision,
+30
View File
@@ -0,0 +1,30 @@
import {
describe,
expect,
it,
} from 'vitest';
import { cn } from './cn';
describe('cn utility', () => {
it('should merge classes with clsx', () => {
expect(cn('class1', 'class2')).toBe('class1 class2');
expect(cn('class1', { class2: true, class3: false })).toBe('class1 class2');
});
it('should resolve tailwind specificity conflicts', () => {
// text-neutral-400 vs text-brand (text-brand should win)
expect(cn('text-neutral-400', 'text-brand')).toBe('text-brand');
// p-4 vs p-2
expect(cn('p-4', 'p-2')).toBe('p-2');
// dark mode classes should be handled correctly too
expect(cn('text-neutral-400 dark:text-neutral-400', 'text-brand dark:text-brand')).toBe(
'text-brand dark:text-brand',
);
});
it('should handle undefined and null inputs', () => {
expect(cn('class1', undefined, null, 'class2')).toBe('class1 class2');
});
});
+13
View File
@@ -0,0 +1,13 @@
import {
type ClassValue,
clsx,
} from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Utility for merging Tailwind classes with clsx and tailwind-merge.
* This resolves specificity conflicts between Tailwind classes.
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+1
View File
@@ -15,6 +15,7 @@ export {
type QueryParamValue,
} from './buildQueryString/buildQueryString';
export { clampNumber } from './clampNumber/clampNumber';
export { cn } from './cn';
export { debounce } from './debounce/debounce';
export { getDecimalPlaces } from './getDecimalPlaces/getDecimalPlaces';
export { getSkeletonWidth } from './getSkeletonWidth/getSkeletonWidth';
+2
View File
@@ -12,6 +12,8 @@ export default defineConfig({
restoreMocks: true,
setupFiles: ['./vitest.setup.component.ts', './vitest.setup.jsdom.ts'],
globals: true,
testTimeout: 15000,
maxWorkers: process.env.CI ? 1 : undefined,
},
resolve: {