diff --git a/.storybook/main.ts b/.storybook/main.ts index 30bbe9f..d23026d 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -20,6 +20,11 @@ const config: StorybookConfig = { if (process.env.STORYBOOK_BASE_HREF) { cfg.base = process.env.STORYBOOK_BASE_HREF } + // Allow all hosts for tunneling (localtunnel, cloudflared, ngrok, etc.) + cfg.server = { + ...(cfg.server || {}), + allowedHosts: true, + } return cfg }, } diff --git a/CHANGELOG.md b/CHANGELOG.md index b73f47f..503fd37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,197 @@ # Changelog -All notable changes to this project will be documented in this file. +## [1.3.0] - 2025-10-11 -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +### Added + +**🌈 Gradient Support for InputColorPicker** + +- **Full gradient support** for `InputColorPicker` component + - Linear gradients with multiple color stops + - Automatic gradient detection and UI switching + - Click gradient text to copy CSS to clipboard + - Opacity control applies to all gradient colors + +- **Gradient Input Features** + - Displays gradient as "Linear" text with copy functionality + - Customizable width via `classNameGradientInput` prop + - Automatic mode switching between solid color and gradient + - Preserves gradient format during opacity changes + +- **Opacity Management** + - Opacity applies to all rgba colors in gradients + - Smooth drag control without value jumps + - Separate opacity state for gradients vs solid colors + - Real-time preview updates + +- **New Props** + - `classNameGradientInput?: string` - Custom classes for gradient input + - `showGradient?: boolean` - Enable gradient mode in ColorPicker popup + - Enhanced `onChange` callback with gradient support + +### Changed + +- **InputColorPicker Component** + - Refactored to support both solid colors and gradients + - Improved state management with `colorType` detection + - Enhanced `useColorPickerState` hook with gradient logic + - Better synchronization between gradient and opacity + +- **Opacity Control** + - Modified to apply opacity to all rgba colors in gradients + - Fixed drag behavior to prevent value resets + - Improved `useEffect` dependencies for gradient handling + +### Fixed + +- 🐛 Fixed opacity jumping between current value and 100% during drag for gradients +- 🐛 Fixed gradient input width inconsistency with solid color input +- 🐛 Fixed initial colorType not detecting gradients on mount +- 🐛 Fixed opacity not applying to all colors in gradient string +- 🐛 Improved gradient CSS parsing and manipulation + +### Documentation + +- Updated README.md with comprehensive InputColorPicker documentation +- Added gradient examples and usage patterns +- Enhanced Storybook with 7 interactive examples: + - Default, BackgroundColor, TextColor + - GradientBackground, MultipleColors + - CardDesign, OpacityControl +- Added API reference for all InputColorPicker props + +### Technical Improvements + +- **TypeScript** + - Added `classNameGradientInput` to InputColorPickerProps interface + - Better type safety for gradient detection + - Improved prop type definitions + +- **Code Quality** + - Modular gradient handling utilities + - Consistent code formatting + - Better error handling for clipboard operations + +- **Bundle Size** + - No significant increase in bundle size + - Tree-shakeable as before + - Optimized gradient regex operations + +### Breaking Changes + +**None** — This release is fully backward compatible. + +- Components without gradient support work exactly as before +- All existing solid color functionality preserved +- New gradient features are opt-in via `showGradient` prop + +## [1.2.0] - 2025-10-10 + +### Added + +**🎨 Alpha Channel Support for HEX Inputs** + +- **`showAlpha` prop** for `InputHex` and `InputHexWithPreview` components + - Enable 8-symbol HEX input (RRGGBBAA format) + - Supports alpha values from 00 (transparent) to FF (opaque) + - Backward compatible — defaults to 6-symbol input + +- **Automatic Preview Transparency** + - `InputHexWithPreview` now displays alpha channel in preview + - Preview shows real transparency on checkerboard background + - Alpha affects preview color automatically + +- **Smart Drag Behavior** + - Drag changes only base color (first 6 symbols) + - Alpha channel is preserved during drag operations + - No conflicts between drag and alpha input + +- **Uppercase Formatting** + - All HEX values automatically converted to uppercase + - Consistent formatting across all inputs + - Applied to both manual input and drag operations + +### Changed + +- **InputHex Component** + - Refactored state management (`disable` → `isEditing`) + - Improved local state handling (`newHex` → `localHex`) + - Enhanced input validation and filtering + - Better synchronization with parent component + +- **InputHexWithPreview Component** + - Removed `opacity` prop (replaced by alpha channel in hex value) + - Preview now uses alpha from hex color directly + - Simplified API — one source of truth for transparency + +### Fixed + +- 🐛 Fixed bug where drag would reset to previous value when alpha present +- 🐛 Fixed race condition in state synchronization after drag +- 🐛 Fixed alpha preservation during drag operations +- 🐛 Improved `useEffect` dependencies for better state management + +### Documentation + +- Updated README.md with alpha channel examples +- Added comprehensive API reference for `showAlpha` prop +- Enhanced Storybook examples with alpha demonstrations +- Improved component descriptions and feature lists +- Added migration notes for `opacity` prop removal + +### Technical Improvements + +- **TypeScript** + - Updated interfaces with `showAlpha?: boolean` + - Better type safety for hex color validation + - Improved prop type definitions + +- **Code Quality** + - Consistent code formatting across components + - Better error handling for edge cases + - Improved component architecture + +- **Bundle Size** + - No increase in bundle size + - Tree-shakeable as before + - Optimized imports + +### Breaking Changes + +**None** — This release is fully backward compatible. + +- Components without `showAlpha` prop work exactly as before (6 symbols) +- Removed `opacity` prop from `InputHexWithPreview` (use alpha in hex value instead) +- All existing code continues to work without modifications + +### Migration Guide + +#### Using Alpha Channel (Optional) + +```tsx +// Before (v1.1.0) + + +// After (v1.2.0) - Use alpha in hex value + +``` + +#### No Changes Required + +If you're not using alpha channel, no changes are needed: + +```tsx +// Still works exactly the same + + +``` ## [1.1.0] - 2025-10-08 ### Added **🎉 13 New Specialized Input Components** + - `OpacityInput` - Opacity control (0-100%) - `AngleInput` - Rotation angle control (0-360°) - `BorderRadiusInput` - Border radius control with unit support @@ -25,6 +207,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `BlurInput` - Blur effect control **Advanced Input Features** + - 🎛️ **5 Progression Types**: linear, arithmetic, geometric, paraboloid, exponential - 🔄 **Drag-to-change** functionality for all numeric inputs - 📐 **Dual orientation**: horizontal and vertical layouts @@ -35,6 +218,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 🎨 **Full customization**: className props for all elements **Modular Architecture** + - Refactored `InputColorPicker` into modular structure: - `hooks/` - useColorPickerState, useDraggable - `utils/` - color conversion utilities @@ -43,6 +227,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Consistent component patterns across all inputs **Documentation** + - Added `INPUTS_GUIDE.md` with comprehensive input documentation - Updated README.md with new components section - Added Storybook examples for all input components @@ -99,6 +284,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] - 2025-10-06 ### Changed + - **Project Renamed**: `@flowscape-ui/color-picker` → `@flowscape-ui/design-system-kit` - Updated package name to reflect expanded scope as a comprehensive design system - Enhanced keywords for better discoverability (design-system, design-system-kit, ui-components, component-library, input-range, ui-kit) @@ -106,195 +292,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prepared for future expansion with additional UI components beyond color picker ### Note -This is the initial release under the new name. The library now serves as a foundation for a complete design system kit, with plans to include input range components and other essential UI elements. - -## [1.1.0] - 2024-12-19 - -### Added -- **Advanced Input Components System** - - New `InputNumberSelect` component with drag-and-drop functionality - - Support for multiple progression types: linear, arithmetic, geometric, paraboloid, exponential - - Horizontal and vertical orientation support - - Configurable precision and step values - - Icon support (string or React component) - - Enhanced validation with min/max constraints - -- **Specialized Color Input Component** - - `InputColorPicker` component with integrated color picker - - Real-time color preview with live updates - - Gradient support (linear and radial) - - Opacity control with visual slider - - Background visibility toggle (show/hide) - - Background deletion functionality - - HEX and RGBA format support - - Draggable color picker modal - -- **Utility Hooks and Functions** - - `cn()` utility for Tailwind CSS class merging - - `removeTrailingZeros()` function for number formatting - - Enhanced TypeScript interfaces and types - -- **New Dependencies** - - `lucide-react` for modern icon set - - `react-icons` for additional icon support - - `tailwind-merge` for intelligent class merging - -### Changed -- **Component Architecture** - - Moved `InputNumberSelect` to `src/components/input/` directory - - Updated import paths in `Inputs.tsx` component - - Improved component organization and modularity - -- **Styling System** - - Integrated Tailwind CSS for modern styling - - Enhanced dark theme support - - Improved responsive design - - Better visual feedback and animations - -- **User Experience** - - More intuitive drag-and-drop interactions - - Enhanced visual feedback for value changes - - Compact design with preserved functionality - - Better keyboard accessibility - - Smooth transitions and animations - -### Technical Improvements -- **TypeScript Enhancements** - - Extended interfaces with better type safety - - Improved component prop definitions - - Better error handling and validation - -- **Performance Optimizations** - - Optimized React hooks usage - - Better component re-rendering patterns - - Improved memory management - -- **Code Quality** - - Modular architecture with separated concerns - - Reusable utility functions - - Better code organization and maintainability - -### Dependencies -- Added `lucide-react@^0.544.0` -- Added `react-icons@^5.5.0` -- Added `tailwind-merge@^3.3.1` -- Kept `tinycolor2@^1.6.0` for color manipulation - -## [1.0.0] - 2024-12-19 - -### Added -- **Core Color Picker Component** - - Full-featured React color picker for whiteboard systems - - Support for multiple color formats: RGB, HSL, HSV, CMYK, HEX - - Gradient support (linear and radial) - - Eye dropper functionality for screen color picking - -- **Advanced Features** - - Dark/light mode with automatic theme detection - - Highly customizable with hide/show options for all components - - Preset color support with custom color arrays - - Advanced sliders for fine-tuning colors - - Color guide and comparable colors display - -- **Component System** - - Modular component architecture - - `ColorPicker` main component - - `useColorPicker` hook for advanced usage - - Individual components: ColorSpectrum, Hue, Opacity, Controls, Inputs, Presets - -- **Configuration Options** - - Custom dimensions (width/height) - - Configurable bar and crosshair sizes - - Default color and gradient values - - Custom styling support - - Localization support - -- **Developer Experience** - - Full TypeScript support with type definitions - - Comprehensive documentation and examples - - Storybook playground with multiple examples - - ESLint configuration for code quality - -- **Build System** - - Vite-based development environment - - TSUP for library building - - Ladle for component development - - Vitest for testing - -### Dependencies -- `react@>=18` (peer dependency) -- `react-dom@>=18` (peer dependency) -- `tinycolor2@^1.6.0` for color manipulation -- `@types/tinycolor2@^1.4.4` for TypeScript support - -### Documentation -- Comprehensive README.md with installation and usage examples -- Complete API documentation -- Multiple playground examples demonstrating all features -- Browser support information -- Contributing guidelines - ---- - -## Version History Summary - -### v1.1.0 - Major Enhancement Release -- **Focus**: Advanced input components and improved UX -- **Key Features**: Drag-and-drop inputs, specialized color picker, Tailwind CSS integration -- **Impact**: Significantly enhanced user experience and component flexibility - -### v1.0.0 - Initial Release -- **Focus**: Core color picker functionality -- **Key Features**: Multi-format color support, gradients, eye dropper, customization -- **Impact**: Solid foundation for whiteboard color selection needs - ---- - -## Migration Guide - -### From v1.0.0 to v1.1.0 - -#### Breaking Changes -- `InputNumberSelect` component moved from `src/components/InputNumberSelect.tsx` to `src/components/input/input-number-select.tsx` -- Import path changed: `import InputNumberSelect from './InputNumberSelect'` → `import { InputNumberSelect } from './input/input-number-select'` - -#### New Dependencies Required -```bash -npm install lucide-react react-icons tailwind-merge -``` - -#### New Components Available -- `InputColorPicker` - Specialized color input with integrated picker -- Enhanced `InputNumberSelect` - Advanced numeric input with drag-and-drop -- `Input` - Base input component with Tailwind styling - -#### Recommended Updates -- Consider using new `InputColorPicker` for color selection interfaces -- Leverage enhanced `InputNumberSelect` for better numeric input UX -- Integrate Tailwind CSS for consistent styling - ---- - -## Future Roadmap - -### Planned Features (v1.2.0) -- [ ] Accessibility improvements (ARIA labels, keyboard navigation) -- [ ] Additional color format support (LAB, XYZ) -- [ ] Color palette management -- [ ] Undo/redo functionality -- [ ] Color history tracking -- [ ] Export/import color schemes - -### Long-term Goals -- [ ] Web Components support -- [ ] React Native compatibility -- [ ] Performance optimizations for large color palettes -- [ ] Advanced gradient editing tools -- [ ] Color harmony suggestions -- [ ] Integration with design systems - ---- - -*This changelog follows [Keep a Changelog](https://keepachangelog.com/) format and uses [Semantic Versioning](https://semver.org/) for version numbers.* - +This is the initial release under the new name. The library now serves as a foundation for a complete design system kit, with plans to include input range components and other essential UI elements. diff --git a/README.md b/README.md index 558a129..c1ac5b0 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,33 @@ -# @flowscape-ui/design-system-kit +
-[![npm version](https://img.shields.io/npm/v/@flowscape-ui/design-system-kit.svg)](https://www.npmjs.com/package/@flowscape-ui/design-system-kit) -[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@flowscape-ui/design-system-kit)](https://bundlephobia.com/package/@flowscape-ui/design-system-kit) -[![license](https://img.shields.io/npm/l/@flowscape-ui/design-system-kit.svg)](https://github.com/flowscape-ui/design-system-kit/blob/main/LICENSE) -[![Buy Me a Coffee](https://img.shields.io/badge/Donate-Buy%20Me%20a%20Coffee-FFDD00?logo=buymeacoffee&logoColor=000)](https://buymeacoffee.com/flowscape) +# 🎨 @flowscape-ui/design-system-kit -A comprehensive React design system kit with color picker, specialized input components, and other essential UI elements. Built with TypeScript, modular architecture, and optimized for modern web applications. +[![npm version](https://img.shields.io/npm/v/@flowscape-ui/design-system-kit.svg?style=flat-square)](https://www.npmjs.com/package/@flowscape-ui/design-system-kit) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@flowscape-ui/design-system-kit?style=flat-square)](https://bundlephobia.com/package/@flowscape-ui/design-system-kit) +[![license](https://img.shields.io/npm/l/@flowscape-ui/design-system-kit.svg?style=flat-square)](https://github.com/flowscape-ui/design-system-kit/blob/main/LICENSE) +[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/) + +**Professional React UI components for color management and design systems** + +[📚 Documentation](https://flowscape-ui.github.io/design-system-kit) • [🎨 Storybook](https://flowscape-ui.github.io/design-system-kit) • [🐛 Report Bug](https://github.com/flowscape-ui/design-system-kit/issues) • [☕ Support](https://buymeacoffee.com/flowscape) + +
+ +--- ## ✨ Features -- 🎨 **Multiple Color Formats**: Support for RGB, HSL, HSV, CMYK, and HEX -- 🌈 **Gradient Support**: Create and edit linear and radial gradients -- 🎯 **Eye Dropper**: Pick colors directly from the screen -- 🔢 **Universal Input Component**: Drag-to-change numeric input for all design properties -- 🎨 **HEX Input Components**: Specialized inputs for color values with drag-to-change -- 🎛️ **Multiple Progression Types**: Linear, arithmetic, geometric, paraboloid, exponential -- 🌓 **Dark/Light Mode**: Automatic theme detection with manual override -- 📦 **Modular Architecture**: Import only what you need for optimal bundle size -- ⚡ **Tree-shakeable**: Reduce bundle size by up to 94% -- 🎨 **Highly Customizable**: Hide/show components and customize styling -- 🔧 **TypeScript Support**: Full type definitions included -- 🎪 **Advanced Controls**: Fine-tune colors and values with precision sliders +- 🎨 **Multiple Color Formats** — RGB, HSL, HSV, CMYK, HEX with alpha channel support +- 🌈 **Gradient Support** — Create and edit linear and radial gradients +- 🎯 **Eye Dropper** — Pick colors directly from the screen +- 🔢 **Universal Input Component** — Drag-to-change numeric input for all design properties +- 🎨 **HEX Input Components** — Specialized inputs with drag-to-change and alpha channel +- 🎛️ **5 Progression Types** — Linear, arithmetic, geometric, paraboloid, exponential +- 🌓 **Dark/Light Mode** — Automatic theme detection with manual override +- 📦 **Modular Architecture** — Import only what you need (tree-shakeable) +- ⚡ **Lightweight** — From 1KB to 62KB per component +- 🔧 **TypeScript** — Full type definitions and IntelliSense support +- 🎪 **Advanced Controls** — Fine-tune colors with precision sliders ## 📦 Installation @@ -28,6 +35,12 @@ A comprehensive React design system kit with color picker, specialized input com npm install @flowscape-ui/design-system-kit ``` +**Peer Dependencies:** + +```bash +npm install react react-dom framer-motion lucide-react react-icons +``` + ## 🚀 Quick Start ### Option 1: Import from main module (convenient) @@ -53,9 +66,22 @@ import { InputHex } from '@flowscape-ui/design-system-kit/input-hex' import { InputHexWithPreview } from '@flowscape-ui/design-system-kit/input-hex-with-preview' ``` -## 📚 Components +## 📚 Components Overview + +| Component | Size | Description | +| ----------------------- | ------ | ----------------------------------------------------------------------------- | +| **ColorPicker** | 134 KB | Full-featured color picker with gradients, eye dropper, and all color formats | +| **InputColorPicker** | 142 KB | Compact color input with popup picker | +| **InputNumberSelect** | 12 KB | Universal drag-to-change numeric input | +| **InputHexWithPreview** | 7.5 KB | HEX input with color preview and alpha support | +| **InputHex** | 6.7 KB | Lightweight HEX input with alpha support | +| **Input** | 847 B | Base input component | + +--- + +## 🎨 ColorPicker -### ColorPicker (59 KB) +Full-featured color picker supporting all color formats, gradients, and advanced controls. ```tsx import { useState } from 'react' @@ -63,598 +89,360 @@ import { ColorPicker } from '@flowscape-ui/design-system-kit/color-picker' function App() { const [color, setColor] = useState('rgba(175, 51, 242, 1)') - return } ``` -### InputNumberSelect (3 KB) +**Key Features:** + +- ✅ RGB, HSL, HSV, CMYK, HEX formats +- ✅ Linear and radial gradients +- ✅ Eye dropper tool +- ✅ Color presets +- ✅ Advanced sliders +- ✅ Dark/light themes + +--- + +## 🔢 InputNumberSelect -Universal drag-to-change numeric input component for all design properties. Supports custom icons, units, precision, and multiple progression types. +Universal drag-to-change numeric input for all design properties. ```tsx -import { useState } from 'react' import { InputNumberSelect } from '@flowscape-ui/design-system-kit/input-number-select' import { RotateCw } from 'lucide-react' -function App() { - const [opacity, setOpacity] = useState(75) - const [angle, setAngle] = useState(45) - - return ( - <> - {/* Opacity control */} - - - {/* Angle control with custom icon */} - } - /> - - ) -} +// Opacity (0-100%) + + +// Angle with custom icon +} +/> ``` **Key Features:** -- 🎯 Drag-to-change with mouse/keyboard support -- 🎨 Custom icons (string or React components) -- 📊 5 progression types (linear, arithmetic, geometric, paraboloid, exponential) -- 🌓 Automatic dark/light theme support via Tailwind CSS -- 📝 Unit display (px, %, rem, em, deg, etc.) -- 🔄 Horizontal/vertical orientation -- ⌨️ Keyboard navigation (Arrow keys, Page Up/Down, Home/End) +- ✅ Drag-to-change with mouse/keyboard +- ✅ 5 progression types (linear, arithmetic, geometric, paraboloid, exponential) +- ✅ Custom icons (string or React components) +- ✅ Unit display (px, %, rem, em, deg, etc.) +- ✅ Horizontal/vertical orientation +- ✅ Keyboard navigation (Arrow keys, Page Up/Down, Home/End) + +--- -### InputColorPicker (62 KB) +## 🎨 InputColorPicker + +Compact color input with popup picker and gradient support. ```tsx -import { useState } from 'react' import { InputColorPicker } from '@flowscape-ui/design-system-kit/input-color-picker' -function App() { - const [color, setColor] = useState('rgba(255, 255, 255, 1)') - - return ( - - ) +// Solid color + + +// Gradient support + +``` + +**Key Features:** + +- ✅ Solid colors and gradients support +- ✅ HEX input with alpha channel (8 symbols) +- ✅ Opacity control with drag slider +- ✅ Click gradient text to copy CSS +- ✅ Integrated ColorPicker popup +- ✅ Background visibility toggle +- ✅ Customizable gradient input width +- ✅ Dark/light theme support + +**Props:** + +```tsx +interface InputColorPickerProps { + value?: string // Color or gradient value + onChange?: (color: string) => void // Change callback + onOpacityChange?: (opacity: number) => void + title?: string // Picker header title + className?: string // Container classes + classNameGradientInput?: string // Gradient input classes (override min-w-[187px]) + showOpacity?: boolean // Show opacity control (default: true) + showGradient?: boolean // Show gradient in picker (default: false) + pickerSize?: number // Picker popup size (default: 250) + onShowPicker?: (shown: boolean) => void + onHideBackground?: (hidden: boolean) => void + onDeleteBackground?: () => void } ``` -### InputHex (1 KB) +--- -Component for HEX color input with drag-to-change. +## 🔤 InputHex + +Lightweight HEX color input with drag-to-change and alpha channel support. ```tsx -import { useState } from 'react' import { InputHex } from '@flowscape-ui/design-system-kit/input-hex' -function App() { - const [color, setColor] = useState('#ff5733') +// Basic usage (6 symbols) + - return -} +// With alpha channel (8 symbols) + ``` **Key Features:** -- 🎨 Drag-to-change for color modification by dragging -- 🔤 Real-time HEX value validation +- ✅ Drag `#` icon to change color +- ✅ Alpha channel support (`showAlpha` prop) +- ✅ Real-time HEX validation +- ✅ Uppercase formatting +- ✅ Customizable callbacks (click, drag start/end) +- ✅ Dark/light theme support -- 🎯 Customizable callbacks for click and drag events -- 🌓 Automatic light/dark theme support +--- -### InputHexWithPreview (1.2 KB) +## 🎨 InputHexWithPreview -Extended version of InputHex with visual color preview. +HEX input with visual color preview and alpha channel support. ```tsx -import { useState } from 'react' import { InputHexWithPreview } from '@flowscape-ui/design-system-kit/input-hex-with-preview' -function App() { - const [color, setColor] = useState('#3498db') - const [opacity, setOpacity] = useState(1) - - return ( - - ) -} +// Basic usage + + +// With alpha channel + ``` **Key Features:** -- 🎨 Everything from InputHex + visual color preview -- 👁️ Square preview with opacity support -- 🎨 Preview style customization -- 📦 Compact size for form integration +- ✅ Everything from InputHex + visual preview +- ✅ Color preview square with alpha support +- ✅ Drag to change color, alpha preserved +- ✅ Customizable preview styles +- ✅ Compact size for forms + +--- + +## 📋 API Reference -### InputHex Props +### InputHex / InputHexWithPreview Props ```tsx interface InputHexProps { - // Main parameters - hexColor: string // HEX color (required) + // Required + hexColor: string // HEX color value handleChange: (hexColor: string) => void // Change callback + // Alpha channel + showAlpha?: boolean // Enable 8-symbol input (RRGGBBAA) + // Styling className?: string // Container classes classNameInput?: string // Input field classes classNameIcon?: string // Icon classes + classNamePreview?: string // Preview classes (InputHexWithPreview only) // Behavior disabled?: boolean // Disable component isDisabledMouseEvent?: boolean // Disable drag functionality // Callbacks - onIconClick?: (hexColor: string) => void // Icon click - onIconPointerDown?: (hexColor: string) => void // Drag start - onIconPointerUp?: (hexColor: string) => void // Drag end - - // HTML input props - ...HTMLInputElement // All standard input props + onIconClick?: (hexColor: string) => void + onIconPointerDown?: (hexColor: string) => void + onIconPointerUp?: (hexColor: string) => void } ``` -### InputHexWithPreview Props +**Alpha Channel Behavior:** -Inherits all props from `InputHexProps` plus: +- When `showAlpha={true}`, input accepts 8 symbols (RRGGBBAA) +- Drag changes only first 6 symbols (base color), alpha is preserved +- Alpha affects preview transparency automatically +- Format: `#RRGGBBAA` where AA is alpha (00=transparent, FF=opaque) -```tsx -interface InputHexWithPreviewProps extends InputHexProps { - opacity?: number // Opacity (0-1), default: 1 - classNamePreview?: string // Classes for color preview -} -``` - -### InputHex Usage Examples +### Usage Examples ```tsx import { InputHex, InputHexWithPreview } from '@flowscape-ui/design-system-kit' -// Basic usage - console.log(color)} -/> +// Basic usage (6 symbols) + + +// With alpha channel (8 symbols) + // With preview - + -// With custom callbacks - console.log('Clicked:', hex)} - onIconPointerDown={(hex) => console.log('Drag start:', hex)} - onIconPointerUp={(hex) => console.log('Drag end:', hex)} -/> +// With preview and alpha + -// Disabled drag +// Custom callbacks console.log('Clicked:', hex)} + onIconPointerDown={(hex) => console.log('Drag start:', hex)} + onIconPointerUp={(hex) => console.log('Drag end:', hex)} /> +// Disabled drag (keyboard only) + + // Custom styles ``` -### InputNumberSelect - Usage Examples - -One universal component for all design properties. Configure it through props: - -```tsx -import { InputNumberSelect } from '@flowscape-ui/design-system-kit/input-number-select' -import { RotateCw, SquareRoundCorner, Type, Blend } from 'lucide-react' - -// Opacity (0-100%) -} -/> - -// Angle (0-360°) -} -/> - -// Border Radius with units -} -/> - -// Font Size -} -/> - -// Line Height (unitless) - - -// Vertical orientation - -``` +--- ### InputNumberSelect Props ```tsx interface InputNumberSelectProps { - // Value control + // Required value: number onChange?: (value: number) => void - // Range configuration + // Range min?: number // Default: 0 max?: number // Default: 100 step?: number // Default: 1 precision?: number // Decimal places, default: 0 - // Progression type for dragging - progression?: 'linear' | 'arithmetic' | 'geometric' | 'paraboloid' | 'exponential' + // Progression type + progression?: + | 'linear' + | 'arithmetic' + | 'geometric' + | 'paraboloid' + | 'exponential' - // Visual configuration + // Visual orientation?: 'horizontal' | 'vertical' // Default: 'horizontal' icon?: React.ReactNode | string // Custom icon or text - - // Unit display unit?: 'px' | '%' | 'rem' | 'em' | 'deg' | 'none' showUnit?: boolean // Show unit after value // Styling - className?: string // Container classes - classNameInput?: string // Input field classes - classNameIcon?: string // Icon container classes + className?: string + classNameInput?: string + classNameIcon?: string - // State - disabled?: boolean // Disable component - isDisabledMouseEvent?: boolean // Disable drag functionality - - // HTML input props - ...HTMLInputElement // All standard input props + // Behavior + disabled?: boolean + isDisabledMouseEvent?: boolean } ``` -### Progression Types +**Progression Types:** -- **linear** - Linear change (default) -- **arithmetic** - Arithmetic progression (×2) -- **geometric** - Geometric progression (×1.05) -- **paraboloid** - Parabolic acceleration -- **exponential** - Exponential change +- `linear` — Linear change (default) +- `arithmetic` — Arithmetic progression (×2) +- `geometric` — Geometric progression (×1.05) +- `paraboloid` — Parabolic acceleration +- `exponential` — Exponential change -## Basic Usage +--- -### Simple Color Picker - -```tsx -import { ColorPicker } from '@flowscape-ui/design-system-kit' -; console.log(color)} /> -``` +## 🎨 ColorPicker Advanced Configuration -### With Custom Dimensions - -```tsx - -``` - -### Gradient Support - -```tsx - -``` - -## Advanced Configuration - -### Hide Specific Components +### Hide Components ```tsx -``` - -### Custom Presets - -```tsx -const customPresets = [ - '#ff0000', - '#00ff00', - '#0000ff', - 'rgba(255, 255, 0, 1)', - 'linear-gradient(45deg, #ff0000, #0000ff)' -] - - -``` - -### Theme Configuration - -```tsx - ``` -### Custom Styling +### Custom Presets & Styling ```tsx -const customStyles = { - body: { - backgroundColor: '#1a1a1a', - borderRadius: '8px', - padding: '16px' - }, - rbgcpInput: { - backgroundColor: '#2a2a2a', - color: '#ffffff', - border: '1px solid #444' - } -} +const presets = ['#ff0000', '#00ff00', '#0000ff'] ``` -### Configuration Options +--- -```tsx -const config = { - barSize: 18, // Size of slider bars - crossSize: 18, // Size of color picker crosshair - defaultColor: 'rgba(175, 51, 242, 1)', - defaultGradient: 'linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)' -} +## 🔧 Advanced: useColorPicker Hook - -``` - -## Using the Hook - -For more control, you can use the `useColorPicker` hook directly: +For custom implementations, use the `useColorPicker` hook: ```tsx import { useColorPicker } from '@flowscape-ui/design-system-kit' -function CustomColorPicker() { - const [color, setColor] = useState('rgba(175, 51, 242, 1)') - - const { - setR, - setG, - setB, - setA, - setHue, - setSaturation, - setLightness, - valueToHex, - valueToHSL, - valueToHSV, - valueToCmyk, - isGradient, - gradientType, - degrees, - rgbaArr, - hslArr, - previousColors, - } = useColorPicker(color, setColor) - - return ( -
- setR(Number(e.target.value))} - /> -

Current color: {valueToHex()}

-

Previous colors: {previousColors.length}

-
- ) -} +const { setR, setG, setB, valueToHex, rgbaArr } = useColorPicker( + color, + setColor +) ``` -## Props API - -### ColorPicker Props - -| Prop | Type | Default | Description | -| ---------------------- | ------------------------- | ------------------------- | ------------------------------------------------ | -| `value` | `string` | `'rgba(175, 51, 242, 1)'` | Current color value (RGB, HEX, HSL, or gradient) | -| `onChange` | `(value: string) => void` | **Required** | Callback when color changes | -| `width` | `number` | `294` | Width of the color picker | -| `height` | `number` | `294` | Height of the color picker | -| `hideControls` | `boolean` | `false` | Hide control buttons | -| `hideInputs` | `boolean` | `false` | Hide input fields | -| `hideOpacity` | `boolean` | `false` | Hide opacity slider | -| `hidePresets` | `boolean` | `false` | Hide preset colors | -| `hideHue` | `boolean` | `false` | Hide hue slider | -| `hideEyeDrop` | `boolean` | `false` | Hide eye dropper | -| `hideAdvancedSliders` | `boolean` | `false` | Hide advanced sliders | -| `hideColorGuide` | `boolean` | `false` | Hide color guide | -| `hideInputType` | `boolean` | `false` | Hide input type dropdown | -| `hideColorTypeBtns` | `boolean` | `false` | Hide solid/gradient buttons | -| `hideGradientType` | `boolean` | `false` | Hide gradient type controls | -| `hideGradientAngle` | `boolean` | `false` | Hide gradient angle controls | -| `hideGradientStop` | `boolean` | `false` | Hide gradient stop controls | -| `hideGradientControls` | `boolean` | `false` | Hide all gradient controls | -| `hidePickerSquare` | `boolean` | `false` | Hide main color square | -| `presets` | `string[]` | `[]` | Custom preset colors | -| `disableDarkMode` | `boolean` | `false` | Disable dark mode | -| `disableLightMode` | `boolean` | `false` | Disable light mode | -| `showHexAlpha` | `boolean` | `false` | Show alpha in hex values | -| `style` | `Styles` | `{}` | Custom styles object | -| `className` | `string` | `undefined` | CSS class name | -| `config` | `PassedConfig` | `{}` | Configuration options | -| `locales` | `LocalesProps` | `defaultLocales` | Localization strings | -| `idSuffix` | `string` | `undefined` | Suffix for element IDs | - -### Configuration Object +--- -```tsx -interface PassedConfig { - barSize?: number // Size of slider bars (default: 18) - crossSize?: number // Size of color picker crosshair (default: 18) - defaultColor?: string // Default color value - defaultGradient?: string // Default gradient value -} -``` - -### Styles Object - -```tsx -interface Styles { - body?: React.CSSProperties - rbgcpControlBtn?: React.CSSProperties - rbgcpControlIcon?: React.CSSProperties - rbgcpInput?: React.CSSProperties - rbgcpHandle?: React.CSSProperties - // ... and many more style properties -} -``` - -## Color Format Support - -The color picker supports multiple color formats: - -- **RGB**: `rgb(255, 0, 0)` or `rgba(255, 0, 0, 0.5)` -- **HEX**: `#ff0000` or `#ff0000ff` -- **HSL**: `hsl(0, 100%, 50%)` or `hsla(0, 100%, 50%, 0.5)` -- **HSV**: `hsv(0, 100%, 100%)` -- **CMYK**: `cmyk(0, 100%, 100%, 0)` -- **Gradients**: `linear-gradient(90deg, #ff0000 0%, #0000ff 100%)` +### ColorPicker Key Props -## Gradient Support +| Prop | Type | Default | Description | +| ---------------------- | ------------------------- | -------- | ------------------------------------- | +| `value` | `string` | Required | Color value (RGB, HEX, HSL, gradient) | +| `onChange` | `(value: string) => void` | Required | Change callback | +| `width` / `height` | `number` | `294` | Picker dimensions | +| `hideOpacity` | `boolean` | `false` | Hide opacity slider | +| `hidePresets` | `boolean` | `false` | Hide preset colors | +| `hideEyeDrop` | `boolean` | `false` | Hide eye dropper | +| `hideGradientControls` | `boolean` | `false` | Hide gradient controls | +| `presets` | `string[]` | `[]` | Custom preset colors | +| `className` | `string` | - | CSS class name | -The color picker includes comprehensive gradient support: +**Supported Color Formats:** -```tsx -// Linear gradient - +- RGB/RGBA: `rgb(255, 0, 0)`, `rgba(255, 0, 0, 0.5)` +- HEX: `#FF0000`, `#FF0000FF` (with alpha) +- HSL/HSLA: `hsl(0, 100%, 50%)`, `hsla(0, 100%, 50%, 0.5)` +- HSV: `hsv(0, 100%, 100%)` +- CMYK: `cmyk(0, 100%, 100%, 0)` +- Gradients: `linear-gradient(90deg, #ff0000 0%, #0000ff 100%)` -// Radial gradient - -``` +--- ## Browser Support @@ -699,40 +487,36 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file - **🐛 Report Issues**: https://github.com/flowscape-ui/design-system-kit/issues - **☕ Support the Project**: [Buy Me a Coffee](https://buymeacoffee.com/flowscape) -## Changelog +## 📝 Changelog -See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes. +### v1.2.0 (Latest) -### v1.1.0 (Latest) +**🎨 Alpha Channel Support** -**🎉 Major Update: Specialized Input Components** +- ✨ **Alpha channel support** for InputHex and InputHexWithPreview +- 🔤 **`showAlpha` prop** — Enable 8-symbol HEX input (RRGGBBAA) +- 🎨 **Automatic preview** — Alpha affects transparency in InputHexWithPreview +- 🖱️ **Smart drag behavior** — Drag changes base color, alpha preserved +- 🔄 **Uppercase formatting** — All HEX values in uppercase +- 📚 **Enhanced documentation** with alpha channel examples -- ✨ **13 new specialized input components** for design properties -- 🎛️ **5 progression types**: linear, arithmetic, geometric, paraboloid, exponential -- 🔄 **Drag-to-change** functionality for all numeric inputs -- 🎨 **Theme support**: light, dark, auto -- 📐 **Vertical and horizontal** orientation -- 🔧 **Modular architecture** for InputColorPicker (hooks, utils, types) -- 🐛 **Bug fixes**: NaN% in gradients, opacity validation -- 📚 **Enhanced documentation** and Storybook examples +**Breaking Changes:** None — fully backward compatible -**Key Features:** +### v1.1.0 + +**🎉 Specialized Input Components** -- Universal InputNumberSelect component for all design properties -- Drag-to-change functionality with 5 progression types -- Custom icons support (string or React components) -- Automatic dark/light theme via Tailwind CSS -- Horizontal and vertical orientation -- Full keyboard navigation support +- ✨ Universal InputNumberSelect component +- 🎛️ 5 progression types (linear, arithmetic, geometric, paraboloid, exponential) +- 🔄 Drag-to-change functionality +- 🎨 Theme support (light, dark, auto) +- 📐 Horizontal/vertical orientation +- 🔧 Modular architecture ### v1.0.0 -- Initial release as `@flowscape-ui/design-system-kit` -- Full color picker functionality -- Gradient support (linear and radial) -- Multiple color format support (RGB, HSL, HSV, CMYK, HEX) -- Dark/light mode with automatic detection -- Eye dropper functionality -- Advanced controls and sliders -- Full TypeScript support with type definitions -- Optimized bundle size (43KB minified) +- 🎉 Initial release +- 🎨 Full-featured ColorPicker +- 🌈 Gradient support +- 🎯 Eye dropper tool +- 🔧 TypeScript support diff --git a/package-lock.json b/package-lock.json index acb015a..350ed63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@storybook/addon-docs": "^9.1.10", "@storybook/react-vite": "^9.1.10", "@tailwindcss/vite": "^4.1.14", + "@types/node": "^24.7.1", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "@types/tinycolor2": "^1.4.6", @@ -2069,6 +2070,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz", + "integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, "node_modules/@types/react": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", @@ -6155,6 +6166,13 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, "node_modules/unicorn-magic": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", diff --git a/package.json b/package.json index b59a23c..d703825 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@flowscape-ui/design-system-kit", - "version": "1.0.0", - "description": "A comprehensive React design system kit with color picker, input range, and other essential UI components. Built with TypeScript and optimized for modern web applications", + "version": "1.3.0", + "description": "Professional React UI components for color management and design systems. Features ColorPicker with gradients, InputColorPicker, InputHex with alpha channel, InputNumberSelect, and more. Built with TypeScript.", "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -120,6 +120,7 @@ "@storybook/addon-docs": "^9.1.10", "@storybook/react-vite": "^9.1.10", "@tailwindcss/vite": "^4.1.14", + "@types/node": "^24.7.1", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "@types/tinycolor2": "^1.4.6", diff --git a/src/color-picker/components/Inputs.tsx b/src/color-picker/components/Inputs.tsx index 2271065..650964f 100644 --- a/src/color-picker/components/Inputs.tsx +++ b/src/color-picker/components/Inputs.tsx @@ -279,10 +279,12 @@ const Inputs = () => { className={`${_defaultStyles} ${_hideOpacity ? 'opacity-50' : ''}`} id={`rbgcp-inputs-wrap${pickerIdSuffix}`} > -
+
{ const sorted = newColors.sort( - (a: GradientProps, b: GradientProps) => a.left - b.left + (a: GradientProps, b: GradientProps) => (a.left ?? 0) - (b.left ?? 0) ) - const colorString = sorted?.map((cc: any) => `${cc?.value} ${cc.left}%`) - const newGrade = `${gradientType}(${degreeStr}, ${colorString.join(', ')})` + const safeDegreeStr = degreeStr && degreeStr.trim() ? degreeStr : '0deg' + const colorString = sorted.map((cc: any) => { + const safeLeft = Number.isFinite(cc?.left) ? cc.left : 0 + return `${cc?.value} ${safeLeft}%` + }) + const newGrade = `${gradientType}(${safeDegreeStr}, ${colorString.join(', ')})` setPrevious({ ...previous, gradient: newGrade }) onChange(newGrade) } @@ -63,8 +67,10 @@ export default function PickerContextWrapper({ const remaining = colors?.filter( (c: GradientProps) => !isUpperCase(c.value) ) + const baseLeft = typeof left === 'number' ? left : currentLeft + const safeLeft = Number.isFinite(baseLeft) ? baseLeft : 0 const newColors = [ - { value: newColor.toUpperCase(), left: left ?? currentLeft }, + { value: newColor.toUpperCase(), left: safeLeft }, ...remaining, ] createGradientStr(newColors) diff --git a/src/input-color-picker/hooks/use-color-picker-state.ts b/src/input-color-picker/hooks/use-color-picker-state.ts index 81256e4..01f3b3b 100644 --- a/src/input-color-picker/hooks/use-color-picker-state.ts +++ b/src/input-color-picker/hooks/use-color-picker-state.ts @@ -21,7 +21,9 @@ export const useColorPickerState = ({ const [color, setColor] = useState(value) const [inputValue, setInputValue] = useState(hex) const [livePreviewColor, setLivePreviewColor] = useState(value) - const [colorType, setColorType] = useState<'color' | 'gradient'>('color') + const [colorType, setColorType] = useState<'color' | 'gradient'>( + value.includes('gradient') ? 'gradient' : 'color' + ) const [opacityValue, setOpacityValue] = useState( isNaN(opacity) ? 100 : opacity ) @@ -31,7 +33,12 @@ export const useColorPickerState = ({ setInputValue(newHex) setColor(value) setLivePreviewColor(value) - setOpacityValue(isNaN(newOpacity) ? 100 : newOpacity) + // Обновляем colorType на основе value + setColorType(value.includes('gradient') ? 'gradient' : 'color') + // Для градиентов не обновляем opacityValue, так как opacity встроен в rgba цвета + if (!value.includes('gradient')) { + setOpacityValue(isNaN(newOpacity) ? 100 : newOpacity) + } }, [value]) const handleInputChange = (e: React.ChangeEvent) => { @@ -57,7 +64,7 @@ export const useColorPickerState = ({ } const handleHexChange = () => { - if (hex === 'Mixed') return + if (hex === 'Linear') return let newHex = inputValue.trim() if (!newHex.startsWith('#')) { @@ -86,6 +93,14 @@ export const useColorPickerState = ({ setOpacityValue(validOpacity) if (color.includes('gradient')) { + // Применяем opacity ко всем rgba цветам в градиенте + const newGradient = color.replace( + /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/g, + (_match, r, g, b) => `rgba(${r}, ${g}, ${b}, ${validOpacity / 100})` + ) + setColor(newGradient) + setLivePreviewColor(newGradient) + onChange?.(newGradient) return } @@ -96,6 +111,7 @@ export const useColorPickerState = ({ const b = rgbMatch[3] const newRgbaColor = `rgba(${r}, ${g}, ${b}, ${validOpacity / 100})` setColor(newRgbaColor) + setLivePreviewColor(newRgbaColor) onChange?.(newRgbaColor) } else { const rgb = hexToRgb(color) @@ -104,6 +120,7 @@ export const useColorPickerState = ({ validOpacity / 100 })` setColor(newRgbaColor) + setLivePreviewColor(newRgbaColor) onChange?.(newRgbaColor) } } diff --git a/src/input-color-picker/input-color-picker.tsx b/src/input-color-picker/input-color-picker.tsx index 2926b65..2fa71d9 100644 --- a/src/input-color-picker/input-color-picker.tsx +++ b/src/input-color-picker/input-color-picker.tsx @@ -1,16 +1,110 @@ import { Eye, EyeOff, Trash2, X } from 'lucide-react' import { useRef, useState } from 'react' import { ColorPicker } from '../color-picker' -import { InputNumberSelect } from '../input-number-select' +import { InputHex } from '../input-hex' import { cn } from '../shared/utils/cn' import { useColorPickerState } from './hooks/use-color-picker-state' import { useDraggable } from './hooks/use-draggable' import type { InputColorPickerProps } from './types' -import { createDisplayColor } from './utils/color-utils' +import { createDisplayColor, opacityToHex } from './utils/color-utils' + +const THEME_CLASSES = { + light: { + container: 'bg-white border-gray-300 focus-within:ring-blue-500', + text: 'text-gray-900', + textMuted: 'text-gray-600', + input: 'text-gray-900', + icon: 'text-gray-600', + dragArea: 'bg-gray-100 hover:bg-gray-200', + preview: 'border-gray-300', + divider: 'bg-gray-300', + }, + dark: { + container: + 'dark:bg-gray-800 dark:border-gray-600 dark:focus-within:ring-blue-400', + text: 'dark:text-gray-200', + textMuted: 'dark:text-gray-400', + input: 'dark:text-gray-100', + icon: 'dark:text-gray-300', + dragArea: 'dark:bg-gray-700 dark:hover:bg-gray-600', + preview: 'dark:border-gray-600', + divider: 'dark:bg-gray-600', + }, +} as const + +interface OpacityDragControlProps { + opacity: number + onChange: (value: number) => void +} + +const OpacityDragControl = ({ opacity, onChange }: OpacityDragControlProps) => { + const [isDragging, setIsDragging] = useState(false) + + const handlePointerDown = (e: React.PointerEvent) => { + e.preventDefault() + const target = e.currentTarget + target.setPointerCapture(e.pointerId) + + const styleElement = document.createElement('style') + styleElement.id = 'opacity-dragging-cursor' + styleElement.innerHTML = ` + body, body * { + cursor: ew-resize !important; + user-select: none !important; + } + ` + document.head.appendChild(styleElement) + + const startX = e.clientX + const startOpacity = opacity + setIsDragging(true) + + const handlePointerMove = (event: PointerEvent) => { + const deltaX = event.clientX - startX + const step = 0.5 + let newOpacity = Math.round(startOpacity + deltaX * step) + newOpacity = Math.max(0, Math.min(100, newOpacity)) + onChange(newOpacity) + } + + const handlePointerUp = (event: PointerEvent) => { + target.releasePointerCapture(event.pointerId) + setIsDragging(false) + + const styleToRemove = document.getElementById('opacity-dragging-cursor') + if (styleToRemove) { + styleToRemove.remove() + } + + target.removeEventListener('pointermove', handlePointerMove) + document.removeEventListener('pointerup', handlePointerUp) + } + + target.addEventListener('pointermove', handlePointerMove) + document.addEventListener('pointerup', handlePointerUp) + } + + return ( + + ) +} export const InputColorPicker = ({ title = 'Background Color', className, + classNameGradientInput, value = 'rgba(255,255,255,1)', onChange, onShowPicker, @@ -33,8 +127,8 @@ export const InputColorPicker = ({ colorType, opacityValue, handleInputChange, - handleInputFocus, - handleHexChange, + handleInputFocus: _handleInputFocus, + handleHexChange: _handleHexChange, handleOpacityChange, handleColorChange, } = useColorPickerState({ value, onChange, onOpacityChange }) @@ -51,11 +145,23 @@ export const InputColorPicker = ({ const displayColor = createDisplayColor(livePreviewColor, opacityValue) + const handleCopyGradient = async () => { + if (colorType === 'gradient' && color) { + try { + await navigator.clipboard.writeText(color) + } catch (err) { + console.error('Failed to copy gradient:', err) + } + } + } + return (
@@ -76,32 +182,68 @@ export const InputColorPicker = ({ className="absolute inset-0" style={{ background: displayColor, - opacity: opacityValue / 100, }} /> - { - if (e.key === 'Enter') { - handleHexChange() - ;(e.target as HTMLInputElement).blur() + { + const cleanHex = newHex.replace('#', '').toUpperCase() + + // Извлекаем base color (первые 6 символов) + const baseColor = cleanHex.slice(0, 6) + + // Извлекаем alpha (последние 2 символа, если есть) + let newOpacity = opacityValue + if (cleanHex.length >= 8) { + const alphaHex = cleanHex.slice(6, 8) + const parsedOpacity = Math.round( + (parseInt(alphaHex, 16) / 255) * 100 + ) + if ( + !isNaN(parsedOpacity) && + parsedOpacity >= 0 && + parsedOpacity <= 100 + ) { + newOpacity = parsedOpacity + handleOpacityChange(parsedOpacity) + } + } + + // Обновляем основной цвет только если он валидный + if ( + baseColor.length === 6 && + /^[0-9A-F]{6}$/i.test(baseColor) + ) { + // Конвертируем HEX в RGB + const r = parseInt(baseColor.slice(0, 2), 16) + const g = parseInt(baseColor.slice(2, 4), 16) + const b = parseInt(baseColor.slice(4, 6), 16) + const newRgbaColor = `rgba(${r}, ${g}, ${b}, ${newOpacity / 100})` + + // Обновляем состояние + onChange?.(newRgbaColor) + handleInputChange({ + target: { value: baseColor }, + } as any) } }} - className="bg-transparent outline-none px-2 text-gray-200 font-mono text-xs" - disabled={hex === 'Mixed'} + showAlpha={true} + className="ml-1 bg-transparent border-none h-auto focus-within:ring-0 flex-1" + classNameInput={cn( + 'bg-transparent px-2 font-mono text-xs', + THEME_CLASSES.light.text, + THEME_CLASSES.dark.text + )} + // disabled={hex === 'Mixed'} />
) : ( - + + +
)} -
- handleOpacityChange(Number(value) || 0)} - step={1} - min={0} - icon="%" - max={100} - className="bg-transparent dark:bg-transparent flex-row-reverse px-0 py-0 h-4 outline-none border-none font-mono text-xs text-gray-400" - classNameInput="w-10 text-gray-400 font-mono text-xs px-0 py-0 text-right" +
+ +
-
diff --git a/src/input-color-picker/types.ts b/src/input-color-picker/types.ts index 87bb2fd..e1b1c7f 100644 --- a/src/input-color-picker/types.ts +++ b/src/input-color-picker/types.ts @@ -2,6 +2,7 @@ export interface InputColorPickerProps { title?: string value?: string className?: string + classNameGradientInput?: string showOpacity?: boolean showGradient?: boolean pickerSize?: number diff --git a/src/input-color-picker/utils/color-utils.ts b/src/input-color-picker/utils/color-utils.ts index 3227b0d..0c8810e 100644 --- a/src/input-color-picker/utils/color-utils.ts +++ b/src/input-color-picker/utils/color-utils.ts @@ -8,7 +8,7 @@ export const parseColor = (colorStr: string): ParsedColor => { // Обработка градиентов if (colorStr.includes('gradient')) { - return { hex: 'Mixed', opacity: 100 } + return { hex: 'Linear', opacity: 100 } } // Обработка HEX @@ -84,45 +84,66 @@ export const hexToRgb = (hexStr: string): RGB | null => { * Конвертирует прозрачность (0-100) в HEX (00-FF) */ export const opacityToHex = (opacity: number): string => { - if (opacity < 0 || opacity > 100) return 'ff' - const alpha = Math.round((opacity / 100) * 255) - return alpha.toString(16).padStart(2, '0') + if (opacity < 0 || opacity > 100) return 'ff' + const alpha = Math.round((opacity / 100) * 255) + return alpha.toString(16).padStart(2, '0') } /** * Создает display color с учетом прозрачности */ export const createDisplayColor = ( - livePreviewColor: string, - opacityValue: number + livePreviewColor: string, + opacityValue: number ): string => { - let displayColor = - livePreviewColor && - (livePreviewColor.includes('gradient') || - livePreviewColor.startsWith('rgba') || - livePreviewColor.startsWith('#')) - ? livePreviewColor - : '#FFFFFF' - - if (displayColor.startsWith('#')) { - let hexVal = displayColor.substring(1) - if (hexVal.length === 3 || hexVal.length === 4) { - hexVal = hexVal - .split('') - .map(char => char + char) - .join('') - } - const rgbHex = hexVal.substring(0, 6) - displayColor = `#${rgbHex}${opacityToHex(opacityValue)}` - } - - return displayColor + let displayColor = + livePreviewColor && + (livePreviewColor.includes('gradient') || + livePreviewColor.startsWith('rgba') || + livePreviewColor.startsWith('rgb') || + livePreviewColor.startsWith('#')) + ? livePreviewColor + : '#FFFFFF' + + // Обработка градиентов - возвращаем как есть + // Opacity для градиентов применяется напрямую к rgba цветам в handleOpacityChange + if (displayColor.includes('gradient')) { + return displayColor + } + + // Нормализация rgb/rgba -> rgba с учётом текущей opacityValue + if (displayColor.startsWith('rgb')) { + const match = displayColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/) + if (match) { + const r = parseInt(match[1], 10) + const g = parseInt(match[2], 10) + const b = parseInt(match[3], 10) + const a = match[4] !== undefined ? parseFloat(match[4]) : undefined + const finalA = isNaN(opacityValue) ? (typeof a === 'number' ? a : 1) : opacityValue / 100 + displayColor = `rgba(${r}, ${g}, ${b}, ${finalA})` + } + } + + // HEX -> HEXA с учётом opacityValue + if (displayColor.startsWith('#')) { + let hexVal = displayColor.substring(1) + if (hexVal.length === 3 || hexVal.length === 4) { + hexVal = hexVal + .split('') + .map(char => char + char) + .join('') + } + const rgbHex = hexVal.substring(0, 6) + displayColor = `#${rgbHex}${opacityToHex(opacityValue)}` + } + + return displayColor } /** * Фильтрует ввод для HEX значения */ export const filterHexInput = (value: string): string => { - const filteredValue = value.replace(/[^0-9a-fA-F]/g, '') - return filteredValue.substring(0, 6).toUpperCase() + const filteredValue = value.replace(/[^0-9a-fA-F]/g, '') + return filteredValue.substring(0, 6).toUpperCase() } diff --git a/src/input-hex-with-preview/input-hex-with-preview.tsx b/src/input-hex-with-preview/input-hex-with-preview.tsx index 10ddaf0..57c608d 100644 --- a/src/input-hex-with-preview/input-hex-with-preview.tsx +++ b/src/input-hex-with-preview/input-hex-with-preview.tsx @@ -6,7 +6,6 @@ import { cn } from '../shared/utils/cn' export interface InputHexWithPreviewProps extends Omit, 'onChange' | 'value'> { hexColor: string - opacity?: number handleChange: (hexColor: string) => void className?: string classNameInput?: string @@ -16,6 +15,11 @@ export interface InputHexWithPreviewProps onIconClick?: (hexColor: string) => void onIconPointerDown?: (hexColor: string) => void onIconPointerUp?: (hexColor: string) => void + /** + * Show and allow alpha channel input (another 2 hex symbols). + * By default disabled (input only 6 symbols). + */ + showAlpha?: boolean } const THEME_CLASSES = { @@ -43,7 +47,6 @@ export const InputHexWithPreview = React.forwardRef< ( { hexColor, - opacity = 1, handleChange, className, classNameInput, @@ -54,55 +57,67 @@ export const InputHexWithPreview = React.forwardRef< onIconClick, onIconPointerDown, onIconPointerUp, + showAlpha = false, ...props }, ref ) => { - const dragRef = useRef(null) - const [disable, setDisable] = useState('') - const hex = tc(hexColor).toHex() - const [newHex, setNewHex] = useState(hex) + const [isEditing, setIsEditing] = useState(false) + + // Normalize input color through tinycolor2 + const color = tc(hexColor) + const hex = color.toHex() // 6 symbols without # + const alpha = color.getAlpha() // 0-1 + const alphaHex = Math.round(alpha * 255) + .toString(16) + .padStart(2, '0') + // Full value: if showAlpha and alpha < 1, add alpha + const hexFromProp = showAlpha && alpha < 1 ? hex + alphaHex : hex + + const [localHex, setLocalHex] = useState(hexFromProp) const dragStartValue = useRef(0) useEffect(() => { - if (disable !== 'hex') { - setNewHex(hex) + if (!isEditing) { + setLocalHex(hexFromProp) } - }, [hexColor, disable, hex]) - const hexFocus = () => { - setDisable('hex') - } - - const hexBlur = () => { - setDisable('') - } + }, [hexColor, isEditing, hexFromProp]) - const getCurrenthexColor = () => { - return newHex || hex + // Helper: converts 8-symbol hex to format with alpha + const convertToHex8 = (hex8: string) => { + const base = hex8.slice(0, 6) + const alphaHex = hex8.slice(6, 8) + const alphaDecimal = parseInt(alphaHex, 16) / 255 + return tc(`#${base}`).setAlpha(alphaDecimal).toHex8String().toUpperCase() } const handleHexInput = (e: React.ChangeEvent) => { - const val = e.target.value - setNewHex(val) - if (tc(val).isValid()) { - const hexWithHash = val.startsWith('#') ? val : `#${val}` - handleChange(hexWithHash) + const maxLen = showAlpha ? 8 : 6 + // Filter only hex symbols, truncate by length + const filtered = e.target.value + .replace(/[^0-9a-fA-F]/g, '') + .slice(0, maxLen) + .toUpperCase() + + setLocalHex(filtered) + + // Emit change only when full length + if (filtered.length === 6) { + handleChange(`#${filtered}`) + } else if (filtered.length === 8 && showAlpha) { + handleChange(convertToHex8(filtered)) } } const handleIconClick = () => { - if (onIconClick) { - onIconClick(getCurrenthexColor()) - } + onIconClick?.(localHex) } const handlePointerDown = (e: React.PointerEvent) => { if (disabled || isDisabledMouseEvent) return e.preventDefault() - if (onIconPointerDown) { - onIconPointerDown(getCurrenthexColor()) - } + onIconPointerDown?.(localHex) const target = e.currentTarget target.setPointerCapture(e.pointerId) @@ -119,7 +134,11 @@ export const InputHexWithPreview = React.forwardRef< dragStartValue.current = parseInt(hex, 16) const startX = e.clientX - setDisable('hex') + setIsEditing(true) + + // Save initial alpha to preserve it during drag + const initialAlpha = + showAlpha && localHex.length >= 8 ? localHex.slice(6, 8) : '' const handlePointerMove = (event: PointerEvent) => { const movementX = event.clientX - startX @@ -129,18 +148,27 @@ export const InputHexWithPreview = React.forwardRef< ) newhexColorInt = Math.max(0, Math.min(0xffffff, newhexColorInt)) - const newhexColorHex = newhexColorInt.toString(16).padStart(6, '0') - setNewHex(newhexColorHex) - handleChange(`#${newhexColorHex}`) + // Drag changes only base color (6 symbols) + const newBaseHex = newhexColorInt + .toString(16) + .padStart(6, '0') + .toUpperCase() + + // Update local state with new base color + preserved alpha + setLocalHex(newBaseHex + initialAlpha) + + // Emit with alpha if it exists, otherwise just base color + if (showAlpha && initialAlpha) { + handleChange(convertToHex8(newBaseHex + initialAlpha)) + } else { + handleChange(`#${newBaseHex}`) + } } const handlePointerUp = (event: PointerEvent) => { target.releasePointerCapture(event.pointerId) - setDisable('') - - if (onIconPointerUp) { - onIconPointerUp(getCurrenthexColor()) - } + setIsEditing(false) + onIconPointerUp?.(localHex) const styleToRemove = document.getElementById('dragging-cursor-style') if (styleToRemove) { @@ -154,13 +182,11 @@ export const InputHexWithPreview = React.forwardRef< document.addEventListener('pointerup', handlePointerUp) } - const displayValue = newHex + // Color for preview with opacity + const previewColor = tc(hexColor).toRgbString() - const hexhexColor = tc(hexColor).toHex() - const rgbahexColor = tc(hexhexColor).setAlpha(opacity).toRgbString() - - const renderIcon = useMemo(() => { - return ( + const renderIcon = useMemo( + () => (
- ) - }, [rgbahexColor, classNamePreview]) + ), + [previewColor, classNamePreview] + ) return (