@neuctra/ui 0.2.5 → 0.2.6

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.
Files changed (77) hide show
  1. package/dist/components/basic/Accordation.d.ts +9 -13
  2. package/dist/components/basic/Alert.d.ts +10 -23
  3. package/dist/components/basic/Avatar.d.ts +7 -16
  4. package/dist/components/basic/Badge.d.ts +9 -14
  5. package/dist/components/basic/Button.d.ts +9 -19
  6. package/dist/components/basic/CheckboxGroup.d.ts +1 -0
  7. package/dist/components/basic/Container.d.ts +2 -19
  8. package/dist/components/basic/Drawer.d.ts +7 -18
  9. package/dist/components/basic/DropDown.d.ts +20 -40
  10. package/dist/components/basic/FlexView.d.ts +16 -0
  11. package/dist/components/basic/GridView.d.ts +4 -9
  12. package/dist/components/basic/Image.d.ts +10 -31
  13. package/dist/components/basic/Input.d.ts +22 -35
  14. package/dist/components/basic/List.d.ts +6 -20
  15. package/dist/components/basic/Modal.d.ts +8 -8
  16. package/dist/components/basic/RadioGroup.d.ts +1 -0
  17. package/dist/components/basic/Stack.d.ts +4 -14
  18. package/dist/components/basic/SwitchGroup.d.ts +1 -0
  19. package/dist/components/basic/Table.d.ts +6 -1
  20. package/dist/index.cjs.js +82 -52
  21. package/dist/index.cjs.js.map +1 -1
  22. package/dist/index.d.ts +3 -4
  23. package/dist/index.es.js +2060 -2902
  24. package/dist/index.es.js.map +1 -1
  25. package/dist/src/components/basic/Accordation.js +25 -26
  26. package/dist/src/components/basic/Alert.js +33 -138
  27. package/dist/src/components/basic/AudioPlayer.js +54 -40
  28. package/dist/src/components/basic/Avatar.js +41 -154
  29. package/dist/src/components/basic/Badge.js +23 -62
  30. package/dist/src/components/basic/Button.js +24 -97
  31. package/dist/src/components/basic/CheckboxGroup.js +36 -13
  32. package/dist/src/components/basic/Container.js +19 -38
  33. package/dist/src/components/basic/Drawer.js +22 -73
  34. package/dist/src/components/basic/DropDown.js +94 -158
  35. package/dist/src/components/basic/FlexView.js +19 -0
  36. package/dist/src/components/basic/GridView.js +15 -48
  37. package/dist/src/components/basic/Image.js +39 -79
  38. package/dist/src/components/basic/Input.js +68 -109
  39. package/dist/src/components/basic/List.js +20 -62
  40. package/dist/src/components/basic/Modal.js +6 -58
  41. package/dist/src/components/basic/RadioGroup.js +35 -18
  42. package/dist/src/components/basic/Stack.js +19 -72
  43. package/dist/src/components/basic/SwitchGroup.js +42 -16
  44. package/dist/src/components/basic/Table.js +15 -36
  45. package/dist/src/components/basic/Tabs.js +3 -12
  46. package/dist/src/index.js +3 -5
  47. package/dist/types/src/components/basic/Accordation.d.ts +9 -13
  48. package/dist/types/src/components/basic/Alert.d.ts +10 -23
  49. package/dist/types/src/components/basic/Avatar.d.ts +7 -16
  50. package/dist/types/src/components/basic/Badge.d.ts +9 -14
  51. package/dist/types/src/components/basic/Button.d.ts +9 -19
  52. package/dist/types/src/components/basic/CheckboxGroup.d.ts +1 -0
  53. package/dist/types/src/components/basic/Container.d.ts +2 -19
  54. package/dist/types/src/components/basic/Drawer.d.ts +7 -18
  55. package/dist/types/src/components/basic/DropDown.d.ts +20 -40
  56. package/dist/types/src/components/basic/FlexView.d.ts +16 -0
  57. package/dist/types/src/components/basic/GridView.d.ts +4 -9
  58. package/dist/types/src/components/basic/Image.d.ts +10 -31
  59. package/dist/types/src/components/basic/Input.d.ts +22 -35
  60. package/dist/types/src/components/basic/List.d.ts +6 -20
  61. package/dist/types/src/components/basic/Modal.d.ts +8 -8
  62. package/dist/types/src/components/basic/RadioGroup.d.ts +1 -0
  63. package/dist/types/src/components/basic/Stack.d.ts +4 -14
  64. package/dist/types/src/components/basic/SwitchGroup.d.ts +1 -0
  65. package/dist/types/src/components/basic/Table.d.ts +6 -1
  66. package/dist/types/src/index.d.ts +3 -4
  67. package/dist/ui.css +1 -1
  68. package/package.json +2 -1
  69. package/dist/components/basic/Card.d.ts +0 -28
  70. package/dist/components/basic/Flexbox.d.ts +0 -25
  71. package/dist/components/basic/Section.d.ts +0 -36
  72. package/dist/src/components/basic/Card.js +0 -47
  73. package/dist/src/components/basic/Flexbox.js +0 -67
  74. package/dist/src/components/basic/Section.js +0 -100
  75. package/dist/types/src/components/basic/Card.d.ts +0 -28
  76. package/dist/types/src/components/basic/Flexbox.d.ts +0 -25
  77. package/dist/types/src/components/basic/Section.d.ts +0 -36
@@ -1,162 +1,98 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useMemo, useState, forwardRef, } from "react";
4
- /* ----------------------
5
- * Default theme tokens
6
- * ---------------------*/
7
- const THEMES = {
8
- light: {
9
- controlBg: "#ffffff",
10
- menuBg: "#ffffff",
11
- textColor: "#111827",
12
- placeholderColor: "#6b7280",
13
- hoverBg: "#f9fafb",
14
- selectedBg: "#eff6ff",
15
- disabledBg: "#f3f4f6",
16
- disabledTextColor: "#9ca3af",
17
- borderColor: "#e5e7eb",
18
- accentColor: "#3b82f6",
19
- },
20
- dark: {
21
- controlBg: "#1f2937",
22
- menuBg: "#111827",
23
- textColor: "#f9fafb",
24
- placeholderColor: "#9ca3af",
25
- hoverBg: "#374151",
26
- selectedBg: "#2563eb33",
27
- disabledBg: "#374151",
28
- disabledTextColor: "#6b7280",
29
- borderColor: "#374151",
30
- accentColor: "#60a5fa",
31
- },
32
- custom: {},
33
- };
34
- /* ----------------------
35
- * Component
36
- * ---------------------*/
37
- const DropdownInner = (props, ref) => {
38
- const id = useId();
39
- const { options, value, values, defaultValue, defaultValues, onChange, placeholder = "Select...", disabled = false, searchable = false, multiSelect = false, clearable = false, virtualized = false, width = "100%", dropdownMaxHeight = "320px", borderRadius = "8px", boxShadow = "0 8px 28px rgba(0,0,0,0.1)", borderColor, accentColor, theme = "light", menuBg, controlBg, textColor, hoverBg, selectedBg, disabledBg, disabledTextColor, placeholderColor, transitionDuration = "180ms", className, controlClassName, menuClassName, optionClassName, style, controlStyle, menuStyle, optionStyle, iconPrefix, iconSuffix, clearIcon = "×", dropdownIcon = "▾", checkIcon = "✓", } = props;
40
- // merge theme colors
41
- const themeVars = {
42
- ...THEMES[theme],
43
- ...(theme === "custom" ? {} : {}),
44
- };
45
- const colors = {
46
- borderColor: borderColor ?? themeVars.borderColor,
47
- accentColor: accentColor ?? themeVars.accentColor,
48
- controlBg: controlBg ?? themeVars.controlBg,
49
- menuBg: menuBg ?? themeVars.menuBg,
50
- textColor: textColor ?? themeVars.textColor,
51
- hoverBg: hoverBg ?? themeVars.hoverBg,
52
- selectedBg: selectedBg ?? themeVars.selectedBg,
53
- disabledBg: disabledBg ?? themeVars.disabledBg,
54
- disabledTextColor: disabledTextColor ?? themeVars.disabledTextColor,
55
- placeholderColor: placeholderColor ?? themeVars.placeholderColor,
56
- };
57
- /* ----------------- State ----------------- */
58
- const [isOpen, setIsOpen] = useState(false);
59
- const [selected, setSelected] = useState(defaultValues ?? (defaultValue ? [defaultValue] : []));
3
+ import { useEffect, useRef, useState, forwardRef, useImperativeHandle, } from "react";
4
+ import { ChevronDown } from "lucide-react";
5
+ export const Dropdown = forwardRef((props, ref) => {
6
+ const { label, name = "", value, defaultValue, onChange, options = [], placeholder = "Select an option", required, disabled, error, success, helperText, labelIcon: LabelIcon, prefixIcon: PrefixIcon, variant = "dark", primaryTheme = true, primaryColor = "#3b82f6", containerClassName = "", labelClassName = "", triggerClassName = "", dropdownClassName = "", optionClassName = "", className = "", } = props;
7
+ const containerRef = useRef(null);
8
+ useImperativeHandle(ref, () => containerRef.current);
9
+ const [open, setOpen] = useState(false);
10
+ const [localValue, setLocalValue] = useState(defaultValue || "");
11
+ const currentValue = value !== undefined ? value : localValue;
12
+ const selectedOption = options.find((o) => o.value === currentValue);
13
+ /** Close outside */
60
14
  useEffect(() => {
61
- if (value)
62
- setSelected([value]);
63
- if (values)
64
- setSelected(values);
65
- }, [value, values]);
66
- const selectedOptions = useMemo(() => options.filter((o) => selected.includes(o.value)), [options, selected]);
67
- const toggle = () => !disabled && setIsOpen((s) => !s);
68
- const selectValue = (val) => {
69
- if (multiSelect) {
70
- const exists = selected.includes(val);
71
- const next = exists
72
- ? selected.filter((v) => v !== val)
73
- : [...selected, val];
74
- setSelected(next);
75
- onChange?.(next);
76
- }
77
- else {
78
- setSelected([val]);
79
- onChange?.(val);
80
- setIsOpen(false);
81
- }
82
- };
83
- const clearSelection = (e) => {
84
- e?.stopPropagation();
85
- setSelected([]);
86
- onChange?.(multiSelect ? [] : "");
15
+ const handler = (e) => {
16
+ if (containerRef.current &&
17
+ !containerRef.current.contains(e.target)) {
18
+ setOpen(false);
19
+ }
20
+ };
21
+ document.addEventListener("mousedown", handler);
22
+ return () => document.removeEventListener("mousedown", handler);
23
+ }, []);
24
+ const handleSelect = (option) => {
25
+ setLocalValue(option.value);
26
+ onChange?.(name, option.value);
27
+ setOpen(false);
87
28
  };
88
- /* ----------------- Render ----------------- */
89
- return (_jsxs("div", { ref: ref, className: className, style: {
90
- position: "relative",
91
- width,
92
- fontFamily: "Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial",
93
- ...style,
94
- }, children: [_jsxs("div", { className: controlClassName, role: "button", tabIndex: 0, onClick: toggle, style: {
95
- display: "flex",
96
- justifyContent: "space-between",
97
- alignItems: "center",
98
- background: colors.controlBg,
99
- color: colors.textColor,
100
- padding: "0.5rem 0.75rem",
101
- border: `1px solid ${isOpen ? colors.accentColor : colors.borderColor}`,
102
- borderRadius,
103
- cursor: disabled ? "not-allowed" : "pointer",
104
- boxShadow: isOpen ? `0 0 0 3px ${colors.accentColor}33` : undefined,
105
- transition: `all ${transitionDuration} ease`,
106
- ...controlStyle,
107
- }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [iconPrefix, selectedOptions.length ? (_jsx("span", { children: selectedOptions.map((s) => s.label).join(", ") })) : (_jsx("span", { style: { color: colors.placeholderColor }, children: placeholder }))] }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [clearable && selected.length > 0 && (_jsx("button", { onClick: clearSelection, style: {
108
- background: "transparent",
109
- border: "none",
110
- color: colors.textColor,
111
- cursor: "pointer",
112
- }, children: clearIcon })), iconSuffix, _jsx("div", { style: {
113
- transform: isOpen ? "rotate(180deg)" : "rotate(0deg)",
114
- transition: `transform ${transitionDuration}`,
115
- }, children: dropdownIcon })] })] }), isOpen && (_jsx("ul", { className: menuClassName, style: {
116
- position: "absolute",
117
- zIndex: 100,
118
- top: "calc(100% + 4px)",
119
- left: 0,
120
- right: 0,
121
- background: colors.menuBg,
122
- border: `1px solid ${colors.borderColor}`,
123
- borderRadius,
124
- boxShadow,
125
- maxHeight: dropdownMaxHeight,
126
- overflowY: "auto",
127
- transition: `opacity ${transitionDuration} ease`,
128
- ...menuStyle,
129
- }, children: options.map((opt) => {
130
- const isSelected = selected.includes(opt.value);
131
- return (_jsxs("li", { onClick: () => !opt.disabled && selectValue(opt.value), className: optionClassName, style: {
132
- display: "flex",
133
- alignItems: "center",
134
- gap: 8,
135
- padding: "0.5rem 0.75rem",
136
- background: opt.disabled
137
- ? colors.disabledBg
138
- : isSelected
139
- ? colors.selectedBg
140
- : "transparent",
141
- color: opt.disabled
142
- ? colors.disabledTextColor
143
- : colors.textColor,
144
- cursor: opt.disabled ? "not-allowed" : "pointer",
145
- borderRadius: 6,
146
- userSelect: "none",
147
- ...optionStyle,
148
- }, onMouseEnter: (e) => {
149
- if (!opt.disabled && !isSelected)
150
- e.currentTarget.style.backgroundColor = colors.hoverBg;
151
- }, onMouseLeave: (e) => {
152
- if (!opt.disabled && !isSelected)
153
- e.currentTarget.style.backgroundColor = "transparent";
154
- }, children: [multiSelect && (_jsx("span", { style: { width: 18, textAlign: "center" }, children: isSelected ? checkIcon : "" })), opt.icon && _jsx("span", { children: opt.icon }), _jsxs("div", { style: { flex: 1 }, children: [opt.label, opt.description && (_jsx("div", { style: {
155
- fontSize: 12,
156
- color: colors.placeholderColor,
157
- marginTop: 2,
158
- }, children: opt.description }))] })] }, opt.value));
159
- }) }))] }));
160
- };
161
- export const Dropdown = forwardRef(DropdownInner);
162
- export default Dropdown;
29
+ /** 🎨 Theme base */
30
+ const theme = {
31
+ dark: {
32
+ bg: "bg-zinc-900",
33
+ text: "text-white",
34
+ border: "border-zinc-800",
35
+ dropdown: "bg-black border-zinc-800",
36
+ option: "text-zinc-300 hover:bg-zinc-800",
37
+ },
38
+ light: {
39
+ bg: "bg-white",
40
+ text: "text-gray-900",
41
+ border: "border-gray-300",
42
+ dropdown: "bg-white border-gray-200",
43
+ option: "text-gray-700 hover:bg-gray-100",
44
+ },
45
+ }[variant];
46
+ /** 🎨 Dynamic styles */
47
+ const dynamicPrimary = !primaryTheme
48
+ ? { color: primaryColor }
49
+ : {};
50
+ const dynamicBorder = !primaryTheme
51
+ ? { borderColor: primaryColor }
52
+ : {};
53
+ const dynamicBgActive = !primaryTheme
54
+ ? { backgroundColor: `${primaryColor}20` } // light tint
55
+ : {};
56
+ /** Border state */
57
+ const borderState = error
58
+ ? "border-rose-500"
59
+ : success
60
+ ? "border-emerald-500"
61
+ : theme.border;
62
+ const paddingLeft = PrefixIcon ? "pl-12 pr-10" : "px-4";
63
+ return (_jsxs("div", { ref: containerRef, className: `w-full space-y-1 ${containerClassName} ${className}`, children: [label && (_jsxs("label", { className: `flex items-center gap-2 text-[13px] font-medium ${labelClassName}`, children: [LabelIcon && (_jsx(LabelIcon, { className: primaryTheme ? "w-4 h-4 text-[var(--primary)]" : "w-4 h-4", style: !primaryTheme ? dynamicPrimary : undefined })), label, required && _jsx("span", { className: "text-rose-500", children: "*" })] })), _jsxs("div", { className: "relative group", children: [PrefixIcon && (_jsx("div", { className: "absolute left-4 top-1/2 -translate-y-1/2 z-10", children: _jsx(PrefixIcon, { className: primaryTheme
64
+ ? "w-4 h-4 text-zinc-500 group-focus-within:text-[var(--primary)]"
65
+ : "w-4 h-4 text-zinc-500", style: !primaryTheme ? dynamicPrimary : undefined }) })), _jsxs("button", { type: "button", disabled: disabled, onClick: () => setOpen((p) => !p), style: !primaryTheme ? dynamicBorder : undefined, className: `
66
+ w-full py-2.5 rounded-lg text-left text-sm
67
+ border transition-all duration-200
68
+ ${theme.bg} ${theme.text}
69
+ ${paddingLeft}
70
+ ${borderState}
71
+ focus:ring-2
72
+ ${primaryTheme
73
+ ? "focus:ring-[var(--primary)]/30 focus:border-[var(--primary)]"
74
+ : ""}
75
+ disabled:opacity-50 disabled:cursor-not-allowed
76
+ ${triggerClassName}
77
+ `, children: [_jsx("span", { className: selectedOption ? theme.text : "text-zinc-500", children: selectedOption ? selectedOption.label : placeholder }), _jsx(ChevronDown, { className: `absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 transition-transform ${open ? "rotate-180" : ""} text-zinc-400` })] }), open && (_jsx("div", { className: `
78
+ absolute z-50 mt-2 w-full rounded-xl border shadow-xl overflow-hidden
79
+ ${theme.dropdown}
80
+ ${dropdownClassName}
81
+ `, children: _jsx("ul", { className: "max-h-60 overflow-y-auto", children: options.map((opt) => {
82
+ const isActive = currentValue === opt.value;
83
+ return (_jsxs("li", { onClick: () => handleSelect(opt), style: !primaryTheme && isActive ? dynamicBgActive : undefined, className: `
84
+ px-4 py-3 cursor-pointer text-sm flex items-center gap-2
85
+ ${isActive
86
+ ? primaryTheme
87
+ ? "bg-[var(--primary)]/15 text-[var(--primary)]"
88
+ : ""
89
+ : theme.option}
90
+ ${optionClassName}
91
+ `, children: [opt.icon && _jsx("span", { children: opt.icon }), opt.label] }, opt.value));
92
+ }) }) }))] }), (helperText || error) && (_jsx("p", { className: `text-xs ${error
93
+ ? "text-rose-500"
94
+ : success
95
+ ? "text-emerald-500"
96
+ : "text-zinc-500"}`, children: typeof error === "string" ? error : helperText }))] }));
97
+ });
98
+ Dropdown.displayName = "Dropdown";
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from "clsx";
3
+ /**
4
+ * Converts responsive prop to Tailwind class string
5
+ */
6
+ const toResponsiveClass = (prefix, value) => {
7
+ if (value == null)
8
+ return "";
9
+ if (typeof value !== "object")
10
+ return `${prefix}-${value}`;
11
+ return Object.entries(value)
12
+ .map(([bp, val]) => `${bp}:${prefix}-${val}`)
13
+ .join(" ");
14
+ };
15
+ export const FlexView = ({ direction = { sm: "column", md: "row" }, align = "center", justify = "between", wrap = "wrap", gap, padding, margin, className, children, }) => {
16
+ const classes = clsx("flex", toResponsiveClass("flex", direction), toResponsiveClass("items", align), toResponsiveClass("justify", justify), toResponsiveClass("flex", wrap), // wrap classes: flex-wrap, flex-nowrap, flex-wrap-reverse
17
+ gap ? toResponsiveClass("gap", gap) : "", padding ? toResponsiveClass("p", padding) : "", margin ? toResponsiveClass("m", margin) : "", className);
18
+ return _jsx("div", { className: classes, children: children });
19
+ };
@@ -1,51 +1,18 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useEffect, useMemo } from "react";
3
- const getScreenSize = (width) => {
4
- if (width < 768)
5
- return "sm";
6
- if (width < 1024)
7
- return "md";
8
- return "lg";
2
+ import clsx from "clsx";
3
+ /**
4
+ * Converts responsive prop to Tailwind class string
5
+ */
6
+ const toResponsiveClass = (prefix, value) => {
7
+ if (value == null)
8
+ return "";
9
+ if (typeof value !== "object")
10
+ return `${prefix}-${value}`;
11
+ return Object.entries(value)
12
+ .map(([bp, val]) => `${bp}:${prefix}-${val}`)
13
+ .join(" ");
9
14
  };
10
- export const GridView = ({ columns = { sm: 1, md: 2, lg: 3 }, gap = 16, padding = 0, alignItems = "stretch", justifyItems = "stretch", backgroundColor = "transparent", width = "100%", maxWidth = "100%", height = "auto", margin = 0, style, className = "", children, }) => {
11
- const [screenSize, setScreenSize] = useState("lg");
12
- useEffect(() => {
13
- setScreenSize(getScreenSize(window.innerWidth));
14
- const onResize = () => setScreenSize(getScreenSize(window.innerWidth));
15
- window.addEventListener("resize", onResize);
16
- return () => window.removeEventListener("resize", onResize);
17
- }, []);
18
- const resolvedColumns = useMemo(() => {
19
- if (typeof columns === "number")
20
- return columns;
21
- return columns[screenSize] ?? 1;
22
- }, [columns, screenSize]);
23
- const styles = useMemo(() => ({
24
- display: "grid",
25
- gridTemplateColumns: `repeat(${resolvedColumns}, 1fr)`,
26
- gap: typeof gap === "number" ? `${gap}px` : gap,
27
- padding: typeof padding === "number" ? `${padding}px` : padding,
28
- margin: typeof margin === "number" ? `${margin}px` : margin,
29
- alignItems,
30
- justifyItems,
31
- backgroundColor,
32
- width,
33
- maxWidth,
34
- height,
35
- boxSizing: "border-box",
36
- ...style,
37
- }), [
38
- resolvedColumns,
39
- gap,
40
- padding,
41
- margin,
42
- alignItems,
43
- justifyItems,
44
- backgroundColor,
45
- width,
46
- maxWidth,
47
- height,
48
- style,
49
- ]);
50
- return (_jsx("div", { style: styles, className: className, children: children }));
15
+ export const GridView = ({ columns = { sm: 1, md: 2, lg: 3 }, gap = 4, padding = 4, alignItems = "stretch", justifyItems = "stretch", className, children, }) => {
16
+ const classes = clsx("grid", toResponsiveClass("grid-cols", columns), toResponsiveClass("gap", gap), toResponsiveClass("p", padding), alignItems !== "stretch" ? `items-${alignItems}` : "items-stretch", justifyItems !== "stretch" ? `justify-items-${justifyItems}` : "justify-items-stretch", className);
17
+ return _jsx("div", { className: classes, children: children });
51
18
  };
@@ -1,56 +1,32 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useCallback } from "react";
3
- /**
4
- * Industry-Standard Image Component
5
- * - SEO & accessibility optimized
6
- * - Lazy loading & responsive
7
- * - Fully customizable styling
8
- * - Smooth hover transitions
9
- * - Overlay and SVG support
10
- */
11
- export const Image = ({ src, alt = "Image", title, width = "100%", height = "auto", borderRadius = "8px", borderColor = "transparent", borderStyle = "solid", borderWidth = "0px", shadow = false, boxShadow, opacity = 1, objectFit = "cover", overlayText, overlayColor = "rgba(0,0,0,0.5)", svgIcon, responsive = false, padding, margin, lazyLoad = true, hoverOpacity, hoverShadow = false, hoverScale = 1, hoverRotate = 0, transitionDuration = "0.3s", overflow = "hidden", className, style, onClick, onLoad, onError, }) => {
12
- // Memoized base style for performance
13
- const baseStyle = useMemo(() => ({
14
- width: responsive ? "100%" : width,
15
- height: responsive ? "auto" : height,
16
- borderRadius,
17
- border: `${borderWidth} ${borderStyle} ${borderColor}`,
3
+ import { useMemo } from "react";
4
+ export const Image = ({ src, alt = "Image", title, width = "w-full", height = "h-auto", rounded = "rounded-lg", borderColor = "transparent", borderWidth = "0", shadow = false, opacity = 1, objectFit = "cover", overlayText, overlayColor = "rgba(0,0,0,0.5)", svgIcon, responsive = false, p, m, hoverScale = 1.05, hoverRotate = 0, hoverOpacity, hoverShadow = false, transitionDuration = "300ms", overflow = "hidden", className = "", style, onClick, onLoad, onError, }) => {
5
+ /** Base Tailwind classes */
6
+ const baseClasses = useMemo(() => {
7
+ const classes = [
8
+ "relative inline-block transition-all",
9
+ shadow ? "shadow-md" : "",
10
+ rounded,
11
+ p ?? "",
12
+ m ?? "",
13
+ responsive ? "w-full h-auto" : "",
14
+ className,
15
+ ];
16
+ return classes.join(" ");
17
+ }, [shadow, rounded, p, m, responsive, className]);
18
+ /** Inline styles for custom widths, hover, borders, etc. */
19
+ const inlineStyles = useMemo(() => ({
20
+ width: typeof width === "number" ? width : undefined,
21
+ height: typeof height === "number" ? height : undefined,
18
22
  objectFit,
19
23
  opacity,
20
- boxShadow: shadow
21
- ? boxShadow || "0 4px 12px rgba(0,0,0,0.15)"
22
- : "none",
23
- transition: `all ${transitionDuration} ease`,
24
- display: "block",
25
- }), [
26
- responsive,
27
- width,
28
- height,
29
- borderRadius,
30
24
  borderColor,
31
- borderStyle,
32
25
  borderWidth,
33
- objectFit,
34
- opacity,
35
- shadow,
36
- boxShadow,
37
- transitionDuration,
38
- ]);
39
- // ✅ Hover effects
40
- const handleMouseEnter = useCallback((e) => {
41
- e.currentTarget.style.opacity =
42
- hoverOpacity !== undefined ? hoverOpacity.toString() : "1";
43
- e.currentTarget.style.boxShadow = hoverShadow
44
- ? "0 8px 20px rgba(0,0,0,0.3)"
45
- : baseStyle.boxShadow || "none";
46
- e.currentTarget.style.transform = `scale(${hoverScale}) rotate(${hoverRotate}deg)`;
47
- }, [hoverOpacity, hoverShadow, hoverScale, hoverRotate, baseStyle.boxShadow]);
48
- const handleMouseLeave = useCallback((e) => {
49
- e.currentTarget.style.opacity = baseStyle.opacity?.toString() || "1";
50
- e.currentTarget.style.boxShadow = baseStyle.boxShadow || "none";
51
- e.currentTarget.style.transform = "scale(1) rotate(0deg)";
52
- }, [baseStyle]);
53
- // ✅ Overflow control
26
+ transition: `all ${transitionDuration} ease`,
27
+ ...style,
28
+ }), [width, height, objectFit, opacity, borderColor, borderWidth, transitionDuration, style]);
29
+ /** Overflow handling */
54
30
  const overflowStyles = useMemo(() => {
55
31
  switch (overflow) {
56
32
  case "x":
@@ -61,35 +37,19 @@ export const Image = ({ src, alt = "Image", title, width = "100%", height = "aut
61
37
  return { overflow };
62
38
  }
63
39
  }, [overflow]);
64
- return (_jsxs("div", { className: className, role: "img", "aria-label": alt, title: title || alt, onClick: onClick, style: {
65
- width: responsive ? "100%" : width,
66
- height: responsive ? "auto" : height,
67
- padding,
68
- margin,
69
- position: "relative",
70
- cursor: onClick ? "pointer" : "default",
71
- display: "inline-block",
72
- transition: `all ${transitionDuration} ease`,
73
- ...overflowStyles,
74
- ...style,
75
- }, children: [svgIcon ? (_jsx("div", { style: {
76
- width: "100%",
77
- height: "100%",
78
- display: "flex",
79
- alignItems: "center",
80
- justifyContent: "center",
81
- }, children: svgIcon })) : (_jsx("img", { src: src, alt: alt, title: title || alt, loading: lazyLoad ? "lazy" : "eager", style: baseStyle, onClick: onClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onLoad: onLoad, onError: onError, decoding: "async", fetchPriority: "high" })), overlayText && (_jsx("div", { style: {
82
- position: "absolute",
83
- inset: 0,
84
- backgroundColor: overlayColor,
85
- color: "#fff",
86
- display: "flex",
87
- alignItems: "center",
88
- justifyContent: "center",
89
- fontWeight: "bold",
90
- fontSize: "1.1rem",
91
- textAlign: "center",
92
- padding: "1rem",
93
- boxSizing: "border-box",
94
- }, children: overlayText }))] }));
40
+ /** Hover effects */
41
+ const handleMouseEnter = (e) => {
42
+ if (hoverOpacity !== undefined)
43
+ e.currentTarget.style.opacity = hoverOpacity.toString();
44
+ e.currentTarget.style.transform = `scale(${hoverScale}) rotate(${hoverRotate}deg)`;
45
+ if (hoverShadow)
46
+ e.currentTarget.style.boxShadow = "0 8px 20px rgba(0,0,0,0.3)";
47
+ };
48
+ const handleMouseLeave = (e) => {
49
+ e.currentTarget.style.opacity = opacity.toString();
50
+ e.currentTarget.style.transform = "scale(1) rotate(0deg)";
51
+ if (hoverShadow)
52
+ e.currentTarget.style.boxShadow = "";
53
+ };
54
+ return (_jsxs("div", { role: "img", "aria-label": alt, title: title || alt, onClick: onClick, className: baseClasses, style: { position: "relative", cursor: onClick ? "pointer" : "default", ...overflowStyles }, children: [svgIcon ? (_jsx("div", { className: "w-full h-full flex items-center justify-center", children: svgIcon })) : (_jsx("img", { src: src, alt: alt, title: title || alt, loading: "lazy", style: inlineStyles, className: "block", onLoad: onLoad, onError: onError, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave })), overlayText && (_jsx("div", { style: { backgroundColor: overlayColor }, className: "absolute inset-0 flex items-center justify-center text-white font-bold text-center p-4", children: overlayText }))] }));
95
55
  };