@prasadj28/react-neu 1.0.27 → 1.0.28

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 (54) hide show
  1. package/package.json +28 -19
  2. package/src/App.tsx +626 -0
  3. package/src/components/Button/Button.css.ts +28 -0
  4. package/src/components/Button/Button.tsx +85 -0
  5. package/src/components/Card/Card.css.ts +11 -0
  6. package/src/components/Card/Card.tsx +106 -0
  7. package/src/components/Checkbox/Checkbox.css.ts +49 -0
  8. package/src/components/Checkbox/Checkbox.tsx +134 -0
  9. package/src/components/Icon/Icon.css.ts +39 -0
  10. package/src/components/Icon/Icon.tsx +163 -0
  11. package/src/components/Icon/IconPaths.tsx +80 -0
  12. package/src/components/Radio/Radio.css.ts +57 -0
  13. package/src/components/Radio/Radio.tsx +130 -0
  14. package/src/components/Ridge/Ridge.css.ts +11 -0
  15. package/src/components/Ridge/Ridge.tsx +66 -0
  16. package/src/components/Slider/Slider.css.ts +36 -0
  17. package/src/components/Slider/Slider.tsx +175 -0
  18. package/src/components/TextInput/TextInput.tsx +71 -0
  19. package/src/components/Toggle/Toggle.css.ts +55 -0
  20. package/src/components/Toggle/Toggle.tsx +167 -0
  21. package/src/index.ts +44 -0
  22. package/{react-neu/src → src}/main.tsx +0 -1
  23. package/src/styles/neumorphicEngine.ts +159 -0
  24. package/src/styles/theme.css.ts +38 -0
  25. package/src/styles/types.ts +45 -0
  26. package/tsconfig.json +26 -0
  27. package/LICENSE +0 -21
  28. package/react-neu/package-lock.json +0 -3753
  29. package/react-neu/package.json +0 -33
  30. package/react-neu/src/App.tsx +0 -30
  31. package/react-neu/src/components/Button/Button.css.ts +0 -12
  32. package/react-neu/src/components/Button/Button.tsx +0 -92
  33. package/react-neu/src/components/TextInput/TextInput.tsx +0 -72
  34. package/react-neu/src/index.ts +0 -3
  35. package/react-neu/src/styles/defaults.css.ts +0 -16
  36. package/react-neu/src/styles/filterDomProps.ts +0 -28
  37. package/react-neu/src/styles/global.css.ts +0 -12
  38. package/react-neu/src/styles/neumorphicEngine.ts +0 -110
  39. package/react-neu/src/styles/shadowUtils.ts +0 -68
  40. package/react-neu/src/styles/theme.css.ts +0 -26
  41. package/react-neu/tsconfig.json +0 -7
  42. /package/{react-neu/README.md → README.md} +0 -0
  43. /package/{react-neu/eslint.config.js → eslint.config.js} +0 -0
  44. /package/{react-neu/index.html → index.html} +0 -0
  45. /package/{react-neu/public → public}/vite.svg +0 -0
  46. /package/{react-neu/src → src}/assets/react.svg +0 -0
  47. /package/{react-neu/src → src}/components/TextInput/TextInput.css.ts +0 -0
  48. /package/{react-neu/src → src}/components/index.ts +0 -0
  49. /package/{react-neu/src → src}/styles/colorUtils.ts +0 -0
  50. /package/{react-neu/src → src}/utils/colorUtils.ts +0 -0
  51. /package/{react-neu/src → src}/utils/neuEngine.ts +0 -0
  52. /package/{react-neu/tsconfig.app.json → tsconfig.app.json} +0 -0
  53. /package/{react-neu/tsconfig.node.json → tsconfig.node.json} +0 -0
  54. /package/{react-neu/vite.config.ts → vite.config.ts} +0 -0
@@ -0,0 +1,80 @@
1
+ import React from "react";
2
+
3
+ export type IconName =
4
+ | "heart" | "bookmark" | "comment" | "star"
5
+ | "circle" | "square" | "triangle" | "diamond"
6
+ | "user" | "gear" | "bell" | "search" | "share"
7
+ | "mail" | "send" | "camera" | "mic";
8
+
9
+ // FIX: Changed 'JSX.Element' to 'React.ReactElement'
10
+ export const iconRegistry: Record<IconName, React.ReactElement> = {
11
+ // --- SHAPES ---
12
+ heart: <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />,
13
+ bookmark: <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />,
14
+ comment: <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />,
15
+
16
+ // --- GEOMETRIC ---
17
+ circle: <circle cx="12" cy="12" r="10" />,
18
+ square: <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />,
19
+ triangle: <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />,
20
+ diamond: <path d="M12 2L2 12l10 10 10-10L12 2z" />,
21
+
22
+ // --- ICONS (Wrapped in <g> if multiple paths) ---
23
+ user: (
24
+ <g>
25
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
26
+ <circle cx="12" cy="7" r="4" />
27
+ </g>
28
+ ),
29
+ gear: (
30
+ <path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
31
+ ),
32
+ bell: (
33
+ <g>
34
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
35
+ <path d="M13.73 21a2 2 0 0 1-3.46 0" />
36
+ </g>
37
+ ),
38
+ star: <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />,
39
+ search: (
40
+ <g>
41
+ <circle cx="11" cy="11" r="8" />
42
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
43
+ </g>
44
+ ),
45
+ share: (
46
+ <g>
47
+ <circle cx="18" cy="5" r="3" />
48
+ <circle cx="6" cy="12" r="3" />
49
+ <circle cx="18" cy="19" r="3" />
50
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
51
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
52
+ </g>
53
+ ),
54
+ mail: (
55
+ <g>
56
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
57
+ <polyline points="22,6 12,13 2,6" />
58
+ </g>
59
+ ),
60
+ send: (
61
+ <g>
62
+ <line x1="22" y1="2" x2="11" y2="13" />
63
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
64
+ </g>
65
+ ),
66
+ camera: (
67
+ <g>
68
+ <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
69
+ <circle cx="12" cy="13" r="4" />
70
+ </g>
71
+ ),
72
+ mic: (
73
+ <g>
74
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
75
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
76
+ <line x1="12" y1="19" x2="12" y2="23" />
77
+ <line x1="8" y1="23" x2="16" y2="23" />
78
+ </g>
79
+ ),
80
+ };
@@ -0,0 +1,57 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const radioLabel = style({
4
+ display: "inline-flex",
5
+ alignItems: "center",
6
+ gap: "12px",
7
+ cursor: "pointer",
8
+ userSelect: "none",
9
+ fontSize: "1rem",
10
+ position: "relative",
11
+ });
12
+
13
+ export const hiddenRadio = style({
14
+ position: "absolute",
15
+ opacity: 0,
16
+ cursor: "pointer",
17
+ height: 0,
18
+ width: 0,
19
+ });
20
+
21
+ export const visualBox = style({
22
+ position: "relative",
23
+ display: "flex",
24
+ alignItems: "center",
25
+ justifyContent: "center",
26
+ transition: "all 0.3s ease",
27
+ boxSizing: "border-box",
28
+ });
29
+
30
+ export const iconContainer = style({
31
+ position: "absolute",
32
+ display: "flex",
33
+ alignItems: "center",
34
+ justifyContent: "center",
35
+ width: "100%",
36
+ height: "100%",
37
+ pointerEvents: "none",
38
+ });
39
+
40
+ // Standard Radio Dot
41
+ export const radioDot = style({
42
+ width: "50%",
43
+ height: "50%",
44
+ borderRadius: "50%",
45
+ backgroundColor: "currentColor",
46
+ transition: "transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.2s ease",
47
+ });
48
+
49
+ // For Check/Cross styles
50
+ export const svgIcon = style({
51
+ width: "60%",
52
+ height: "60%",
53
+ strokeLinecap: "round",
54
+ strokeLinejoin: "round",
55
+ fill: "none",
56
+ transition: "all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)",
57
+ });
@@ -0,0 +1,130 @@
1
+ import React from "react";
2
+ import { radioLabel, hiddenRadio, visualBox, iconContainer, radioDot, svgIcon } from "./Radio.css";
3
+ import { getNeumorphicStyle } from "../../styles/neumorphicEngine";
4
+ import type { NeumorphicProps } from "../../styles/types";
5
+
6
+ interface RadioProps extends NeumorphicProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "color"> {
7
+ /**
8
+ * Visual style when selected.
9
+ * - 'dot': Standard radio circle.
10
+ * - 'check': Checkmark.
11
+ * - 'cross': X mark.
12
+ * - 'fill': Sinks in (Inset) without an icon.
13
+ * @default "dot"
14
+ */
15
+ selectionStyle?: "dot" | "check" | "cross" | "fill";
16
+
17
+ /**
18
+ * Color of the Dot/Icon.
19
+ * @default "#1e1e1e"
20
+ */
21
+ selectedColor?: string;
22
+
23
+ /**
24
+ * Size of the radio button.
25
+ * @default "26px"
26
+ */
27
+ size?: number | string;
28
+ }
29
+
30
+ export const NeuRadio: React.FC<RadioProps> = ({
31
+ // Neumorphic Defaults
32
+ variant = "pop",
33
+ surface = "flat",
34
+ color,
35
+ elevation = 2,
36
+ intensity,
37
+ shape = "circle", // Default for Radio
38
+ angleDeg,
39
+ border = false,
40
+ ridge = false,
41
+
42
+ // Custom Logic
43
+ selectionStyle = "dot",
44
+ selectedColor = "#1e1e1e",
45
+ size = "26px",
46
+
47
+ // Standard Props
48
+ checked,
49
+ defaultChecked,
50
+ children,
51
+ className,
52
+ style,
53
+ disabled,
54
+ ...htmlProps
55
+ }) => {
56
+ // Controlled/Uncontrolled logic
57
+ const isChecked = checked || false;
58
+
59
+ // LOGIC: If checked, we go "active" (pressed/sink) unless it's just a dot on a flat surface
60
+ const engineState = isChecked ? "active" : "default";
61
+
62
+ const boxStyle = getNeumorphicStyle({
63
+ variant,
64
+ surface,
65
+ color,
66
+ elevation,
67
+ intensity,
68
+ shape,
69
+ angleDeg,
70
+ border,
71
+ ridge,
72
+ state: engineState,
73
+ });
74
+
75
+ const showIcon = selectionStyle !== "fill";
76
+
77
+ return (
78
+ <label
79
+ className={`${radioLabel} ${className || ""}`}
80
+ style={{ opacity: disabled ? 0.6 : 1, ...style }}
81
+ >
82
+ <input
83
+ type="radio"
84
+ className={hiddenRadio}
85
+ checked={isChecked}
86
+ disabled={disabled}
87
+ {...htmlProps}
88
+ />
89
+
90
+ <div
91
+ className={visualBox}
92
+ style={{
93
+ width: size,
94
+ height: size,
95
+ ...boxStyle,
96
+ color: selectedColor, // Inherited by radioDot
97
+ }}
98
+ >
99
+ {showIcon && (
100
+ <div className={iconContainer} style={{ opacity: isChecked ? 1 : 0 }}>
101
+ {selectionStyle === "dot" ? (
102
+ // Standard Dot
103
+ <div
104
+ className={radioDot}
105
+ style={{ transform: isChecked ? "scale(1)" : "scale(0)" }}
106
+ />
107
+ ) : (
108
+ // SVG Icons (Check/Cross)
109
+ <svg
110
+ viewBox="0 0 24 24"
111
+ className={svgIcon}
112
+ style={{ stroke: selectedColor, strokeWidth: 3 }}
113
+ >
114
+ {selectionStyle === "check" && <polyline points="20 6 9 17 4 12" />}
115
+ {selectionStyle === "cross" && (
116
+ <>
117
+ <line x1="18" y1="6" x2="6" y2="18" />
118
+ <line x1="6" y1="6" x2="18" y2="18" />
119
+ </>
120
+ )}
121
+ </svg>
122
+ )}
123
+ </div>
124
+ )}
125
+ </div>
126
+
127
+ {children && <span>{children}</span>}
128
+ </label>
129
+ );
130
+ };
@@ -0,0 +1,11 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const ridgeContainer = style({
4
+ display: "inline-flex",
5
+ alignItems: "center",
6
+ justifyContent: "center",
7
+ boxSizing: "border-box",
8
+ // Ensures no gaps between the ridge border and the child content
9
+ padding: 0,
10
+ overflow: "hidden",
11
+ });
@@ -0,0 +1,66 @@
1
+ import React from "react";
2
+ import { ridgeContainer } from "./Ridge.css";
3
+ import { getNeumorphicStyle } from "../../styles/neumorphicEngine";
4
+ import type { NeumorphicProps } from "../../styles/types";
5
+
6
+ interface RidgeProps extends NeumorphicProps {
7
+ children: React.ReactNode;
8
+ /**
9
+ * Sets the thickness of the ridge frame (padding).
10
+ * Can be a number (pixels) or string (e.g., "1rem").
11
+ * @default "10px"
12
+ */
13
+ ridgeWidth?: number | string;
14
+ className?: string;
15
+ style?: React.CSSProperties;
16
+ }
17
+
18
+ export const NeuRidge: React.FC<RidgeProps> = ({
19
+ children,
20
+ // 1. Expose all Neumorphic Props with "Ridge-like" defaults
21
+ variant = "flat", // Default to flat surface
22
+ surface = "flat",
23
+ color,
24
+ shape = "rounded",
25
+ elevation = 2,
26
+ intensity,
27
+ angleDeg,
28
+ border = true, // Default to showing the seam
29
+ ridge = false,
30
+
31
+ // 2. New specific prop
32
+ ridgeWidth = "10px",
33
+
34
+ className,
35
+ style,
36
+ }) => {
37
+ // Generate styles allowing full customization
38
+ const ridgeStyle = getNeumorphicStyle({
39
+ variant,
40
+ surface,
41
+ border,
42
+ color,
43
+ shape,
44
+ elevation,
45
+ intensity,
46
+ angleDeg,
47
+ ridge,
48
+ state: "default",
49
+ });
50
+
51
+ return (
52
+ <div
53
+ className={`${ridgeContainer} ${className || ""}`}
54
+ style={{
55
+ ...ridgeStyle,
56
+ ...style,
57
+ // Apply the custom width as padding
58
+ padding: typeof ridgeWidth === 'number' ? `${ridgeWidth}px` : ridgeWidth,
59
+ // Ridges are usually static structural elements
60
+ transition: style?.transition || "none",
61
+ }}
62
+ >
63
+ {children}
64
+ </div>
65
+ );
66
+ };
@@ -0,0 +1,36 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const sliderContainer = style({
4
+ position: "relative",
5
+ width: "100%",
6
+ height: "32px", // Increased height for better touch target
7
+ display: "flex",
8
+ alignItems: "center",
9
+ touchAction: "none",
10
+ cursor: "pointer",
11
+ userSelect: "none",
12
+ });
13
+
14
+ export const sliderTrack = style({
15
+ width: "100%",
16
+ height: "12px", // Slightly thicker track for "Ridge" visibility
17
+ position: "relative",
18
+ });
19
+
20
+ export const sliderThumb = style({
21
+ position: "absolute",
22
+ top: "50%",
23
+ // We don't set 'left' here, it's set inline via React
24
+ width: "24px",
25
+ height: "24px",
26
+ borderRadius: "50%",
27
+ // Center the thumb vertically, but horizontally we handle it via calculation to prevent overflow
28
+ transform: "translate(-50%, -50%)",
29
+ cursor: "grab",
30
+ transition: "box-shadow 0.2s ease, transform 0.1s ease",
31
+ zIndex: 2,
32
+ ":active": {
33
+ cursor: "grabbing",
34
+ transform: "translate(-50%, -50%) scale(0.95)", // Subtle shrink on press
35
+ },
36
+ });
@@ -0,0 +1,175 @@
1
+ import React, { useRef, useState, useEffect } from "react";
2
+ import { sliderContainer, sliderTrack, sliderThumb } from "./Slider.css";
3
+ import { getNeumorphicStyle } from "../../styles/neumorphicEngine";
4
+ import type { NeumorphicProps } from "../../styles/types";
5
+
6
+ interface SliderProps extends NeumorphicProps {
7
+ min?: number;
8
+ max?: number;
9
+ step?: number;
10
+ value: number;
11
+ onChange: (value: number) => void;
12
+ className?: string;
13
+ style?: React.CSSProperties;
14
+ disabled?: boolean;
15
+ thumbSize?: string;
16
+ }
17
+
18
+ export const NeuSlider: React.FC<SliderProps> = ({
19
+ // Defaults
20
+ variant = "inset", // Used for the Track
21
+ surface = "flat",
22
+ color,
23
+ elevation = 2,
24
+ intensity,
25
+ angleDeg,
26
+ shape = "pill",
27
+ border = true, // Slider tracks usually look best with a border
28
+ ridge = false,
29
+
30
+ // Logic
31
+ min = 0,
32
+ max = 100,
33
+ step = 1,
34
+ value,
35
+ onChange,
36
+ className,
37
+ style,
38
+ disabled,
39
+ thumbSize = "24px",
40
+ }) => {
41
+ const trackRef = useRef<HTMLDivElement>(null);
42
+ const [isDragging, setIsDragging] = useState(false);
43
+
44
+ // Parse size to number for calculations
45
+ const sizeNum = parseInt(thumbSize) || 24;
46
+ const padding = 4; // Gap between thumb and track wall
47
+
48
+ // 1. STYLE: The Track (The Enclosure)
49
+ const trackStyle = getNeumorphicStyle({
50
+ variant, // Use the prop (Default "inset")
51
+ surface,
52
+ shape,
53
+ color,
54
+ elevation,
55
+ intensity,
56
+ angleDeg,
57
+ border, // Use the prop (Default true)
58
+ ridge,
59
+ state: "default",
60
+ });
61
+
62
+ // 2. STYLE: The Thumb (The Knob)
63
+ const thumbStyle = getNeumorphicStyle({
64
+ variant: "pop", // Thumbs are always popped
65
+ surface: "convex",
66
+ shape: "circle",
67
+ color,
68
+ elevation: 3,
69
+ intensity,
70
+ angleDeg,
71
+ state: isDragging ? "active" : "default",
72
+ });
73
+
74
+ // 3. LOGIC
75
+ const handleUpdate = (clientX: number) => {
76
+ if (disabled || !trackRef.current) return;
77
+ const rect = trackRef.current.getBoundingClientRect();
78
+
79
+ // Available width for the center of the thumb
80
+ const availableWidth = rect.width - (sizeNum + padding * 2);
81
+ const startX = rect.left + padding + sizeNum / 2;
82
+
83
+ // Calculate position relative to the "safe zone"
84
+ const x = clientX - startX;
85
+
86
+ let percent = x / availableWidth;
87
+
88
+ // Clamp
89
+ const rawValue = min + percent * (max - min);
90
+ const steppedValue = Math.round(rawValue / step) * step;
91
+ const finalValue = Math.max(min, Math.min(max, steppedValue));
92
+
93
+ if (finalValue !== value) {
94
+ onChange(finalValue);
95
+ }
96
+ };
97
+
98
+ const handleMouseDown = (e: React.MouseEvent) => {
99
+ if (disabled) return;
100
+ setIsDragging(true);
101
+ handleUpdate(e.clientX);
102
+ };
103
+
104
+ const handleTouchStart = (e: React.TouchEvent) => {
105
+ if (disabled) return;
106
+ setIsDragging(true);
107
+ handleUpdate(e.touches[0].clientX);
108
+ };
109
+
110
+ useEffect(() => {
111
+ if (!isDragging) return;
112
+
113
+ const onMove = (e: MouseEvent) => { e.preventDefault(); handleUpdate(e.clientX); };
114
+ const onUp = () => setIsDragging(false);
115
+
116
+ const onTouchMove = (e: TouchEvent) => { handleUpdate(e.touches[0].clientX); };
117
+
118
+ window.addEventListener("mousemove", onMove);
119
+ window.addEventListener("mouseup", onUp);
120
+ window.addEventListener("touchmove", onTouchMove, { passive: false });
121
+ window.addEventListener("touchend", onUp);
122
+
123
+ return () => {
124
+ window.removeEventListener("mousemove", onMove);
125
+ window.removeEventListener("mouseup", onUp);
126
+ window.removeEventListener("touchmove", onTouchMove);
127
+ window.removeEventListener("touchend", onUp);
128
+ };
129
+ }, [isDragging, min, max, step, sizeNum]);
130
+
131
+ // Visual Position
132
+ const percentage = Math.max(0, Math.min(100, ((value - min) / (max - min)) * 100));
133
+
134
+ return (
135
+ <div
136
+ className={`${sliderContainer} ${className || ""}`}
137
+ style={{
138
+ ...style,
139
+ opacity: disabled ? 0.6 : 1,
140
+ height: `${sizeNum + padding * 2}px`,
141
+ }}
142
+ onMouseDown={handleMouseDown}
143
+ onTouchStart={handleTouchStart}
144
+ >
145
+ {/* The Track acts as the Capsule Enclosure */}
146
+ <div
147
+ ref={trackRef}
148
+ className={sliderTrack}
149
+ style={{
150
+ ...trackStyle,
151
+ borderRadius: "999px",
152
+ height: "100%",
153
+ width: "100%",
154
+ position: "relative",
155
+ boxSizing: "border-box",
156
+ }}
157
+ >
158
+ {/* The Thumb */}
159
+ <div
160
+ className={sliderThumb}
161
+ style={{
162
+ ...thumbStyle,
163
+ // Dynamic Positioning using CSS Calc to stay within padding
164
+ left: `calc(${padding}px + (${percentage / 100} * (100% - ${sizeNum + padding * 2}px)))`,
165
+ top: "50%",
166
+ transform: "translateY(-50%)",
167
+ width: `${sizeNum}px`,
168
+ height: `${sizeNum}px`,
169
+ color: isDragging ? "#3b82f6" : "inherit"
170
+ }}
171
+ />
172
+ </div>
173
+ </div>
174
+ );
175
+ };
@@ -0,0 +1,71 @@
1
+ import React, { useState } from "react";
2
+ import { baseInput } from "./TextInput.css";
3
+ import { getNeumorphicStyle } from "../../styles/neumorphicEngine";
4
+ import type { NeumorphicProps } from "../../styles/types";
5
+
6
+ // FIX: Use InputHTMLAttributes instead of generic HTMLAttributes
7
+ // This ensures 'placeholder', 'value', 'type', etc. are valid.
8
+ type TextInputProps = NeumorphicProps & React.InputHTMLAttributes<HTMLInputElement>;
9
+
10
+ export const NeuTextInput: React.FC<TextInputProps> = ({
11
+ // Default to 'inset' as that is standard for inputs
12
+ variant = "inset",
13
+ color,
14
+ elevation = 2,
15
+ intensity,
16
+ shape = "rounded",
17
+ angleDeg,
18
+ border = false,
19
+ ridge = false,
20
+
21
+ // Standard Props
22
+ className,
23
+ style,
24
+ disabled,
25
+ onFocus,
26
+ onBlur,
27
+ ...htmlProps
28
+ }) => {
29
+ const [isFocused, setIsFocused] = useState(false);
30
+
31
+ // Map Focus -> Active
32
+ const neuStyles = getNeumorphicStyle({
33
+ variant,
34
+ color,
35
+ elevation,
36
+ intensity,
37
+ shape,
38
+ angleDeg,
39
+ border,
40
+ ridge,
41
+ state: isFocused ? "active" : "default",
42
+ });
43
+
44
+ return (
45
+ <input
46
+ className={`${baseInput} ${className || ""}`}
47
+ disabled={disabled}
48
+
49
+ // Events
50
+ onFocus={(e) => {
51
+ setIsFocused(true);
52
+ onFocus?.(e);
53
+ }}
54
+ onBlur={(e) => {
55
+ setIsFocused(false);
56
+ onBlur?.(e);
57
+ }}
58
+
59
+ // Styling
60
+ style={{
61
+ ...neuStyles,
62
+ ...style,
63
+ // Ensure background adapts if user overrides color
64
+ background: style?.background || neuStyles.background,
65
+ color: style?.color || "inherit",
66
+ }}
67
+
68
+ {...htmlProps}
69
+ />
70
+ );
71
+ };
@@ -0,0 +1,55 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const switchLabel = style({
4
+ display: "inline-flex",
5
+ alignItems: "center",
6
+ gap: "12px",
7
+ cursor: "pointer",
8
+ userSelect: "none",
9
+ position: "relative",
10
+ });
11
+
12
+ export const hiddenInput = style({
13
+ position: "absolute",
14
+ opacity: 0,
15
+ cursor: "pointer",
16
+ height: 0,
17
+ width: 0,
18
+ });
19
+
20
+ export const switchTrack = style({
21
+ position: "relative",
22
+ transition: "all 0.3s ease",
23
+ display: "flex",
24
+ alignItems: "center",
25
+ boxSizing: "border-box",
26
+ });
27
+
28
+ export const switchThumb = style({
29
+ position: "absolute",
30
+ display: "flex",
31
+ alignItems: "center",
32
+ justifyContent: "center",
33
+ transition: "transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1), box-shadow 0.3s ease",
34
+ zIndex: 2,
35
+ });
36
+
37
+ export const iconContainer = style({
38
+ position: "absolute",
39
+ display: "flex",
40
+ alignItems: "center",
41
+ justifyContent: "center",
42
+ width: "100%",
43
+ height: "100%",
44
+ pointerEvents: "none",
45
+ });
46
+
47
+ // Added missing export
48
+ export const svgIcon = style({
49
+ width: "60%",
50
+ height: "60%",
51
+ strokeLinecap: "round",
52
+ strokeLinejoin: "round",
53
+ fill: "none",
54
+ transition: "all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)",
55
+ });