@flikk/ui 1.0.0-beta.21 → 1.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai/CodeBlock/CodeBlock.theme.js +2 -2
- package/dist/components/ai/PromptInput/PromptInput.theme.js +3 -3
- package/dist/components/ai/PromptSuggestions/PromptSuggestion.theme.js +1 -1
- package/dist/components/ai/StreamingResponse/StreamingResponse.theme.js +4 -4
- package/dist/components/ai/TokenCounter/TokenCounter.theme.js +6 -6
- package/dist/components/charts/ActivityRings/ActivityRings.theme.js +2 -2
- package/dist/components/charts/DonutChart/DonutChart.theme.js +2 -2
- package/dist/components/charts/DonutChart/MultiSegmentDonutChart.theme.js +1 -1
- package/dist/components/charts/Heatmap/Heatmap.theme.js +1 -1
- package/dist/components/charts/RadarChart/RadarChart.theme.js +2 -2
- package/dist/components/charts/ScatterPlot/ScatterPlot.theme.js +2 -2
- package/dist/components/charts/shared/ChartText/ChartText.theme.js +1 -1
- package/dist/components/charts/shared/ChartTooltip/ChartTooltip.theme.js +4 -4
- package/dist/components/core/Alert/Alert.theme.js +1 -1
- package/dist/components/core/Avatar/Avatar.theme.js +10 -10
- package/dist/components/core/AvatarGroup/AvatarGroup.theme.js +1 -1
- package/dist/components/core/Badge/Badge.js +2 -12
- package/dist/components/core/Badge/Badge.theme.js +2 -2
- package/dist/components/core/Button/Button.theme.js +6 -6
- package/dist/components/core/ButtonGroup/ButtonGroup.theme.js +1 -1
- package/dist/components/core/Calendar/Calendar.theme.js +5 -5
- package/dist/components/core/Card/Card.theme.js +2 -2
- package/dist/components/core/CommandPalette/CommandPalette.theme.js +4 -4
- package/dist/components/core/Drawer/Drawer.animations.js +5 -4
- package/dist/components/core/Drawer/Drawer.theme.js +2 -2
- package/dist/components/core/Dropdown/Dropdown.theme.js +1 -1
- package/dist/components/core/Empty/Empty.theme.js +2 -2
- package/dist/components/core/Icon/Icon.d.ts +3 -0
- package/dist/components/core/Icon/Icon.js +70 -0
- package/dist/components/core/Icon/Icon.theme.d.ts +2 -0
- package/dist/components/core/Icon/Icon.theme.js +19 -0
- package/dist/components/core/Icon/Icon.types.d.ts +32 -0
- package/dist/components/core/{FileTypeIcon → Icon/data}/fileTypes.d.ts +1 -1
- package/dist/components/core/Icon/data/paymentMethods.d.ts +27 -0
- package/dist/components/core/Icon/data/paymentMethods.js +169 -0
- package/dist/components/core/Icon/index.d.ts +4 -0
- package/dist/components/core/Kbd/Kbd.theme.js +2 -2
- package/dist/components/core/MenuItem/MenuItem.theme.js +2 -2
- package/dist/components/core/Message/Message.theme.js +6 -6
- package/dist/components/core/Modal/Modal.animations.js +3 -1
- package/dist/components/core/Modal/Modal.theme.js +2 -2
- package/dist/components/core/Modal/ModalBody.js +7 -8
- package/dist/components/core/NavItem/NavItem.theme.js +1 -1
- package/dist/components/core/OfflineIndicator/OfflineIndicator.theme.js +2 -2
- package/dist/components/core/PageHeading/PageHeading.theme.js +2 -2
- package/dist/components/core/Pagination/Pagination.theme.js +2 -2
- package/dist/components/core/Pill/Pill.theme.js +3 -3
- package/dist/components/core/Progress/Progress.theme.js +2 -2
- package/dist/components/core/Segmented/Segmented.theme.js +3 -3
- package/dist/components/core/Tabs/Tabs.theme.js +2 -2
- package/dist/components/core/Tag/Tag.theme.js +2 -2
- package/dist/components/core/Toast/Toast.theme.js +2 -2
- package/dist/components/core/Tooltip/Tooltip.theme.js +1 -1
- package/dist/components/core/Tree/Tree.theme.js +1 -1
- package/dist/components/core/index.d.ts +1 -2
- package/dist/components/core/index.js +2 -2
- package/dist/components/data-display/DescriptionList/DescriptionList.theme.js +2 -2
- package/dist/components/data-display/Feed/Feed.theme.js +1 -1
- package/dist/components/data-display/GanttChart/GanttChart.theme.js +2 -2
- package/dist/components/data-display/KPI/KPI.theme.js +9 -9
- package/dist/components/data-display/Metric/Metric.theme.js +9 -9
- package/dist/components/data-display/Table/Table.animations.js +16 -10
- package/dist/components/data-display/Table/Table.filterUtils.js +8 -1
- package/dist/components/data-display/Table/Table.js +10 -14
- package/dist/components/data-display/Table/Table.theme.js +3 -3
- package/dist/components/data-display/Table/Table.types.d.ts +2 -0
- package/dist/components/data-display/Table/TableActions.js +3 -3
- package/dist/components/data-display/Table/TableActionsMenu.js +2 -3
- package/dist/components/data-display/Table/TableCell.js +6 -3
- package/dist/components/data-display/Table/TableDeclarative.js +5 -7
- package/dist/components/data-display/Table/TableHeader.js +5 -2
- package/dist/components/data-display/Table/TablePagination.js +2 -6
- package/dist/components/data-display/Table/TableRow.js +2 -6
- package/dist/components/data-display/Table/index.js +1 -0
- package/dist/components/effects/CustomCursor/CustomCursor.theme.js +1 -1
- package/dist/components/effects/Overlay/Overlay.js +50 -21
- package/dist/components/forms/Checkbox/Checkbox.theme.js +3 -3
- package/dist/components/forms/ColorPicker/ColorPicker.d.ts +5 -9
- package/dist/components/forms/ColorPicker/ColorPicker.js +126 -22
- package/dist/components/forms/ColorPicker/ColorPicker.theme.js +9 -6
- package/dist/components/forms/ColorPicker/ColorPicker.types.d.ts +99 -0
- package/dist/components/forms/ColorPicker/ColorPicker2DCanvas.js +43 -6
- package/dist/components/forms/ColorPicker/ColorPickerBody.js +1 -1
- package/dist/components/forms/ColorPicker/ColorPickerContext.d.ts +4 -0
- package/dist/components/forms/ColorPicker/ColorPickerContext.js +12 -0
- package/dist/components/forms/ColorPicker/ColorPickerEyeDropper.js +1 -1
- package/dist/components/forms/ColorPicker/ColorPickerFormatSelector.js +1 -1
- package/dist/components/forms/ColorPicker/ColorPickerGradient.d.ts +7 -0
- package/dist/components/forms/ColorPicker/ColorPickerGradient.js +186 -0
- package/dist/components/forms/ColorPicker/ColorPickerHeader.d.ts +14 -0
- package/dist/components/forms/ColorPicker/ColorPickerHeader.js +31 -0
- package/dist/components/forms/ColorPicker/ColorPickerImage.d.ts +6 -0
- package/dist/components/forms/ColorPicker/ColorPickerImage.js +154 -0
- package/dist/components/forms/ColorPicker/ColorPickerInput.d.ts +0 -4
- package/dist/components/forms/ColorPicker/ColorPickerInput.js +53 -61
- package/dist/components/forms/ColorPicker/ColorPickerPresets.js +13 -12
- package/dist/components/forms/ColorPicker/ColorPickerSavedColors.d.ts +3 -0
- package/dist/components/forms/ColorPicker/ColorPickerSavedColors.js +69 -0
- package/dist/components/forms/ColorPicker/ColorPickerSliders.d.ts +1 -2
- package/dist/components/forms/ColorPicker/ColorPickerSliders.js +84 -23
- package/dist/components/forms/ColorPicker/ColorPickerSwatch.d.ts +1 -1
- package/dist/components/forms/ColorPicker/ColorPickerSwatch.js +10 -9
- package/dist/components/forms/ColorPicker/ColorPickerTrigger.js +7 -3
- package/dist/components/forms/ColorPicker/index.d.ts +8 -2
- package/dist/components/forms/CronInput/CronInput.theme.js +8 -8
- package/dist/components/forms/DatePicker/DatePicker.theme.js +1 -1
- package/dist/components/forms/DateRangePicker/DateRangePicker.theme.js +3 -3
- package/dist/components/forms/FileUpload/FileUpload.theme.js +4 -4
- package/dist/components/forms/FileUpload/FileUploadProgress.js +3 -3
- package/dist/components/forms/FileUpload/FileUploadProgress.theme.js +2 -2
- package/dist/components/forms/FormLabel/FormLabel.theme.js +1 -1
- package/dist/components/forms/Input/Input.js +3 -3
- package/dist/components/forms/Input/Input.theme.js +1 -1
- package/dist/components/forms/Input/Input.types.d.ts +2 -0
- package/dist/components/forms/InputCounter/InputCounter.theme.js +1 -1
- package/dist/components/forms/InputOTP/InputOTP.theme.js +1 -1
- package/dist/components/forms/Mention/Mention.theme.js +3 -3
- package/dist/components/forms/Radio/Radio.theme.js +3 -3
- package/dist/components/forms/RichTextEditor/RichTextEditor.theme.js +1 -1
- package/dist/components/forms/Select/Select.theme.js +1 -1
- package/dist/components/forms/SelectableCard/SelectableCard.js +42 -27
- package/dist/components/forms/SelectableCard/SelectableCard.theme.js +15 -25
- package/dist/components/forms/SelectableCard/SelectableCard.types.d.ts +16 -3
- package/dist/components/forms/SelectableCard/SelectableCardContext.d.ts +1 -0
- package/dist/components/forms/SelectableCard/SelectableCardContext.js +4 -7
- package/dist/components/forms/SelectableCard/SelectableCardGroup.js +31 -10
- package/dist/components/forms/SelectableCard/index.d.ts +1 -1
- package/dist/components/forms/Signature/Signature.theme.js +1 -1
- package/dist/components/forms/Slider/Slider.theme.js +3 -3
- package/dist/components/forms/Switch/Switch.theme.js +1 -1
- package/dist/components/forms/TimePicker/TimePicker.theme.js +2 -2
- package/dist/components/forms/forms.theme.js +6 -6
- package/dist/components/forms/index.js +6 -1
- package/dist/components/layout/FormLayout/FormLayout.theme.js +2 -2
- package/dist/hooks/useOverlay.d.ts +2 -0
- package/dist/hooks/useOverlay.js +34 -4
- package/dist/index.js +8 -3
- package/dist/styles.css +1 -1
- package/package.json +2 -1
- package/src/global.scss +1 -1
- package/dist/bg/bg/1.jpg +0 -0
- package/dist/bg/bg/10.jpg +0 -0
- package/dist/bg/bg/11.jpg +0 -0
- package/dist/bg/bg/14.jpg +0 -0
- package/dist/bg/bg/15.jpg +0 -0
- package/dist/bg/bg/16.jpg +0 -0
- package/dist/bg/bg/17.jpg +0 -0
- package/dist/bg/bg/18.jpg +0 -0
- package/dist/bg/bg/19.jpg +0 -0
- package/dist/bg/bg/2.jpg +0 -0
- package/dist/bg/bg/20.jpg +0 -0
- package/dist/bg/bg/21.jpg +0 -0
- package/dist/bg/bg/22.jpg +0 -0
- package/dist/bg/bg/23.jpg +0 -0
- package/dist/bg/bg/24.jpg +0 -0
- package/dist/bg/bg/3.jpg +0 -0
- package/dist/bg/bg/4.jpg +0 -0
- package/dist/bg/bg/5.jpg +0 -0
- package/dist/bg/bg/6.jpg +0 -0
- package/dist/bg/bg/7.jpg +0 -0
- package/dist/bg/bg/8.jpg +0 -0
- package/dist/bg/bg/9.jpg +0 -0
- package/dist/components/core/FileTypeIcon/FileTypeIcon.d.ts +0 -3
- package/dist/components/core/FileTypeIcon/FileTypeIcon.js +0 -33
- package/dist/components/core/FileTypeIcon/FileTypeIcon.theme.d.ts +0 -2
- package/dist/components/core/FileTypeIcon/FileTypeIcon.theme.js +0 -12
- package/dist/components/core/FileTypeIcon/FileTypeIcon.types.d.ts +0 -19
- package/dist/components/core/FileTypeIcon/index.d.ts +0 -2
- package/dist/components/core/SocialIcon/SocialIcon.d.ts +0 -3
- package/dist/components/core/SocialIcon/SocialIcon.js +0 -30
- package/dist/components/core/SocialIcon/SocialIcon.theme.d.ts +0 -2
- package/dist/components/core/SocialIcon/SocialIcon.theme.js +0 -12
- package/dist/components/core/SocialIcon/SocialIcon.types.d.ts +0 -18
- package/dist/components/core/SocialIcon/index.d.ts +0 -2
- /package/dist/components/core/{FileTypeIcon → Icon/data}/fileTypes.js +0 -0
- /package/dist/components/core/{SocialIcon → Icon/data}/platforms.d.ts +0 -0
- /package/dist/components/core/{SocialIcon → Icon/data}/platforms.js +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { TrashIcon, PlusIcon, ClipboardDocumentCheckIcon, ClipboardDocumentIcon } from '@heroicons/react/20/solid';
|
|
3
|
+
import { useContext, useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
+
import { cn } from '../../../utils/cn.js';
|
|
5
|
+
import { Button } from '../../core/Button/Button.js';
|
|
6
|
+
import { Segmented } from '../../core/Segmented/Segmented.js';
|
|
7
|
+
import { Input } from '../Input/Input.js';
|
|
8
|
+
import '../Input/Input.theme.js';
|
|
9
|
+
import { ColorPickerContext } from './ColorPickerContext.js';
|
|
10
|
+
import { ColorPicker2DCanvas } from './ColorPicker2DCanvas.js';
|
|
11
|
+
import { ColorPickerSliders } from './ColorPickerSliders.js';
|
|
12
|
+
import { parseColor, formatHex } from './colorUtils.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ColorPickerGradient - Gradient editor with canvas, sliders, stop bar, type toggle, angle control.
|
|
16
|
+
* The 2D canvas and hue slider pick the color for the currently selected gradient stop.
|
|
17
|
+
*/
|
|
18
|
+
const ColorPickerGradient = ({ className, ...props }) => {
|
|
19
|
+
var _a, _b, _c;
|
|
20
|
+
const context = useContext(ColorPickerContext);
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error('ColorPickerGradient must be used within a ColorPicker');
|
|
23
|
+
}
|
|
24
|
+
const { color, setColor, gradient, setGradient, updateGradientStop, addGradientStop, removeGradientStop, getOutputValue, showEyeDropper, } = context;
|
|
25
|
+
const [selectedStopId, setSelectedStopId] = useState((_b = (_a = gradient.stops[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null);
|
|
26
|
+
const [copied, setCopied] = useState(false);
|
|
27
|
+
const [dragStopId, setDragStopId] = useState(null);
|
|
28
|
+
const stopBarRef = useRef(null);
|
|
29
|
+
// Track whether color change originated from stop selection (to avoid update loops)
|
|
30
|
+
const isSelectingStopRef = useRef(false);
|
|
31
|
+
const selectedStop = gradient.stops.find(s => s.id === selectedStopId);
|
|
32
|
+
// --- Sync selected stop color → main color picker (when selecting a stop) ---
|
|
33
|
+
const selectStop = useCallback((stopId) => {
|
|
34
|
+
setSelectedStopId(stopId);
|
|
35
|
+
const stop = gradient.stops.find(s => s.id === stopId);
|
|
36
|
+
if (stop) {
|
|
37
|
+
const parsed = parseColor(stop.color);
|
|
38
|
+
if (parsed) {
|
|
39
|
+
isSelectingStopRef.current = true;
|
|
40
|
+
setColor(parsed);
|
|
41
|
+
// Reset after a tick so the next color change from canvas/slider will update the stop
|
|
42
|
+
requestAnimationFrame(() => { isSelectingStopRef.current = false; });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}, [gradient.stops, setColor]);
|
|
46
|
+
// Load first stop's color into picker on mount
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (selectedStopId) {
|
|
49
|
+
const stop = gradient.stops.find(s => s.id === selectedStopId);
|
|
50
|
+
if (stop) {
|
|
51
|
+
const parsed = parseColor(stop.color);
|
|
52
|
+
if (parsed) {
|
|
53
|
+
isSelectingStopRef.current = true;
|
|
54
|
+
setColor(parsed);
|
|
55
|
+
requestAnimationFrame(() => { isSelectingStopRef.current = false; });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
60
|
+
// --- Sync main color picker → selected stop (when user picks via canvas/slider) ---
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (isSelectingStopRef.current || !selectedStopId)
|
|
63
|
+
return;
|
|
64
|
+
const hex = formatHex(color.h, color.s, color.l);
|
|
65
|
+
updateGradientStop(selectedStopId, { color: hex });
|
|
66
|
+
}, [color.h, color.s, color.l]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
67
|
+
// Build the left-to-right preview gradient (ignoring angle for the bar)
|
|
68
|
+
const barGradientCSS = (() => {
|
|
69
|
+
const sorted = [...gradient.stops].sort((a, b) => a.position - b.position);
|
|
70
|
+
const stopStrings = sorted.map(s => `${s.color} ${s.position}%`).join(', ');
|
|
71
|
+
return `linear-gradient(to right, ${stopStrings})`;
|
|
72
|
+
})();
|
|
73
|
+
// Full output CSS
|
|
74
|
+
const outputCSS = getOutputValue();
|
|
75
|
+
// --- Type toggle ---
|
|
76
|
+
const handleTypeChange = (value) => {
|
|
77
|
+
setGradient({ ...gradient, type: value });
|
|
78
|
+
};
|
|
79
|
+
// --- Angle ---
|
|
80
|
+
const handleAngleChange = (e) => {
|
|
81
|
+
const val = parseInt(e.target.value, 10);
|
|
82
|
+
if (!isNaN(val)) {
|
|
83
|
+
setGradient({ ...gradient, angle: Math.max(0, Math.min(360, val)) });
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
// --- Stop bar interaction ---
|
|
87
|
+
const getPositionFromX = useCallback((clientX) => {
|
|
88
|
+
if (!stopBarRef.current)
|
|
89
|
+
return 0;
|
|
90
|
+
const rect = stopBarRef.current.getBoundingClientRect();
|
|
91
|
+
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
92
|
+
return Math.round(ratio * 100);
|
|
93
|
+
}, []);
|
|
94
|
+
const handleStopBarClick = (e) => {
|
|
95
|
+
if (e.target.closest('[data-stop-marker]'))
|
|
96
|
+
return;
|
|
97
|
+
const position = getPositionFromX(e.clientX);
|
|
98
|
+
addGradientStop(position);
|
|
99
|
+
};
|
|
100
|
+
const handleStopPointerDown = useCallback((e, stopId) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
setDragStopId(stopId);
|
|
104
|
+
selectStop(stopId);
|
|
105
|
+
}, [selectStop]);
|
|
106
|
+
const handlePointerMove = useCallback((e) => {
|
|
107
|
+
if (!dragStopId)
|
|
108
|
+
return;
|
|
109
|
+
const position = getPositionFromX(e.clientX);
|
|
110
|
+
updateGradientStop(dragStopId, { position });
|
|
111
|
+
}, [dragStopId, getPositionFromX, updateGradientStop]);
|
|
112
|
+
const handlePointerUp = useCallback(() => {
|
|
113
|
+
setDragStopId(null);
|
|
114
|
+
}, []);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (dragStopId) {
|
|
117
|
+
window.addEventListener('pointermove', handlePointerMove);
|
|
118
|
+
window.addEventListener('pointerup', handlePointerUp);
|
|
119
|
+
return () => {
|
|
120
|
+
window.removeEventListener('pointermove', handlePointerMove);
|
|
121
|
+
window.removeEventListener('pointerup', handlePointerUp);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}, [dragStopId, handlePointerMove, handlePointerUp]);
|
|
125
|
+
// --- Selected stop color input editing ---
|
|
126
|
+
const [colorInput, setColorInput] = useState((_c = selectedStop === null || selectedStop === void 0 ? void 0 : selectedStop.color) !== null && _c !== void 0 ? _c : '');
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (selectedStop) {
|
|
129
|
+
setColorInput(selectedStop.color);
|
|
130
|
+
}
|
|
131
|
+
}, [selectedStop === null || selectedStop === void 0 ? void 0 : selectedStop.id, selectedStop === null || selectedStop === void 0 ? void 0 : selectedStop.color]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
132
|
+
const handleColorInputChange = (e) => {
|
|
133
|
+
const val = e.target.value;
|
|
134
|
+
setColorInput(val);
|
|
135
|
+
if (selectedStop) {
|
|
136
|
+
const parsed = parseColor(val);
|
|
137
|
+
if (parsed) {
|
|
138
|
+
updateGradientStop(selectedStop.id, {
|
|
139
|
+
color: formatHex(parsed.h, parsed.s, parsed.l),
|
|
140
|
+
});
|
|
141
|
+
// Also update the canvas/sliders
|
|
142
|
+
isSelectingStopRef.current = true;
|
|
143
|
+
setColor(parsed);
|
|
144
|
+
requestAnimationFrame(() => { isSelectingStopRef.current = false; });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const handleColorInputBlur = () => {
|
|
149
|
+
if (selectedStop) {
|
|
150
|
+
setColorInput(selectedStop.color);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const handlePositionChange = (e) => {
|
|
154
|
+
const val = parseInt(e.target.value, 10);
|
|
155
|
+
if (selectedStop && !isNaN(val)) {
|
|
156
|
+
updateGradientStop(selectedStop.id, {
|
|
157
|
+
position: Math.max(0, Math.min(100, val)),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
// --- Copy ---
|
|
162
|
+
const handleCopy = async () => {
|
|
163
|
+
try {
|
|
164
|
+
await navigator.clipboard.writeText(outputCSS);
|
|
165
|
+
setCopied(true);
|
|
166
|
+
setTimeout(() => setCopied(false), 1500);
|
|
167
|
+
}
|
|
168
|
+
catch (_a) { }
|
|
169
|
+
};
|
|
170
|
+
return (jsxs("div", { className: cn('space-y-3', className), ...props, children: [jsx(ColorPicker2DCanvas, {}), jsx(ColorPickerSliders, { className: "flex-1" }), jsxs("div", { className: "flex items-center gap-3", children: [jsxs(Segmented, { value: gradient.type, onChange: handleTypeChange, size: "sm", children: [jsx(Segmented.Item, { value: "linear", children: "Linear" }), jsx(Segmented.Item, { value: "radial", children: "Radial" })] }), gradient.type === 'linear' && (jsxs("div", { className: "flex items-center gap-1", children: [jsx(Input, { size: "sm", type: "number", value: String(gradient.angle), onChange: handleAngleChange, className: "!w-16 text-center", "aria-label": "Gradient angle" }), jsx("span", { className: "text-xs text-[var(--color-text-muted)]", children: "deg" })] }))] }), jsx("div", { className: "h-8 rounded-lg ring-1 ring-inset ring-black/5", style: { background: outputCSS }, "aria-label": "Gradient preview" }), jsxs("div", { className: "space-y-1", children: [jsx("div", { className: "text-xs font-medium text-[var(--color-text-muted)]", children: "Color Stops" }), jsx("div", { ref: stopBarRef, onClick: handleStopBarClick, className: "relative h-4 rounded-full cursor-pointer touch-none select-none", style: { backgroundImage: barGradientCSS }, "aria-label": "Gradient stops", children: gradient.stops.map(stop => (jsx("div", { "data-stop-marker": true, onPointerDown: (e) => handleStopPointerDown(e, stop.id), onClick: (e) => {
|
|
171
|
+
e.stopPropagation();
|
|
172
|
+
selectStop(stop.id);
|
|
173
|
+
}, className: cn('absolute top-1/2 size-4 rounded-full border-2 border-white shadow-md cursor-grab active:cursor-grabbing', selectedStopId === stop.id && 'ring-2 ring-[var(--color-primary)] ring-offset-1'), style: {
|
|
174
|
+
left: `${stop.position}%`,
|
|
175
|
+
transform: 'translate(-50%, -50%)',
|
|
176
|
+
backgroundColor: stop.color,
|
|
177
|
+
}, role: "slider", "aria-label": `Color stop at ${stop.position}%`, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": stop.position, tabIndex: 0 }, stop.id))) })] }), selectedStop && (jsxs("div", { className: "flex items-center gap-2", children: [jsx(Input, { size: "sm", value: colorInput, onChange: handleColorInputChange, onBlur: handleColorInputBlur, "aria-label": "Stop color", className: "flex-1 !font-mono", contentStart: jsx("div", { className: "size-4 rounded-full flex-shrink-0 border border-[var(--color-border)]", style: { backgroundColor: selectedStop.color } }) }), jsxs("div", { className: "flex items-center gap-0.5", children: [jsx(Input, { size: "sm", type: "number", value: String(selectedStop.position), onChange: handlePositionChange, className: "!w-16 text-center", "aria-label": "Stop position" }), jsx("span", { className: "text-xs text-[var(--color-text-muted)]", children: "%" })] }), gradient.stops.length > 2 && (jsx(Button, { variant: "ghost", size: "sm", color: "danger", iconOnly: true, onClick: () => {
|
|
178
|
+
removeGradientStop(selectedStop.id);
|
|
179
|
+
const remaining = gradient.stops.filter(s => s.id !== selectedStop.id);
|
|
180
|
+
if (remaining[0])
|
|
181
|
+
selectStop(remaining[0].id);
|
|
182
|
+
}, "aria-label": "Remove color stop", children: jsx(TrashIcon, { className: "size-4" }) })), jsx(Button, { variant: "ghost", size: "sm", color: "neutral", iconOnly: true, onClick: () => addGradientStop(50), "aria-label": "Add color stop", children: jsx(PlusIcon, { className: "size-4" }) })] })), jsxs("div", { className: "flex items-center gap-2 border border-[var(--color-border)] rounded-lg px-3 py-2", children: [jsx("code", { className: "flex-1 text-xs font-mono text-[var(--color-text-secondary)] truncate", children: outputCSS }), jsx(Button, { variant: "ghost", size: "sm", color: "neutral", iconOnly: true, onClick: handleCopy, "aria-label": "Copy gradient CSS", title: copied ? 'Copied!' : 'Copy CSS', children: copied ? (jsx(ClipboardDocumentCheckIcon, { className: "size-4 text-[var(--color-success)]" })) : (jsx(ClipboardDocumentIcon, { className: "size-4" })) })] })] }));
|
|
183
|
+
};
|
|
184
|
+
ColorPickerGradient.displayName = 'ColorPickerGradient';
|
|
185
|
+
|
|
186
|
+
export { ColorPickerGradient };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ColorPickerMode } from './ColorPicker.types';
|
|
3
|
+
export interface ColorPickerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** Available modes/tabs to show */
|
|
5
|
+
modes?: ColorPickerMode[];
|
|
6
|
+
/** Currently active mode (overrides context) */
|
|
7
|
+
activeMode?: ColorPickerMode;
|
|
8
|
+
/** Callback when mode changes */
|
|
9
|
+
onModeChange?: (mode: ColorPickerMode) => void;
|
|
10
|
+
/** Whether to show the close button */
|
|
11
|
+
showClose?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const ColorPickerHeader: React.FC<ColorPickerHeaderProps>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { XMarkIcon } from '@heroicons/react/20/solid';
|
|
3
|
+
import { useContext } from 'react';
|
|
4
|
+
import { cn } from '../../../utils/cn.js';
|
|
5
|
+
import { ColorPickerContext } from './ColorPickerContext.js';
|
|
6
|
+
|
|
7
|
+
const modeLabels = {
|
|
8
|
+
solid: 'Solid',
|
|
9
|
+
gradient: 'Gradient',
|
|
10
|
+
image: 'Image',
|
|
11
|
+
};
|
|
12
|
+
const ColorPickerHeader = ({ modes = ['solid'], activeMode: activeModeFromProp, onModeChange, showClose = true, className, ...props }) => {
|
|
13
|
+
const context = useContext(ColorPickerContext);
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error('ColorPickerHeader must be used within a ColorPicker');
|
|
16
|
+
}
|
|
17
|
+
const { setIsOpen, mode: contextMode, setMode: contextSetMode } = context;
|
|
18
|
+
const activeMode = activeModeFromProp !== null && activeModeFromProp !== void 0 ? activeModeFromProp : contextMode;
|
|
19
|
+
const handleModeChange = (mode) => {
|
|
20
|
+
if (onModeChange) {
|
|
21
|
+
onModeChange(mode);
|
|
22
|
+
}
|
|
23
|
+
contextSetMode(mode);
|
|
24
|
+
};
|
|
25
|
+
return (jsxs("div", { className: cn('flex items-center justify-between border-b border-[var(--color-border)] -mx-4 -mt-4 px-4', className), ...props, children: [jsx("div", { className: "flex items-center gap-4", children: modes.map((mode) => (jsxs("button", { type: "button", onClick: () => handleModeChange(mode), className: cn('py-3 text-sm font-medium transition-colors relative cursor-pointer', activeMode === mode
|
|
26
|
+
? 'text-[var(--color-primary)]'
|
|
27
|
+
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)]'), "aria-selected": activeMode === mode, role: "tab", children: [modeLabels[mode], activeMode === mode && (jsx("div", { className: "absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--color-primary)] rounded-full" }))] }, mode))) }), showClose && (jsx("button", { type: "button", onClick: () => setIsOpen(false), className: "p-1 text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors rounded-md hover:bg-[var(--color-background-secondary)] cursor-pointer", "aria-label": "Close color picker", children: jsx(XMarkIcon, { className: "size-5" }) }))] }));
|
|
28
|
+
};
|
|
29
|
+
ColorPickerHeader.displayName = 'ColorPickerHeader';
|
|
30
|
+
|
|
31
|
+
export { ColorPickerHeader };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { ArrowUpTrayIcon, XMarkIcon } from '@heroicons/react/20/solid';
|
|
3
|
+
import { useContext, useRef, useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { cn } from '../../../utils/cn.js';
|
|
5
|
+
import { Button } from '../../core/Button/Button.js';
|
|
6
|
+
import { ColorPickerContext } from './ColorPickerContext.js';
|
|
7
|
+
import { rgbToHsl } from './colorUtils.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ColorPickerImage - Pick colors from an uploaded image
|
|
11
|
+
*/
|
|
12
|
+
const ColorPickerImage = ({ className, ...props }) => {
|
|
13
|
+
const context = useContext(ColorPickerContext);
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error('ColorPickerImage must be used within a ColorPicker');
|
|
16
|
+
}
|
|
17
|
+
const { setColor } = context;
|
|
18
|
+
const canvasRef = useRef(null);
|
|
19
|
+
const fileInputRef = useRef(null);
|
|
20
|
+
const [imageSrc, setImageSrc] = useState(null);
|
|
21
|
+
const [imageLoaded, setImageLoaded] = useState(false);
|
|
22
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
23
|
+
const [pickedColor, setPickedColor] = useState(null);
|
|
24
|
+
const [cursorPos, setCursorPos] = useState(null);
|
|
25
|
+
// Clean up object URL on unmount or change
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
return () => {
|
|
28
|
+
if (imageSrc)
|
|
29
|
+
URL.revokeObjectURL(imageSrc);
|
|
30
|
+
};
|
|
31
|
+
}, [imageSrc]);
|
|
32
|
+
const loadImage = useCallback((file) => {
|
|
33
|
+
if (!file.type.startsWith('image/'))
|
|
34
|
+
return;
|
|
35
|
+
// Clean up previous
|
|
36
|
+
if (imageSrc)
|
|
37
|
+
URL.revokeObjectURL(imageSrc);
|
|
38
|
+
const url = URL.createObjectURL(file);
|
|
39
|
+
setImageSrc(url);
|
|
40
|
+
setImageLoaded(false);
|
|
41
|
+
setPickedColor(null);
|
|
42
|
+
setCursorPos(null);
|
|
43
|
+
const img = new Image();
|
|
44
|
+
img.onload = () => {
|
|
45
|
+
const canvas = canvasRef.current;
|
|
46
|
+
if (!canvas)
|
|
47
|
+
return;
|
|
48
|
+
// Fit image within container while maintaining aspect ratio
|
|
49
|
+
const maxWidth = 300;
|
|
50
|
+
const maxHeight = 200;
|
|
51
|
+
let width = img.width;
|
|
52
|
+
let height = img.height;
|
|
53
|
+
if (width > maxWidth || height > maxHeight) {
|
|
54
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
55
|
+
width = Math.round(width * ratio);
|
|
56
|
+
height = Math.round(height * ratio);
|
|
57
|
+
}
|
|
58
|
+
// Account for device pixel ratio for sharp rendering
|
|
59
|
+
const dpr = window.devicePixelRatio || 1;
|
|
60
|
+
canvas.width = width * dpr;
|
|
61
|
+
canvas.height = height * dpr;
|
|
62
|
+
canvas.style.width = `${width}px`;
|
|
63
|
+
canvas.style.height = `${height}px`;
|
|
64
|
+
const ctx = canvas.getContext('2d');
|
|
65
|
+
if (!ctx)
|
|
66
|
+
return;
|
|
67
|
+
ctx.scale(dpr, dpr);
|
|
68
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
69
|
+
setImageLoaded(true);
|
|
70
|
+
};
|
|
71
|
+
img.src = url;
|
|
72
|
+
}, [imageSrc]);
|
|
73
|
+
// File input handler
|
|
74
|
+
const handleFileChange = (e) => {
|
|
75
|
+
var _a;
|
|
76
|
+
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
77
|
+
if (file)
|
|
78
|
+
loadImage(file);
|
|
79
|
+
};
|
|
80
|
+
// Drag and drop handlers
|
|
81
|
+
const handleDragOver = (e) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
setIsDragOver(true);
|
|
84
|
+
};
|
|
85
|
+
const handleDragLeave = () => {
|
|
86
|
+
setIsDragOver(false);
|
|
87
|
+
};
|
|
88
|
+
const handleDrop = (e) => {
|
|
89
|
+
var _a;
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
setIsDragOver(false);
|
|
92
|
+
const file = (_a = e.dataTransfer.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
93
|
+
if (file)
|
|
94
|
+
loadImage(file);
|
|
95
|
+
};
|
|
96
|
+
// Pick color from canvas
|
|
97
|
+
const pickColorAtPosition = useCallback((clientX, clientY) => {
|
|
98
|
+
const canvas = canvasRef.current;
|
|
99
|
+
if (!canvas)
|
|
100
|
+
return;
|
|
101
|
+
const rect = canvas.getBoundingClientRect();
|
|
102
|
+
const dpr = window.devicePixelRatio || 1;
|
|
103
|
+
// Map click position to canvas pixel coordinates
|
|
104
|
+
const x = Math.round(((clientX - rect.left) / rect.width) * canvas.width / dpr) * dpr;
|
|
105
|
+
const y = Math.round(((clientY - rect.top) / rect.height) * canvas.height / dpr) * dpr;
|
|
106
|
+
const ctx = canvas.getContext('2d');
|
|
107
|
+
if (!ctx)
|
|
108
|
+
return;
|
|
109
|
+
const pixel = ctx.getImageData(x, y, 1, 1).data;
|
|
110
|
+
const r = pixel[0];
|
|
111
|
+
const g = pixel[1];
|
|
112
|
+
const b = pixel[2];
|
|
113
|
+
const a = pixel[3] / 255;
|
|
114
|
+
const hsl = rgbToHsl(r, g, b);
|
|
115
|
+
setColor({ ...hsl, a });
|
|
116
|
+
// Set picked color for display
|
|
117
|
+
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
|
|
118
|
+
setPickedColor(hex);
|
|
119
|
+
// Set cursor position as percentage
|
|
120
|
+
setCursorPos({
|
|
121
|
+
x: ((clientX - rect.left) / rect.width) * 100,
|
|
122
|
+
y: ((clientY - rect.top) / rect.height) * 100,
|
|
123
|
+
});
|
|
124
|
+
}, [setColor]);
|
|
125
|
+
const handleCanvasClick = (e) => {
|
|
126
|
+
pickColorAtPosition(e.clientX, e.clientY);
|
|
127
|
+
};
|
|
128
|
+
// Clear image
|
|
129
|
+
const handleClear = () => {
|
|
130
|
+
if (imageSrc)
|
|
131
|
+
URL.revokeObjectURL(imageSrc);
|
|
132
|
+
setImageSrc(null);
|
|
133
|
+
setImageLoaded(false);
|
|
134
|
+
setPickedColor(null);
|
|
135
|
+
setCursorPos(null);
|
|
136
|
+
if (fileInputRef.current)
|
|
137
|
+
fileInputRef.current.value = '';
|
|
138
|
+
};
|
|
139
|
+
return (jsxs("div", { className: cn('space-y-3', className), ...props, children: [!imageLoaded ? (
|
|
140
|
+
/* Upload dropzone */
|
|
141
|
+
jsxs("div", { onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, className: cn('flex flex-col items-center justify-center gap-2 h-[200px] rounded-xl border-2 border-dashed cursor-pointer transition-colors', isDragOver
|
|
142
|
+
? 'border-[var(--color-primary)] bg-[var(--color-primary-50)]'
|
|
143
|
+
: 'border-[var(--color-border)] hover:border-[var(--color-primary)] hover:bg-[var(--color-background-secondary)]'), children: [jsx(ArrowUpTrayIcon, { className: "size-8 text-[var(--color-text-muted)]" }), jsx("div", { className: "text-sm text-[var(--color-text-muted)]", children: "Click or drag an image" }), jsx("div", { className: "text-xs text-[var(--color-text-placeholder)]", children: "PNG, JPG, SVG, WebP" }), jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleFileChange, className: "hidden", "aria-label": "Upload image" })] })) : (
|
|
144
|
+
/* Canvas with color picking */
|
|
145
|
+
jsxs("div", { className: "space-y-2", children: [jsxs("div", { className: "relative inline-block rounded-xl overflow-hidden ring-1 ring-inset ring-black/5", children: [jsx("canvas", { ref: canvasRef, onClick: handleCanvasClick, className: "cursor-crosshair block" }), cursorPos && (jsx("div", { className: "absolute w-5 h-5 border-2 border-white rounded-full pointer-events-none", style: {
|
|
146
|
+
left: `${cursorPos.x}%`,
|
|
147
|
+
top: `${cursorPos.y}%`,
|
|
148
|
+
transform: 'translate(-50%, -50%)',
|
|
149
|
+
boxShadow: '0 0 0 1.5px rgba(0,0,0,0.15), 0 2px 6px rgba(0,0,0,0.15)',
|
|
150
|
+
}, children: pickedColor && (jsx("div", { className: "absolute inset-1 rounded-full", style: { backgroundColor: pickedColor } })) }))] }), jsxs("div", { className: "flex items-center justify-between", children: [pickedColor && (jsxs("div", { className: "flex items-center gap-2", children: [jsx("div", { className: "size-5 rounded-full border border-[var(--color-border)]", style: { backgroundColor: pickedColor } }), jsx("span", { className: "text-xs font-mono text-[var(--color-text-secondary)]", children: pickedColor })] })), jsxs(Button, { variant: "ghost", size: "sm", color: "neutral", onClick: handleClear, "aria-label": "Remove image", className: "ml-auto", children: [jsx(XMarkIcon, { className: "size-3.5" }), "Change"] })] })] })), !imageLoaded && jsx("canvas", { ref: canvasRef, className: "hidden" })] }));
|
|
151
|
+
};
|
|
152
|
+
ColorPickerImage.displayName = 'ColorPickerImage';
|
|
153
|
+
|
|
154
|
+
export { ColorPickerImage };
|
|
@@ -1,7 +1,3 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ColorPickerInputProps } from "./ColorPicker.types";
|
|
3
|
-
/**
|
|
4
|
-
* ColorPickerInput - Manual color value input
|
|
5
|
-
* Supports hex, rgb, and hsl input based on selected format
|
|
6
|
-
*/
|
|
7
3
|
export declare const ColorPickerInput: React.FC<ColorPickerInputProps>;
|
|
@@ -1,27 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useContext, useState } from 'react';
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useContext, useState, useEffect } from 'react';
|
|
3
|
+
import { ChevronUpDownIcon, ClipboardDocumentCheckIcon, ClipboardDocumentIcon } from '@heroicons/react/20/solid';
|
|
3
4
|
import { cn } from '../../../utils/cn.js';
|
|
4
|
-
import {
|
|
5
|
-
import '
|
|
6
|
-
import {
|
|
7
|
-
import { hslToRgb, parseColor } from './colorUtils.js';
|
|
5
|
+
import { Button } from '../../core/Button/Button.js';
|
|
6
|
+
import { ColorPickerContext } from './ColorPickerContext.js';
|
|
7
|
+
import { parseColor, hslToRgb } from './colorUtils.js';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* ColorPickerInput - Manual color value input
|
|
11
|
-
* Supports hex, rgb, and hsl input based on selected format
|
|
12
|
-
*/
|
|
13
9
|
const ColorPickerInput = ({ className, ...props }) => {
|
|
14
10
|
const context = useContext(ColorPickerContext);
|
|
15
11
|
if (!context) {
|
|
16
12
|
throw new Error("ColorPickerInput must be used within a ColorPicker");
|
|
17
13
|
}
|
|
18
|
-
const { color, setColor, format, theme, getFormattedValue } = context;
|
|
14
|
+
const { color, setColor, format, setFormat, theme, getFormattedValue, showAlpha } = context;
|
|
19
15
|
const [inputValue, setInputValue] = useState(getFormattedValue());
|
|
20
16
|
const [isInvalid, setIsInvalid] = useState(false);
|
|
17
|
+
const [copied, setCopied] = useState(false);
|
|
18
|
+
// Sync inputValue when color changes externally (canvas, sliders, presets)
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!isInvalid) {
|
|
21
|
+
setInputValue(getFormattedValue());
|
|
22
|
+
}
|
|
23
|
+
}, [color, format]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
24
|
+
const handleCopy = async () => {
|
|
25
|
+
try {
|
|
26
|
+
await navigator.clipboard.writeText(getFormattedValue());
|
|
27
|
+
setCopied(true);
|
|
28
|
+
setTimeout(() => setCopied(false), 1500);
|
|
29
|
+
}
|
|
30
|
+
catch (_a) { }
|
|
31
|
+
};
|
|
21
32
|
const handleChange = (e) => {
|
|
22
33
|
const value = e.target.value;
|
|
23
34
|
setInputValue(value);
|
|
24
|
-
// Try to parse the color
|
|
25
35
|
const parsed = parseColor(value);
|
|
26
36
|
if (parsed) {
|
|
27
37
|
setColor(parsed);
|
|
@@ -32,7 +42,6 @@ const ColorPickerInput = ({ className, ...props }) => {
|
|
|
32
42
|
}
|
|
33
43
|
};
|
|
34
44
|
const handleBlur = () => {
|
|
35
|
-
// Reset to current valid color if invalid
|
|
36
45
|
if (isInvalid) {
|
|
37
46
|
setInputValue(getFormattedValue());
|
|
38
47
|
setIsInvalid(false);
|
|
@@ -43,54 +52,37 @@ const ColorPickerInput = ({ className, ...props }) => {
|
|
|
43
52
|
e.currentTarget.blur();
|
|
44
53
|
}
|
|
45
54
|
};
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const newH = parseInt(e.target.value, 10);
|
|
78
|
-
if (!isNaN(newH) && newH >= 0 && newH <= 360) {
|
|
79
|
-
setColor({ ...color, h: newH });
|
|
80
|
-
}
|
|
81
|
-
}, className: "flex-1 text-base", "aria-label": "Hue", placeholder: "H" }), jsx(Input, { type: "number", size: "sm", min: 0, max: 100, value: Math.round(color.s), onChange: (e) => {
|
|
82
|
-
const newS = parseInt(e.target.value, 10);
|
|
83
|
-
if (!isNaN(newS) && newS >= 0 && newS <= 100) {
|
|
84
|
-
setColor({ ...color, s: newS });
|
|
85
|
-
}
|
|
86
|
-
}, className: "flex-1 text-base", "aria-label": "Saturation", placeholder: "S" }), jsx(Input, { type: "number", size: "sm", min: 0, max: 100, value: Math.round(color.l), onChange: (e) => {
|
|
87
|
-
const newL = parseInt(e.target.value, 10);
|
|
88
|
-
if (!isNaN(newL) && newL >= 0 && newL <= 100) {
|
|
89
|
-
setColor({ ...color, l: newL });
|
|
90
|
-
}
|
|
91
|
-
}, className: "flex-1 text-base", "aria-label": "Lightness", placeholder: "L" })] }));
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
55
|
+
// Cycle through formats on click
|
|
56
|
+
const cycleFormat = () => {
|
|
57
|
+
const formats = ['hex', 'rgb', 'hsl'];
|
|
58
|
+
const currentIndex = formats.indexOf(format);
|
|
59
|
+
const nextIndex = (currentIndex + 1) % formats.length;
|
|
60
|
+
setFormat(formats[nextIndex]);
|
|
61
|
+
};
|
|
62
|
+
// Format display label
|
|
63
|
+
const formatLabel = format === 'hex' ? 'Hex' : format === 'rgb' ? 'RGB' : 'HSL';
|
|
64
|
+
// Alpha as percentage
|
|
65
|
+
const alphaPercent = Math.round(color.a * 100);
|
|
66
|
+
// Handle alpha change
|
|
67
|
+
const handleAlphaChange = (e) => {
|
|
68
|
+
const val = parseInt(e.target.value, 10);
|
|
69
|
+
if (!isNaN(val) && val >= 0 && val <= 100) {
|
|
70
|
+
setColor({ ...color, a: val / 100 });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
// For RGB/HSL, show channel values inline
|
|
74
|
+
const getDisplayValue = () => {
|
|
75
|
+
if (format === 'hex')
|
|
76
|
+
return inputValue;
|
|
77
|
+
if (format === 'rgb') {
|
|
78
|
+
const rgb = hslToRgb(color.h, color.s, color.l);
|
|
79
|
+
return `${rgb.r}, ${rgb.g}, ${rgb.b}`;
|
|
80
|
+
}
|
|
81
|
+
return `${Math.round(color.h)}°, ${Math.round(color.s)}%, ${Math.round(color.l)}%`;
|
|
82
|
+
};
|
|
83
|
+
// Get the current color as rgba for the swatch
|
|
84
|
+
const swatchColor = `hsla(${color.h}, ${color.s}%, ${color.l}%, ${color.a})`;
|
|
85
|
+
return (jsxs("div", { className: cn("flex items-stretch border border-[var(--color-border)] rounded-lg overflow-hidden divide-x divide-[var(--color-border)]", className), ...props, children: [jsxs(Button, { variant: "ghost", size: "sm", color: "neutral", onClick: cycleFormat, "aria-label": "Change color format", className: "!rounded-none !border-0 !px-3 !py-2 flex-shrink-0", children: [formatLabel, jsx(ChevronUpDownIcon, { className: "size-4 text-[var(--color-text-muted)]" })] }), jsxs("div", { className: "flex items-center gap-2 px-3 py-2 flex-1 min-w-0", children: [jsx("div", { className: "size-5 rounded-full flex-shrink-0 border border-[var(--color-border)]", style: { backgroundColor: swatchColor } }), jsx("input", { type: "text", value: format === 'hex' ? inputValue : getDisplayValue(), onChange: handleChange, onBlur: handleBlur, onKeyDown: handleKeyDown, readOnly: format !== 'hex', className: cn("flex-1 min-w-0 bg-transparent text-sm font-mono text-[var(--color-text-primary)] outline-none", isInvalid && "text-[var(--color-danger)]"), "aria-label": "Color value" }), jsx(Button, { variant: "ghost", size: "sm", color: "neutral", iconOnly: true, onClick: handleCopy, "aria-label": "Copy color value", title: copied ? "Copied!" : "Copy color value", className: "!p-0 !size-auto flex-shrink-0", children: copied ? (jsx(ClipboardDocumentCheckIcon, { className: "size-4 text-[var(--color-success)]" })) : (jsx(ClipboardDocumentIcon, { className: "size-4" })) })] }), showAlpha && (jsxs("div", { className: "flex items-center px-3 py-2 flex-shrink-0", children: [jsx("input", { type: "number", min: 0, max: 100, value: alphaPercent, onChange: handleAlphaChange, className: "w-10 bg-transparent text-sm text-[var(--color-text-primary)] text-center outline-none font-mono", "aria-label": "Opacity percentage" }), jsx("span", { className: "text-sm text-[var(--color-text-muted)]", children: "%" })] }))] }));
|
|
94
86
|
};
|
|
95
87
|
ColorPickerInput.displayName = "ColorPickerInput";
|
|
96
88
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { useContext } from 'react';
|
|
3
|
+
import { CheckIcon } from '@heroicons/react/16/solid';
|
|
3
4
|
import { cn } from '../../../utils/cn.js';
|
|
4
|
-
import { ColorPickerContext } from './
|
|
5
|
+
import { ColorPickerContext } from './ColorPickerContext.js';
|
|
5
6
|
import { parseColor } from './colorUtils.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* ColorPickerPresets - Grid of preset color swatches
|
|
9
10
|
* Allows quick selection from predefined colors
|
|
10
11
|
*/
|
|
11
|
-
const ColorPickerPresets = ({ colors: customColors, className, ...props }) => {
|
|
12
|
+
const ColorPickerPresets = ({ colors: customColors, label = 'Preset colors', className, ...props }) => {
|
|
12
13
|
const context = useContext(ColorPickerContext);
|
|
13
14
|
if (!context) {
|
|
14
15
|
throw new Error('ColorPickerPresets must be used within a ColorPicker');
|
|
@@ -41,15 +42,15 @@ const ColorPickerPresets = ({ colors: customColors, className, ...props }) => {
|
|
|
41
42
|
Math.abs(parsed.s - color.s) < 1 &&
|
|
42
43
|
Math.abs(parsed.l - color.l) < 1);
|
|
43
44
|
};
|
|
44
|
-
return (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
return (jsxs("div", { className: cn(className), ...props, children: [label !== false && (jsx("div", { className: theme.sectionLabelStyle, children: label })), jsx("div", { className: cn(theme.presetGridStyle), children: colors.map((presetColor, index) => {
|
|
46
|
+
const selected = isSelected(presetColor);
|
|
47
|
+
return (jsxs("button", { type: "button", role: "button", tabIndex: 0, onClick: () => handlePresetClick(presetColor), onKeyDown: (e) => handleKeyDown(e, presetColor), "data-selected": selected ? '' : undefined, className: cn(theme.presetSwatchStyle), "aria-label": `Select color ${presetColor}`, title: presetColor, children: [jsx("div", { className: "absolute inset-0 rounded", style: {
|
|
48
|
+
backgroundImage: 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")',
|
|
49
|
+
backgroundSize: '8px 8px',
|
|
50
|
+
} }), jsx("div", { className: "absolute inset-0 rounded", style: {
|
|
51
|
+
backgroundColor: presetColor,
|
|
52
|
+
} }), selected && (jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsx(CheckIcon, { className: "size-3.5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]" }) }))] }, `${presetColor}-${index}`));
|
|
53
|
+
}) })] }));
|
|
53
54
|
};
|
|
54
55
|
ColorPickerPresets.displayName = 'ColorPickerPresets';
|
|
55
56
|
|