@winglet/style-utils
Version:
Comprehensive CSS and style management utilities including className manipulation, CSS compression, and universal style manager for TypeScript projects
587 lines (421 loc) • 15 kB
Markdown
# @winglet/style-utils
[]()
[]()
[]()
[]()
## Overview
`@winglet/style-utils` is a comprehensive CSS and style management utility package for JavaScript/TypeScript projects.
This library provides powerful tools for className manipulation, CSS compression, and universal style management with scoped CSS capabilities. It's designed to be framework-agnostic and can be used in any web environment, including Shadow DOM.
## Installation
```bash
# Using npm
npm install @winglet/style-utils
# Using yarn
yarn add @winglet/style-utils
```
## Sub-path Imports
This package supports sub-path imports to enable more granular imports and optimize bundle size. You can import specific modules directly without importing the entire package:
```typescript
// Main exports
import { cx, cxLite, compressCss, styleManagerFactory, destroyScope } from '@winglet/style-utils';
// Style management utilities
import { styleManagerFactory, destroyScope } from '@winglet/style-utils/style-manager';
// All utilities (including className utilities and CSS compression)
import { cx, cxLite, compressCss } from '@winglet/style-utils/util';
```
### Available Sub-paths
Based on the package.json exports configuration:
- `@winglet/style-utils` - Main exports (all utilities)
- `@winglet/style-utils/style-manager` - Scoped style management utilities (styleManagerFactory, destroyScope)
- `@winglet/style-utils/util` - Utility functions (cx, cxLite, compressCss)
## Compatibility
This package is written using ECMAScript 2020 (ES2020) syntax.
**Supported Environments:**
- Node.js 14.0.0 or higher
- Modern browsers (with ES2020 support)
**For Legacy Environment Support:**
Use transpilers like Babel to convert the code to match your target environment.
## Key Features
### ClassNames Management
#### **[`cx`](./src/utils/cx/cx.ts)**
Concatenates CSS class names conditionally, similar to clsx/classnames but more lightweight.
Accepts various input types including strings, numbers, arrays, and objects.
Filters out falsy values and handles nested structures recursively.
#### **[`cxLite`](./src/utils/cx/cxLite.ts)**
Lightweight version of the cx function that concatenates CSS class names.
Only handles simple truthy/falsy filtering without object or array processing.
Provides better performance for basic use cases where complex input types are not needed.
### CSS Compression
#### **[`compressCss`](./src/utils/compressCss/compressCss.ts)**
High-performance CSS compression utility that removes unnecessary whitespace, comments, and redundant semicolons.
Uses a single-pass approach optimized for performance and memory usage.
### Style Management
#### **[`styleManagerFactory`](./src/utils/styleManager/styleManagerFactory.ts)**
Factory function that creates a scoped CSS management system for efficient style injection and cleanup.
Returns a curried function that allows you to add CSS styles to a specific scope with automatic scoping, and provides cleanup functions to remove specific styles.
Supports both regular DOM and Shadow DOM environments with automatic scoping to prevent style conflicts.
All style updates are batched using requestAnimationFrame for optimal performance.
#### **[`destroyScope`](./src/utils/styleManager/destroyScope.ts)**
Utility function that completely destroys a specific style scope and removes all associated styles from the DOM.
This function performs complete cleanup including canceling pending animation frames, removing stylesheets, and clearing all cached styles for the scope.
## Usage Examples
### Using ClassNames Utilities
```typescript
import { cx, cxLite } from '@winglet/style-utils';
// Using cx with various input types
const classes = cx('btn', 'btn-primary', { 'btn-active': isActive });
console.log(classes); // → 'btn btn-primary btn-active'
// Using cx with arrays and objects
const classes2 = cx(['btn', 'btn-primary'], { 'btn-disabled': disabled });
console.log(classes2); // → 'btn btn-primary' (if disabled is false)
// Using cxLite for simple cases (better performance)
const classes3 = cxLite('btn', 'btn-primary', isActive && 'btn-active');
console.log(classes3); // → 'btn btn-primary btn-active'
// With conditional classes
const buttonClasses = cx(
'btn',
`btn-${variant}`,
size && `btn-${size}`,
{
'btn-disabled': disabled,
'btn-loading': loading
}
);
// React component example
const Button = ({ variant = 'primary', size, disabled, loading, children }) => (
<button
className={cx(
'btn',
`btn-${variant}`,
size && `btn-${size}`,
{ 'btn-disabled': disabled, 'btn-loading': loading }
)}
>
{children}
</button>
);
```
### Using CSS Compression
```typescript
import { compressCss } from '@winglet/style-utils';
const originalCSS = `
.container {
color: red;
background: white;
/* This is a comment */
padding: 16px;
}
.button {
border: none;
border-radius: 4px;
}
`;
const compressed = compressCss(originalCSS);
console.log(compressed);
// → '.container{color:red;background:white;padding:16px}.button{border:none;border-radius:4px}'
// File size reduction example
console.log(`Original: ${originalCSS.length} bytes`);
console.log(`Compressed: ${compressed.length} bytes`);
console.log(
`Reduction: ${((1 - compressed.length / originalCSS.length) * 100).toFixed(1)}%`,
);
```
### Using styleManagerFactory for Scoped CSS
```typescript
import { destroyScope, styleManagerFactory } from '@winglet/style-utils';
// Create a style manager for a specific component scope
const addStyle = styleManagerFactory('my-component');
// Add component-specific styles and get cleanup function
const removeButtonStyles = addStyle(
'button-styles',
`
.btn {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background: #0056b3;
}
.btn:disabled {
background: #6c757d;
cursor: not-allowed;
}
`,
);
// The CSS will be automatically scoped as:
// .my-component .btn { ... }
// .my-component .btn:hover { ... }
// .my-component .btn:disabled { ... }
// Apply the scope to your HTML element
const componentElement = document.getElementById('my-component');
componentElement.className += ' my-component';
// Add more styles dynamically
const removeLayoutStyles = addStyle(
'layout-styles',
`
.container {
display: flex;
gap: 1rem;
}
`,
);
// Remove specific styles
removeLayoutStyles();
// or
removeButtonStyles();
// Clean up everything when component unmounts
destroyScope('my-component');
```
### Using styleManagerFactory with Shadow DOM
```typescript
import { destroyScope, styleManagerFactory } from '@winglet/style-utils';
// Create a custom element with Shadow DOM
class MyCustomElement extends HTMLElement {
private shadowRoot: ShadowRoot;
private addStyle: (styleId: string, css: string) => () => void;
private cleanupFunctions: Array<() => void> = [];
constructor() {
super();
this.shadowRoot = this.attachShadow({ mode: 'open' });
// Create a style manager for this shadow root
this.addStyle = styleManagerFactory('custom-element', {
shadowRoot: this.shadowRoot,
});
this.setupStyles();
this.render();
}
private setupStyles() {
const removeHostStyles = this.addStyle(
'host-styles',
`
:host {
display: block;
font-family: Arial, sans-serif;
}
`,
);
const removeComponentStyles = this.addStyle(
'component-styles',
`
.header {
background: #f5f5f5;
padding: 1rem;
border-bottom: 1px solid #ddd;
}
.content {
padding: 1rem;
}
`,
);
// Store cleanup functions for later use
this.cleanupFunctions.push(removeHostStyles, removeComponentStyles);
}
private render() {
this.shadowRoot.innerHTML = `
<div class="header">
<h2>Custom Element</h2>
</div>
<div class="content">
<slot></slot>
</div>
`;
}
disconnectedCallback() {
// Clean up styles when element is removed
destroyScope('custom-element');
}
}
customElements.define('my-custom-element', MyCustomElement);
```
### Dynamic Theme Management
```typescript
import { destroyScope, styleManagerFactory } from '@winglet/style-utils';
class ThemeManager {
private addStyle: (styleId: string, css: string) => () => void;
private customStyles = new Map<string, () => void>();
constructor() {
this.addStyle = styleManagerFactory('theme');
}
applyTheme(theme: 'light' | 'dark') {
const colors =
theme === 'light'
? {
background: '#ffffff',
text: '#333333',
primary: '#007bff',
}
: {
background: '#1a1a1a',
text: '#ffffff',
primary: '#0d6efd',
};
// Remove previous theme if exists
this.customStyles.get('theme-colors')?.();
// Add new theme
const removeTheme = this.addStyle(
'theme-colors',
`
:root {
--color-background: ${colors.background};
--color-text: ${colors.text};
--color-primary: ${colors.primary};
}
body {
background-color: var(--color-background);
color: var(--color-text);
transition: background-color 0.3s, color 0.3s;
}
`,
);
this.customStyles.set('theme-colors', removeTheme);
}
addCustomStyles(id: string, css: string) {
// Remove existing style if present
this.customStyles.get(id)?.();
const removeStyle = this.addStyle(id, css);
this.customStyles.set(id, removeStyle);
}
removeCustomStyles(id: string) {
const removeStyle = this.customStyles.get(id);
if (removeStyle) {
removeStyle();
this.customStyles.delete(id);
}
}
destroy() {
destroyScope('theme');
this.customStyles.clear();
}
}
// Usage
const themeManager = new ThemeManager();
themeManager.applyTheme('dark');
// Add scope class to body element
document.body.className += ' theme';
```
### Performance Optimization Examples
```typescript
import { compressCss, styleManagerFactory } from '@winglet/style-utils';
// Pre-compress CSS for production
const productionCSS = compressCss(`
.component {
/* Development comments and formatting */
display: flex;
flex-direction: column;
gap: 1rem;
}
`);
// Use compressed CSS to skip compression step
const addOptimizedStyle = styleManagerFactory('optimized-component');
addOptimizedStyle('styles', productionCSS, true); // true = already compressed
// Batch style updates for better performance
const addBatchStyle = styleManagerFactory('batch-component');
// All these updates will be batched into a single DOM update
const cleanupFns = [
addBatchStyle('style1', '.class1 { color: red; }'),
addBatchStyle('style2', '.class2 { color: blue; }'),
addBatchStyle('style3', '.class3 { color: green; }'),
];
// DOM update happens in next animation frame
// Later cleanup if needed
cleanupFns.forEach((cleanup) => cleanup());
```
## API Reference
### ClassNames
#### `cx(...args: ClassValue[]): string`
Conditionally concatenates multiple class values into a single space-separated string.
**Parameters:**
- `args`: Variable number of class value arguments (strings, numbers, arrays, objects)
**Returns:** Concatenated class name string
#### `cxLite(...args: ClassValue[]): string`
Lightweight version of the cx function that performs simple truthy/falsy filtering only.
**Parameters:**
- `args`: Variable number of class value arguments (primarily strings and numbers)
**Returns:** Concatenated class name string
### CSS Compression
#### `compressCss(css: string): string`
Compresses CSS by removing unnecessary whitespace and comments.
**Parameters:**
- `css`: CSS string to compress
**Returns:** Compressed CSS string
### Style Management
#### `styleManagerFactory(scopeId: string, config?: StyleManagerConfig): (styleId: string, css: string, compressed?: boolean) => () => void`
Creates a style manager factory for a specific scope that can add scoped CSS styles.
The returned function automatically scopes CSS selectors with the provided scopeId (e.g., `.scopeId .selector`) and provides efficient style injection with batched DOM updates using requestAnimationFrame.
**Parameters:**
- `scopeId`: Unique identifier for the style scope
- `config`: Optional configuration object
- `shadowRoot`: ShadowRoot instance for Shadow DOM support (optional)
**Returns:** A function that accepts `(styleId, cssString, compressed?)` and returns a cleanup function
**Example:**
```typescript
// Create a style manager for a component
const addStyle = styleManagerFactory('my-component');
// Add styles and get cleanup function
const removeButtonStyle = addStyle(
'button-style',
`
.btn {
background: blue;
color: white;
}
`,
);
// Later, remove the specific style
removeButtonStyle();
// For Shadow DOM
const addShadowStyle = styleManagerFactory('shadow-scope', {
shadowRoot: myElement.shadowRoot,
});
```
#### `destroyScope(scopeId: string): void`
Destroys a specific style scope and removes all associated styles from the DOM.
This function performs complete cleanup including:
- Canceling any pending animation frames
- Removing the scope's stylesheet from `document.adoptedStyleSheets` (modern browsers)
- Removing the scope's style element from the DOM (fallback browsers)
- Clearing all cached styles for the scope
- Removing the scope instance from the internal registry
**Parameters:**
- `scopeId`: The unique identifier of the scope to destroy
**Example:**
```typescript
// Create and use styles
const addStyle = styleManagerFactory('temp-scope');
addStyle('style1', '.class { color: red; }');
// Later, destroy the entire scope
destroyScope('temp-scope'); // Removes all styles for this scope
```
#### `StyleManagerConfig`
Configuration interface for styleManagerFactory.
**Properties:**
- `shadowRoot?: ShadowRoot` - Optional ShadowRoot for Shadow DOM style injection
## Development Setup
```bash
# Clone repository
dir=your-albatrion && git clone https://github.com/vincent-kk/albatrion.git "$dir" && cd "$dir"
# Install dependencies
nvm use && yarn install && yarn run:all build
# Development build
yarn styleUtils build
# Run tests
yarn styleUtils test
```
## License
This project is licensed under the MIT License. See the [`LICENSE`](./LICENSE) file for details.
## Contact
For inquiries or suggestions related to the project, please create an issue.