Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions docs/cn-helper.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Docs/Utils/cn Helper" />

# cn Helper Function

The `cn` helper function is a utility that combines [classnames](https://github.com/JedWatson/classnames) and [tailwind-merge](https://github.com/dcastil/tailwind-merge) to intelligently merge Tailwind CSS classes.

## Basic Usage

```tsx
import { cn } from 'reablocks';

// Merge classes - later classes override earlier ones
const className = cn('p-4 bg-blue-500', 'p-2 text-white');
// Result: 'bg-blue-500 p-2 text-white'

// Conditional classes
const isActive = true;
const className = cn('base-class', isActive && 'active-class');
// Result: 'base-class active-class'
```

## Custom Configuration with createCn

The `createCn` function allows you to create a customized version of `cn` with tailwind-merge configuration. This is useful when you need to:

- Add custom class groups for your design system
- Configure custom prefixes or separators
- Define conflict resolution for custom utility classes

### Example: Custom Class Groups

```tsx
import { createCn } from 'reablocks';

// Create a custom cn with extended class groups
const customCn = createCn({
extend: {
classGroups: {
'font-size': [
{ text: ['xs', 'sm', 'base', 'lg', 'xl', '2xl', 'display-sm', 'display-md', 'display-lg'] }
],
'brand-color': [
{ 'bg-brand': ['primary', 'secondary', 'accent'] },
{ 'text-brand': ['primary', 'secondary', 'accent'] }
]
}
}
});

// Now use it like regular cn, but with custom class merging
const heading = customCn('text-2xl text-brand-primary', 'text-display-md');
// Result: 'text-brand-primary text-display-md' (custom classes merge correctly)
```

### Example: Custom Prefix

```tsx
import { createCn } from 'reablocks';

const customCn = createCn({
prefix: 'tw-'
});

const className = customCn('tw-p-4', 'tw-p-2');
// Result: 'tw-p-2'
```

### Example: Custom Separator

```tsx
import { createCn } from 'reablocks';

const customCn = createCn({
separator: '_'
});

const className = customCn('hover_bg-blue-500', 'hover_bg-red-500');
// Result: 'hover_bg-red-500'
```

## Configuration Options

The `createCn` function accepts a configuration object that is passed directly to [tailwind-merge's extendTailwindMerge](https://github.com/dcastil/tailwind-merge/blob/v2.6.0/docs/configuration.md) function. You can:

- **extend**: Add new class groups and theme values without removing the defaults
- **override**: Replace default class groups and theme values
- **prefix**: Set a custom prefix for Tailwind classes
- **separator**: Set a custom separator for modifiers
- **cacheSize**: Configure the LRU cache size for performance

See the [tailwind-merge documentation](https://github.com/dcastil/tailwind-merge/blob/v2.6.0/docs/configuration.md) for detailed configuration options.

## API Reference

### cn(...classes)

Merges class names using classnames and tailwind-merge.

**Parameters:**
- `classes`: Any number of class names (strings, objects, arrays, or conditionals)

**Returns:** Merged class name string

### createCn(config)

Creates a custom cn function with tailwind-merge configuration.

**Parameters:**
- `config`: Configuration object for tailwind-merge

**Returns:** A customized cn function with the same signature as the default cn

## Examples

See the [cn Helper Story](?path=/story/utils-cn-helper--default-cn) for interactive examples.
124 changes: 124 additions & 0 deletions src/utils/Theme/helpers/cn.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { describe, test, expect } from 'vitest';
import { cn, createCn } from './cn';

describe('cn', () => {
test('should merge class names', () => {
const result = cn('text-red-500', 'text-blue-500');
expect(result).toBe('text-blue-500');
});

test('should handle conditional classes', () => {
const isHidden = false;
const result = cn('base-class', isHidden && 'hidden', 'visible');
expect(result).toBe('base-class visible');
});

test('should merge tailwind classes correctly', () => {
const result = cn('p-4', 'p-2');
expect(result).toBe('p-2');
});

test('should handle arrays', () => {
const result = cn(['text-red-500', 'bg-blue-500'], 'text-green-500');
expect(result).toBe('bg-blue-500 text-green-500');
});

test('should handle objects', () => {
const result = cn({ 'text-red-500': true, 'bg-blue-500': false });
expect(result).toBe('text-red-500');
});
});

describe('createCn', () => {
test('should create a custom cn function', () => {
const customCn = createCn({
extend: {
classGroups: {
'font-size': [{ text: ['custom-size'] }]
}
}
});

expect(typeof customCn).toBe('function');
});

test('custom cn should merge classes', () => {
const customCn = createCn({});
const result = customCn('text-red-500', 'text-blue-500');
expect(result).toBe('text-blue-500');
});

test('custom cn should respect custom config', () => {
const customCn = createCn({
extend: {
classGroups: {
'custom-group': ['custom-a', 'custom-b']
},
conflictingClassGroups: {
'custom-group': ['text-color']
}
}
});

// Custom classes should work
const result1 = customCn('custom-a', 'custom-b');
expect(result1).toBe('custom-b');

// Should still handle regular tailwind classes
const result2 = customCn('p-4', 'p-2');
expect(result2).toBe('p-2');
});

test('custom cn should handle conditional classes', () => {
const customCn = createCn({});
const isHidden = false;
const result = customCn('base-class', isHidden && 'hidden', 'visible');
expect(result).toBe('base-class visible');
});

test('custom cn with prefix override', () => {
const customCn = createCn({
prefix: 'tw-'
});

const result = customCn('tw-p-4', 'tw-p-2');
expect(result).toBe('tw-p-2');
});

test('custom cn with separator override', () => {
const customCn = createCn({
separator: '_'
});

const result = customCn('hover_bg-blue-500', 'hover_bg-red-500');
expect(result).toBe('hover_bg-red-500');
});

test('multiple custom cn instances should be independent', () => {
const customCn1 = createCn({
extend: {
classGroups: {
'custom-1': ['class-1']
}
}
});

const customCn2 = createCn({
extend: {
classGroups: {
'custom-2': ['class-2']
}
}
});

// Both should work independently
expect(typeof customCn1).toBe('function');
expect(typeof customCn2).toBe('function');

const result1 = customCn1('p-4', 'p-2');
const result2 = customCn2('m-4', 'm-2');

expect(result1).toBe('p-2');
expect(result2).toBe('m-2');
});
});
Loading