@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.
- package/package.json +28 -19
- package/src/App.tsx +626 -0
- package/src/components/Button/Button.css.ts +28 -0
- package/src/components/Button/Button.tsx +85 -0
- package/src/components/Card/Card.css.ts +11 -0
- package/src/components/Card/Card.tsx +106 -0
- package/src/components/Checkbox/Checkbox.css.ts +49 -0
- package/src/components/Checkbox/Checkbox.tsx +134 -0
- package/src/components/Icon/Icon.css.ts +39 -0
- package/src/components/Icon/Icon.tsx +163 -0
- package/src/components/Icon/IconPaths.tsx +80 -0
- package/src/components/Radio/Radio.css.ts +57 -0
- package/src/components/Radio/Radio.tsx +130 -0
- package/src/components/Ridge/Ridge.css.ts +11 -0
- package/src/components/Ridge/Ridge.tsx +66 -0
- package/src/components/Slider/Slider.css.ts +36 -0
- package/src/components/Slider/Slider.tsx +175 -0
- package/src/components/TextInput/TextInput.tsx +71 -0
- package/src/components/Toggle/Toggle.css.ts +55 -0
- package/src/components/Toggle/Toggle.tsx +167 -0
- package/src/index.ts +44 -0
- package/{react-neu/src → src}/main.tsx +0 -1
- package/src/styles/neumorphicEngine.ts +159 -0
- package/src/styles/theme.css.ts +38 -0
- package/src/styles/types.ts +45 -0
- package/tsconfig.json +26 -0
- package/LICENSE +0 -21
- package/react-neu/package-lock.json +0 -3753
- package/react-neu/package.json +0 -33
- package/react-neu/src/App.tsx +0 -30
- package/react-neu/src/components/Button/Button.css.ts +0 -12
- package/react-neu/src/components/Button/Button.tsx +0 -92
- package/react-neu/src/components/TextInput/TextInput.tsx +0 -72
- package/react-neu/src/index.ts +0 -3
- package/react-neu/src/styles/defaults.css.ts +0 -16
- package/react-neu/src/styles/filterDomProps.ts +0 -28
- package/react-neu/src/styles/global.css.ts +0 -12
- package/react-neu/src/styles/neumorphicEngine.ts +0 -110
- package/react-neu/src/styles/shadowUtils.ts +0 -68
- package/react-neu/src/styles/theme.css.ts +0 -26
- package/react-neu/tsconfig.json +0 -7
- /package/{react-neu/README.md → README.md} +0 -0
- /package/{react-neu/eslint.config.js → eslint.config.js} +0 -0
- /package/{react-neu/index.html → index.html} +0 -0
- /package/{react-neu/public → public}/vite.svg +0 -0
- /package/{react-neu/src → src}/assets/react.svg +0 -0
- /package/{react-neu/src → src}/components/TextInput/TextInput.css.ts +0 -0
- /package/{react-neu/src → src}/components/index.ts +0 -0
- /package/{react-neu/src → src}/styles/colorUtils.ts +0 -0
- /package/{react-neu/src → src}/utils/colorUtils.ts +0 -0
- /package/{react-neu/src → src}/utils/neuEngine.ts +0 -0
- /package/{react-neu/tsconfig.app.json → tsconfig.app.json} +0 -0
- /package/{react-neu/tsconfig.node.json → tsconfig.node.json} +0 -0
- /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
|
+
});
|