diff --git a/README.md b/README.md index 6344fb2..558a129 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A comprehensive React design system kit with color picker, specialized input com - 🌈 **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 @@ -36,6 +37,8 @@ import { ColorPicker, InputNumberSelect, InputColorPicker, + InputHex, + InputHexWithPreview, } from '@flowscape-ui/design-system-kit' ``` @@ -46,6 +49,8 @@ import { import { ColorPicker } from '@flowscape-ui/design-system-kit/color-picker' import { InputNumberSelect } from '@flowscape-ui/design-system-kit/input-number-select' import { InputColorPicker } from '@flowscape-ui/design-system-kit/input-color-picker' +import { InputHex } from '@flowscape-ui/design-system-kit/input-hex' +import { InputHexWithPreview } from '@flowscape-ui/design-system-kit/input-hex-with-preview' ``` ## 📚 Components @@ -133,6 +138,140 @@ function App() { } ``` +### InputHex (1 KB) + +Component for HEX color input with drag-to-change. + +```tsx +import { useState } from 'react' +import { InputHex } from '@flowscape-ui/design-system-kit/input-hex' + +function App() { + const [color, setColor] = useState('#ff5733') + + return +} +``` + +**Key Features:** + +- 🎨 Drag-to-change for color modification by dragging +- 🔤 Real-time HEX value validation + +- 🎯 Customizable callbacks for click and drag events +- 🌓 Automatic light/dark theme support + +### InputHexWithPreview (1.2 KB) + +Extended version of InputHex with visual color preview. + +```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 ( + + ) +} +``` + +**Key Features:** + +- 🎨 Everything from InputHex + visual color preview +- 👁️ Square preview with opacity support +- 🎨 Preview style customization +- 📦 Compact size for form integration + +### InputHex Props + +```tsx +interface InputHexProps { + // Main parameters + hexColor: string // HEX color (required) + handleChange: (hexColor: string) => void // Change callback + + // Styling + className?: string // Container classes + classNameInput?: string // Input field classes + classNameIcon?: string // Icon classes + + // 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 +} +``` + +### InputHexWithPreview Props + +Inherits all props from `InputHexProps` plus: + +```tsx +interface InputHexWithPreviewProps extends InputHexProps { + opacity?: number // Opacity (0-1), default: 1 + classNamePreview?: string // Classes for color preview +} +``` + +### InputHex Usage Examples + +```tsx +import { InputHex, InputHexWithPreview } from '@flowscape-ui/design-system-kit' + +// Basic usage + console.log(color)} +/> + +// With preview + + +// With custom callbacks + console.log('Clicked:', hex)} + onIconPointerDown={(hex) => console.log('Drag start:', hex)} + onIconPointerUp={(hex) => console.log('Drag end:', hex)} +/> + +// Disabled drag + + +// Custom styles + +``` + ### InputNumberSelect - Usage Examples One universal component for all design properties. Configure it through props: diff --git a/package.json b/package.json index 499e233..b59a23c 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,16 @@ "types": "./dist/shared/index.d.ts", "import": "./dist/shared/index.js", "require": "./dist/shared/index.cjs" + }, + "./input-hex": { + "types": "./dist/input-hex/index.d.ts", + "import": "./dist/input-hex/index.js", + "require": "./dist/input-hex/index.cjs" + }, + "./input-hex-with-preview": { + "types": "./dist/input-hex-with-preview/index.d.ts", + "import": "./dist/input-hex-with-preview/index.js", + "require": "./dist/input-hex-with-preview/index.cjs" } }, "files": [ diff --git a/src/color-picker/components/GradientControls.tsx b/src/color-picker/components/GradientControls.tsx index 208f864..0ad693d 100644 --- a/src/color-picker/components/GradientControls.tsx +++ b/src/color-picker/components/GradientControls.tsx @@ -1,10 +1,10 @@ import { usePicker } from '../context' -import { formatInputValues, low, high } from '../utils/formatters' import { controlBtnStyles } from '../styles/styles' +import { formatInputValues, high, low } from '../utils/formatters' import TrashIcon, { + DegreesIcon, LinearIcon, RadialIcon, - DegreesIcon, StopIcon, } from './icon' diff --git a/src/color-picker/components/Inputs.tsx b/src/color-picker/components/Inputs.tsx index 6537246..2271065 100644 --- a/src/color-picker/components/Inputs.tsx +++ b/src/color-picker/components/Inputs.tsx @@ -1,137 +1,10 @@ -import React, { useEffect, useRef, useState } from 'react' import tc from 'tinycolor2' +import { InputHex } from '../../input-hex/input-hex' import { InputNumberSelect } from '../../input-number-select' import { usePicker } from '../context' -import { cmykToRgb, getHexAlpha, rgb2cmyk } from '../utils/converters' +import { cmykToRgb, rgb2cmyk } from '../utils/converters' import { round } from '../utils/formatters' -const HexInput = ({ - opacity, - tinyColor, - showHexAlpha, - handleChange, -}: { - tinyColor: any - opacity: number - showHexAlpha: boolean - handleChange: (arg0: string) => void -}) => { - const [disable, setDisable] = useState('') - const hex = tinyColor.toHex() - const [newHex, setNewHex] = useState(hex) - const dragStartValue = useRef(0) - const dragStartX = useRef(0) - - useEffect(() => { - if (disable !== 'hex') { - setNewHex(hex) - } - }, [tinyColor, disable, hex]) - - const hexFocus = () => { - setDisable('hex') - } - - const hexBlur = () => { - setDisable('') - } - - const handleHexInput = (e: React.ChangeEvent) => { - const val = e.target.value - setNewHex(val) - if (tc(val).isValid()) { - const { r, g, b } = tc(val).toRgb() - const newColor = `rgba(${r}, ${g}, ${b}, ${opacity})` - handleChange(newColor) - } - } - - const handleMouseDown = (e: React.MouseEvent) => { - e.preventDefault() - dragStartValue.current = parseInt(tinyColor.toHex(), 16) - dragStartX.current = e.clientX - setDisable('hex') - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - } - - const handleMouseUp = () => { - setDisable('') - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - } - - const handleMouseMove = (e: MouseEvent) => { - const movementX = e.clientX - dragStartX.current - const step = 55000 - let newColorInt = Math.round(dragStartValue.current + movementX * step) - newColorInt = Math.max(0, Math.min(0xffffff, newColorInt)) - - const newColorHex = newColorInt.toString(16).padStart(6, '0') - setNewHex(newColorHex) - - const { r, g, b } = tc(newColorHex).toRgb() - handleChange(`rgba(${r}, ${g}, ${b}, ${opacity})`) - } - - const displayValue = showHexAlpha - ? `${newHex}${getHexAlpha(opacity)}` - : newHex - - const wrapperStyle: React.CSSProperties = { - display: 'inline-flex', - alignItems: 'center', - overflow: 'hidden', - borderRadius: '6px', - border: '1px solid #333', - backgroundColor: '#1a1a1a', - height: 28, - width: '108px', - } - - const handleStyle: React.CSSProperties = { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: '100%', - width: 28, - cursor: 'ew-resize', - color: '#888', - fontWeight: 'bold', - fontFamily: 'monospace', - userSelect: 'none', - fontSize: '16px', - } - - const inputStyle: React.CSSProperties = { - width: '100%', - textAlign: 'left', - paddingLeft: 8, - border: 'none', - borderRadius: 0, - height: '100%', - backgroundColor: 'transparent', - color: '#D4D4D4', - outline: 'none', - fontSize: '14px', - } - - return ( -
-
- # -
- -
- ) -} - const RGBInputs = ({ hc, handleChange, @@ -395,7 +268,7 @@ const Inputs = () => { inputType, tinyColor, hideOpacity: _hideOpacity, - showHexAlpha, + showHexAlpha: _showHexAlpha, handleChange, defaultStyles: _defaultStyles, pickerIdSuffix, @@ -407,10 +280,8 @@ const Inputs = () => { id={`rbgcp-inputs-wrap${pickerIdSuffix}`} >
- { max={100} min={0} step={1} - precision={0} value={Math.round(hc?.a * 100)} onChange={(newVal: string | number) => handleChange( diff --git a/src/color-picker/utils/converters.ts b/src/color-picker/utils/converters.ts index 4271948..d506b9a 100644 --- a/src/color-picker/utils/converters.ts +++ b/src/color-picker/utils/converters.ts @@ -55,22 +55,3 @@ export const cmykToRgb = ({ return { r: r, g: g, b: b } } - -export const getHexAlpha = (opacityPercent: number): string => { - if (typeof opacityPercent !== 'number') { - return 'FF' - } - - if (opacityPercent < 0) { - return '00' - } - - if (opacityPercent > 1) { - return 'FF' - } - - return Math.round(opacityPercent * 255) - .toString(16) - .padStart(2, '0') - .toUpperCase() -} diff --git a/src/index.ts b/src/index.ts index 1b1fba4..9350a5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ export * from './color-picker' export * from './input' export * from './input-color-picker' +export * from './input-hex' +export * from './input-hex-with-preview' export * from './input-number-select' export * from './shared' diff --git a/src/input-hex-with-preview/index.tsx b/src/input-hex-with-preview/index.tsx new file mode 100644 index 0000000..0001697 --- /dev/null +++ b/src/input-hex-with-preview/index.tsx @@ -0,0 +1,2 @@ +export { InputHexWithPreview } from './input-hex-with-preview' +export type { InputHexWithPreviewProps } from './input-hex-with-preview' diff --git a/src/input-hex-with-preview/input-hex-with-preview.tsx b/src/input-hex-with-preview/input-hex-with-preview.tsx new file mode 100644 index 0000000..10ddaf0 --- /dev/null +++ b/src/input-hex-with-preview/input-hex-with-preview.tsx @@ -0,0 +1,238 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import tc from 'tinycolor2' +import { Input } from '../input' +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 + classNameIcon?: string + classNamePreview?: string + isDisabledMouseEvent?: boolean + onIconClick?: (hexColor: string) => void + onIconPointerDown?: (hexColor: string) => void + onIconPointerUp?: (hexColor: string) => void +} + +const THEME_CLASSES = { + light: { + container: 'bg-white border-gray-300 focus-within:ring-blue-500', + input: 'text-gray-900', + icon: 'text-gray-600', + dragArea: 'bg-gray-100 hover:bg-gray-200', + preview: 'border-gray-300', + }, + dark: { + container: + 'dark:bg-gray-800 dark:border-gray-600 dark:focus-within:ring-blue-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', + }, +} as const + +export const InputHexWithPreview = React.forwardRef< + HTMLInputElement, + InputHexWithPreviewProps +>( + ( + { + hexColor, + opacity = 1, + handleChange, + className, + classNameInput, + classNameIcon, + classNamePreview, + disabled, + isDisabledMouseEvent = false, + onIconClick, + onIconPointerDown, + onIconPointerUp, + ...props + }, + ref + ) => { + const dragRef = useRef(null) + const [disable, setDisable] = useState('') + const hex = tc(hexColor).toHex() + const [newHex, setNewHex] = useState(hex) + const dragStartValue = useRef(0) + + useEffect(() => { + if (disable !== 'hex') { + setNewHex(hex) + } + }, [hexColor, disable, hex]) + const hexFocus = () => { + setDisable('hex') + } + + const hexBlur = () => { + setDisable('') + } + + const getCurrenthexColor = () => { + return newHex || hex + } + + 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 handleIconClick = () => { + if (onIconClick) { + onIconClick(getCurrenthexColor()) + } + } + + const handlePointerDown = (e: React.PointerEvent) => { + if (disabled || isDisabledMouseEvent) return + e.preventDefault() + + if (onIconPointerDown) { + onIconPointerDown(getCurrenthexColor()) + } + + const target = e.currentTarget + target.setPointerCapture(e.pointerId) + + const styleElement = document.createElement('style') + styleElement.id = 'dragging-cursor-style' + styleElement.innerHTML = ` + body, body * { + cursor: ew-resize !important; + user-select: none !important; + } + ` + document.head.appendChild(styleElement) + + dragStartValue.current = parseInt(hex, 16) + const startX = e.clientX + setDisable('hex') + + const handlePointerMove = (event: PointerEvent) => { + const movementX = event.clientX - startX + const step = 55000 + let newhexColorInt = Math.round( + dragStartValue.current + movementX * step + ) + newhexColorInt = Math.max(0, Math.min(0xffffff, newhexColorInt)) + + const newhexColorHex = newhexColorInt.toString(16).padStart(6, '0') + setNewHex(newhexColorHex) + handleChange(`#${newhexColorHex}`) + } + + const handlePointerUp = (event: PointerEvent) => { + target.releasePointerCapture(event.pointerId) + setDisable('') + + if (onIconPointerUp) { + onIconPointerUp(getCurrenthexColor()) + } + + const styleToRemove = document.getElementById('dragging-cursor-style') + if (styleToRemove) { + styleToRemove.remove() + } + target.removeEventListener('pointermove', handlePointerMove) + document.removeEventListener('pointerup', handlePointerUp) + } + + target.addEventListener('pointermove', handlePointerMove) + document.addEventListener('pointerup', handlePointerUp) + } + + const displayValue = newHex + + const hexhexColor = tc(hexColor).toHex() + const rgbahexColor = tc(hexhexColor).setAlpha(opacity).toRgbString() + + const renderIcon = useMemo(() => { + return ( +
+ ) + }, [rgbahexColor, classNamePreview]) + + return ( +
+ + + + # + + + +
+ ) + } +) + +InputHexWithPreview.displayName = 'InputHexWithPreview' diff --git a/src/input-hex/index.tsx b/src/input-hex/index.tsx new file mode 100644 index 0000000..c045bf5 --- /dev/null +++ b/src/input-hex/index.tsx @@ -0,0 +1,2 @@ +export { InputHex } from './input-hex' +export type { InputHexProps } from './input-hex' diff --git a/src/input-hex/input-hex.tsx b/src/input-hex/input-hex.tsx new file mode 100644 index 0000000..c3e6554 --- /dev/null +++ b/src/input-hex/input-hex.tsx @@ -0,0 +1,203 @@ +import React, { useEffect, useRef, useState } from 'react' +import tc from 'tinycolor2' +import { Input } from '../input' +import { cn } from '../shared/utils/cn' + +export interface InputHexProps + extends Omit, 'onChange' | 'value'> { + hexColor: string + handleChange: (hexColor: string) => void + className?: string + classNameInput?: string + classNameIcon?: string + isDisabledMouseEvent?: boolean + onIconClick?: (hexColor: string) => void + onIconPointerDown?: (hexColor: string) => void + onIconPointerUp?: (hexColor: string) => void +} + +const THEME_CLASSES = { + light: { + container: 'bg-white border-gray-300 focus-within:ring-blue-500', + input: 'text-gray-900', + icon: 'text-gray-600', + dragArea: 'bg-gray-100 hover:bg-gray-200', + }, + dark: { + container: + 'dark:bg-gray-800 dark:border-gray-600 dark:focus-within:ring-blue-400', + input: 'dark:text-gray-100', + icon: 'dark:text-gray-300', + dragArea: 'dark:bg-gray-700 dark:hover:bg-gray-600', + }, +} as const + +export const InputHex = React.forwardRef( + ( + { + hexColor, + handleChange, + className, + classNameInput, + classNameIcon, + disabled, + isDisabledMouseEvent = false, + onIconClick, + onIconPointerDown, + onIconPointerUp, + ...props + }, + ref + ) => { + const dragRef = useRef(null) + const [disable, setDisable] = useState('') + const hex = tc(hexColor).toHex() + const [newHex, setNewHex] = useState(hex) + const dragStartValue = useRef(0) + + useEffect(() => { + if (disable !== 'hex') { + setNewHex(hex) + } + }, [hexColor, disable, hex]) + + const hexFocus = () => { + setDisable('hex') + } + + const hexBlur = () => { + setDisable('') + } + + const getCurrenthexColor = () => { + return newHex || hex + } + + 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 handleIconClick = () => { + if (onIconClick) { + onIconClick(getCurrenthexColor()) + } + } + + const handlePointerDown = (e: React.PointerEvent) => { + if (disabled || isDisabledMouseEvent) return + e.preventDefault() + + if (onIconPointerDown) { + onIconPointerDown(getCurrenthexColor()) + } + + const target = e.currentTarget + target.setPointerCapture(e.pointerId) + + const styleElement = document.createElement('style') + styleElement.id = 'dragging-cursor-style' + styleElement.innerHTML = ` + body, body * { + cursor: ew-resize !important; + user-select: none !important; + } + ` + document.head.appendChild(styleElement) + + dragStartValue.current = parseInt(hex, 16) + const startX = e.clientX + setDisable('hex') + + const handlePointerMove = (event: PointerEvent) => { + const movementX = event.clientX - startX + const step = 55000 + let newhexColorInt = Math.round( + dragStartValue.current + movementX * step + ) + newhexColorInt = Math.max(0, Math.min(0xffffff, newhexColorInt)) + + const newhexColorHex = newhexColorInt.toString(16).padStart(6, '0') + setNewHex(newhexColorHex) + handleChange(`#${newhexColorHex}`) + } + + const handlePointerUp = (event: PointerEvent) => { + target.releasePointerCapture(event.pointerId) + setDisable('') + + if (onIconPointerUp) { + onIconPointerUp(getCurrenthexColor()) + } + + const styleToRemove = document.getElementById('dragging-cursor-style') + if (styleToRemove) { + styleToRemove.remove() + } + target.removeEventListener('pointermove', handlePointerMove) + document.removeEventListener('pointerup', handlePointerUp) + } + + target.addEventListener('pointermove', handlePointerMove) + document.addEventListener('pointerup', handlePointerUp) + } + + const displayValue = newHex + + return ( +
+ + + +
+ ) + } +) + +InputHex.displayName = 'InputHex' diff --git a/src/shared/utils/get-hex-alpha.ts b/src/shared/utils/get-hex-alpha.ts new file mode 100644 index 0000000..7d35e40 --- /dev/null +++ b/src/shared/utils/get-hex-alpha.ts @@ -0,0 +1,18 @@ +export const getHexAlpha = (opacityPercent: number): string => { + if (typeof opacityPercent !== 'number') { + return 'FF' + } + + if (opacityPercent < 0) { + return '00' + } + + if (opacityPercent > 1) { + return 'FF' + } + + return Math.round(opacityPercent * 255) + .toString(16) + .padStart(2, '0') + .toUpperCase() +} diff --git a/src/stories/InputHex.stories.tsx b/src/stories/InputHex.stories.tsx new file mode 100644 index 0000000..ceaa393 --- /dev/null +++ b/src/stories/InputHex.stories.tsx @@ -0,0 +1,138 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React, { useState } from 'react' +import { InputHex } from '../input-hex' + +const meta = { + title: 'Components/InputHex', + component: InputHex, + parameters: { + layout: 'centered', + docs: { + toc: { + title: 'Table of Contents', + }, + }, + backgrounds: { + default: 'light', + values: [ + { name: 'light', value: '#ffffff' }, + { name: 'dark', value: '#1a1a1a' }, + ], + }, + }, + tags: ['autodocs'], + argTypes: { + hexColor: { + control: 'color', + description: 'HEX color string (e.g., "#AF33F2")', + }, + disabled: { + control: 'boolean', + description: 'Disable component', + }, + isDisabledMouseEvent: { + control: 'boolean', + description: 'Disable mouse dragging', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +/** + * Basic example of InputHex component. + * Use Controls to change parameters. + * Switch background in toolbar to change theme (light/dark). + */ +export const Default: Story = { + args: { + hexColor: '#AF33F2', + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color, setColor] = useState('#AF33F2') + + return ( +
+ +

+ Current color: {color.toUpperCase()} +

+
+ ) + }, +} + +/** + * ## Disabled state + * Component in disabled state. + */ +export const DisabledState: Story = { + args: { + hexColor: '#AF33F2', + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color] = useState('#AF33F2') + + return ( +
+
+

+ InputHex (disabled) +

+ {}} disabled={true} /> +
+
+ ) + }, +} + +/** + * ## Mouse event control + * Demonstration of `isDisabledMouseEvent` property. + * When enabled, dragging is disabled, only keyboard input remains. + */ +export const MouseEventControl: Story = { + args: { + hexColor: '#AF33F2', + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color1, setColor1] = useState('#AF33F2') + const [color2, setColor2] = useState('#AF33F2') + + return ( +
+
+

+ With dragging (default) +

+ +

+ Can drag # icon and enter value +

+
+ +
+

+ Without dragging +

+ +

+ Keyboard input only, dragging disabled +

+
+
+ ) + }, +} diff --git a/src/stories/InputHexWithPreview.stories.tsx b/src/stories/InputHexWithPreview.stories.tsx new file mode 100644 index 0000000..025fe1c --- /dev/null +++ b/src/stories/InputHexWithPreview.stories.tsx @@ -0,0 +1,603 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Palette } from 'lucide-react' +import React, { useState } from 'react' +import tc from 'tinycolor2' +import { InputHexWithPreview } from '../input-hex-with-preview' + +const meta = { + title: 'Components/InputHexWithPreview', + component: InputHexWithPreview, + parameters: { + layout: 'centered', + docs: { + toc: { + title: 'Table of Contents', + }, + }, + backgrounds: { + default: 'light', + values: [ + { name: 'light', value: '#ffffff' }, + { name: 'dark', value: '#1a1a1a' }, + ], + }, + }, + tags: ['autodocs'], + argTypes: { + hexColor: { + control: 'color', + description: 'HEX color string (e.g., "#AF33F2")', + }, + opacity: { + control: { type: 'range', min: 0, max: 1, step: 0.01 }, + description: 'Opacity value (0-1)', + }, + disabled: { + control: 'boolean', + description: 'Disable component', + }, + isDisabledMouseEvent: { + control: 'boolean', + description: 'Disable mouse dragging', + }, + classNamePreview: { + control: 'text', + description: 'Custom classes for color preview', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +/** + * Basic example of InputHexWithPreview component. + * Use Controls to change parameters. + * Switch background in toolbar to change theme (light/dark). + */ +export const Default: Story = { + args: { + hexColor: '#AF33F2', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color, setColor] = useState('#AF33F2') + + return ( +
+ +

+ Current color: {color.toUpperCase()} +

+
+ ) + }, +} + +/** + * ## Color palette + * Interactive palette with multiple colors. + */ +export const ColorPalette: Story = { + args: { + hexColor: '#AF33F2', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [colors, setColors] = useState([ + '#FF6B6B', + '#4ECDC4', + '#45B7D1', + '#FFA07A', + '#98D8C8', + '#F7DC6F', + ]) + + const handleColorChange = (index: number) => (newColor: string) => { + const newColors = [...colors] + newColors[index] = newColor + setColors(newColors) + } + + return ( +
+
+ {colors.map((color, index) => ( + + ))} +
+
+ {colors.map((color, index) => ( +
+ ))} +
+
+ ) + }, +} + +/** + * ## Gradient + * Creating a gradient from two colors. + */ +export const GradientCreator: Story = { + args: { + hexColor: '#FF6B6B', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color1, setColor1] = useState('#FF6B6B') + const [color2, setColor2] = useState('#4ECDC4') + + return ( +
+
+
+

+ Start +

+ +
+
+

+ End +

+ +
+
+
+
+ ) + }, +} + +/** + * ## With opacity + * Component with alpha channel support. + */ +export const WithOpacity: Story = { + args: { + hexColor: '#AF33F2', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color, setColor] = useState('#AF33F2') + const [opacity, setOpacity] = useState(0.75) + + const handleChange = (newColor: string) => { + setColor(newColor) + } + + return ( +
+
+ +
+ + setOpacity(parseFloat(e.target.value))} + className="w-32" + /> +

+ {Math.round(opacity * 100)}% +

+
+
+
+
+
+
+
+ ) + }, +} + +/** + * ## Color scheme + * Creating a color scheme with primary and complementary colors. + */ +export const ColorScheme: Story = { + args: { + hexColor: '#AF33F2', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [primaryColor, setPrimaryColor] = useState('#AF33F2') + const hexColor = tc(primaryColor) + + // Generate complementary colors + const complementary = hexColor.complement().toHexString() + const analogous = hexColor.analogous(3) + const triad = hexColor.triad() + + return ( +
+
+

+ Primary color +

+ +
+ +
+

+ Complementary +

+
+
+ + {complementary.toUpperCase()} + +
+
+ +
+

+ Analogous +

+
+ {analogous.map((color, index) => ( +
+
+ + {color.toHexString().toUpperCase()} + +
+ ))} +
+
+ +
+

+ Triad +

+
+ {triad.map((color, index) => ( +
+
+ + {color.toHexString().toUpperCase()} + +
+ ))} +
+
+
+ ) + }, +} + +/** + * ## Tints and shades + * Generating tints and shades of a color. + */ +export const TintsAndShades: Story = { + args: { + hexColor: '#AF33F2', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color, setColor] = useState('#AF33F2') + const hexColor = tc(color) + + // Generate tints (lighter) + const tints = Array.from({ length: 5 }, (_, i) => { + const amount = (i + 1) * 15 + return hexColor.clone().lighten(amount) + }) + + // Generate shades (darker) + const shades = Array.from({ length: 5 }, (_, i) => { + const amount = (i + 1) * 15 + return hexColor.clone().darken(amount) + }) + + return ( +
+
+

+ Base color +

+ +
+ +
+

+ Tints (lighter) +

+
+ {tints.map((tint, index) => ( +
+ ))} +
+
+ +
+

+ Shades (darker) +

+
+ {shades.map((shade, index) => ( +
+ ))} +
+
+
+ ) + }, +} + +/** + * ## Random colors + * Random color generator. + */ +export const RandomColors: Story = { + args: { + hexColor: '#AF33F2', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [colors, setColors] = useState([ + '#AF33F2', + '#FF6B6B', + '#4ECDC4', + ]) + + const generateRandomColor = () => { + return tc.random().toHexString() + } + + const handleGenerate = () => { + setColors([ + generateRandomColor(), + generateRandomColor(), + generateRandomColor(), + ]) + } + + const handleColorChange = (index: number) => (newColor: string) => { + const newColors = [...colors] + newColors[index] = newColor + setColors(newColors) + } + + return ( +
+
+ {colors.map((color, index) => ( + + ))} +
+ +
+ ) + }, +} + +/** + * ## Disabled state + * Component in disabled state. + */ +export const DisabledState: Story = { + args: { + hexColor: '#AF33F2', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color] = useState('#AF33F2') + + return ( +
+
+

+ InputHexWithPreview (disabled) +

+ {}} + disabled={true} + /> +
+
+ ) + }, +} + +/** + * ## Mouse event control + * Demonstration of `isDisabledMouseEvent` property. + * When enabled, dragging is disabled, only keyboard input remains. + */ +export const MouseEventControl: Story = { + args: { + hexColor: '#AF33F2', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color1, setColor1] = useState('#AF33F2') + const [color2, setColor2] = useState('#AF33F2') + + return ( +
+
+

+ With dragging (default) +

+ +

+ Can drag color preview and enter value +

+
+ +
+

+ Without dragging +

+ +

+ Keyboard input only, dragging disabled +

+
+
+ ) + }, +} + +/** + * ## Custom styles + * Examples with custom preview styles. + */ +export const CustomStyles: Story = { + args: { + hexColor: '#AF33F2', + opacity: 1, + handleChange: (newColor: string) => {}, + }, + render: () => { + const [color1, setColor1] = useState('#AF33F2') + const [color2, setColor2] = useState('#FF6B6B') + const [color3, setColor3] = useState('#4ECDC4') + + return ( +
+
+

+ Large preview +

+ +
+ +
+

+ Rounded preview +

+ +
+ +
+

+ Without border +

+ +
+
+ ) + }, +} diff --git a/tsup.config.ts b/tsup.config.ts index 44e517c..44b013b 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -2,14 +2,16 @@ import { defineConfig } from 'tsup' export default defineConfig({ entry: { - // Главный экспорт (все компоненты) + // Main export (all components) index: 'src/index.ts', - // Модульные экспорты + // Modular exports 'color-picker/index': 'src/color-picker/index.tsx', 'input-number-select/index': 'src/input-number-select/index.tsx', 'input-color-picker/index': 'src/input-color-picker/index.tsx', 'input/index': 'src/input/index.tsx', + 'input-hex/index': 'src/input-hex/index.tsx', + 'input-hex-with-preview/index': 'src/input-hex-with-preview/index.tsx', 'shared/index': 'src/shared/index.ts', }, format: ['esm', 'cjs'], @@ -18,7 +20,7 @@ export default defineConfig({ clean: true, target: 'es2020', treeshake: true, - minify: false, // Не минифицируем - код должен быть читаемым + minify: false, // Don't minify - code should be readable splitting: false, external: [ 'react',