@@ -155,7 +150,7 @@ const RoleEditModal = ({ roleId, isOpen, onClose, onSaved }) => {
{Object.entries(PERMISSION_GROUPS).map(([group, perms]) => (
diff --git a/src/shared/components/ColorPicker.client.js b/src/shared/components/ColorPicker.client.js
new file mode 100644
index 0000000..832147e
--- /dev/null
+++ b/src/shared/components/ColorPicker.client.js
@@ -0,0 +1,150 @@
+'use client';
+
+import { useState, useRef } from 'react';
+
+const PRESET_COLORS = [
+ '#1abc9c', '#2ecc71', '#3498db', '#9b59b6', '#e91e63',
+ '#f1c40f', '#e67e22', '#e74c3c', '#95a5a6', '#607d8b',
+ '#11806a', '#1f8b4c', '#206694', '#71368a', '#ad1457',
+ '#c27c0e', '#a84300', '#992d22', '#979c9f', '#546e7a',
+];
+
+const isValidHex = (hex) => /^#[0-9a-fA-F]{6}$/.test(hex);
+
+const ColorPicker = ({
+ value,
+ onChange,
+ label,
+ description,
+ required = false,
+ disabled = false,
+}) => {
+ const [hexInput, setHexInput] = useState(value || '#6b7280');
+ const nativeRef = useRef(null);
+
+ const selected = isValidHex(value) ? value.toLowerCase() : '#6b7280';
+
+ const handleSwatchClick = (color) => {
+ if (disabled) return;
+ setHexInput(color);
+ onChange?.(color);
+ };
+
+ const handleHexChange = (e) => {
+ const raw = e.target.value;
+ setHexInput(raw);
+ const normalized = raw.startsWith('#') ? raw : `#${raw}`;
+ if (isValidHex(normalized)) {
+ onChange?.(normalized.toLowerCase());
+ }
+ };
+
+ const handleHexBlur = () => {
+ const normalized = hexInput.startsWith('#') ? hexInput : `#${hexInput}`;
+ if (isValidHex(normalized)) {
+ const lower = normalized.toLowerCase();
+ setHexInput(lower);
+ onChange?.(lower);
+ } else {
+ setHexInput(selected);
+ }
+ };
+
+ const handleCustomClick = () => {
+ if (disabled) return;
+ nativeRef.current?.click();
+ };
+
+ const handleNativeChange = (e) => {
+ const color = e.target.value.toLowerCase();
+ setHexInput(color);
+ onChange?.(color);
+ };
+
+ return (
+
+ {label && (
+
+ )}
+ {description && (
+
{description}
+ )}
+
+
+
+ {/* Custom color swatch */}
+
+
+ {/* Preset swatches */}
+ {PRESET_COLORS.map((color) => {
+ const isSelected = selected === color.toLowerCase();
+ return (
+
+ );
+ })}
+
+
+ {/* Hex input */}
+
+
+
+ );
+};
+
+export default ColorPicker;
diff --git a/src/shared/components/index.js b/src/shared/components/index.js
index b72ce0d..94f29d9 100644
--- a/src/shared/components/index.js
+++ b/src/shared/components/index.js
@@ -27,3 +27,4 @@ export { default as Breadcrumb } from './Breadcrumb';
export { default as Switch } from './Switch';
export { default as TagInput } from './TagInput';
export { default as UserAvatar } from './UserAvatar';
+export { default as ColorPicker } from './ColorPicker.client';