@mohammadsalman/storybook-custom-ui 1.0.0

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/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # Storybook Component Library
2
+
3
+ A minimal React TypeScript component library with Storybook, featuring a flexible theming system.
4
+
5
+ ## Features
6
+
7
+ - ✅ Button component with multiple variants, colors, and sizes
8
+ - ✅ Custom `useFormInput` hook for form input management
9
+ - ✅ TypeScript support with full type safety
10
+ - ✅ Theming system that can be adapted to any project
11
+ - ✅ Storybook with interactive controls
12
+ - ✅ Zero external UI library dependencies
13
+
14
+ ## Getting Started
15
+
16
+ ### Installation
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ ### Running Storybook
23
+
24
+ ```bash
25
+ npm run storybook
26
+ ```
27
+
28
+ This will start Storybook on `http://localhost:6006`
29
+
30
+ ### Building Storybook
31
+
32
+ ```bash
33
+ npm run build-storybook
34
+ ```
35
+
36
+ ## Theming System
37
+
38
+ The theming system is designed to be easily adaptable to any project. All components use the theme through the `useTheme` hook.
39
+
40
+ ### Default Theme
41
+
42
+ The default theme is located in `src/theme/defaultTheme.ts`. It includes:
43
+
44
+ - **Palette**: Colors for primary, secondary, success, error, warning, info, and grey scales
45
+ - **Typography**: Font families, sizes, weights, and line heights for all text variants
46
+ - **Spacing**: Consistent spacing unit (default: 8px)
47
+ - **Shadows**: Elevation shadows (0-24)
48
+ - **Shape**: Border radius values
49
+ - **Z-Index**: Layering system
50
+
51
+ ### Using the Theme
52
+
53
+ Wrap your application with the `ThemeProvider`:
54
+
55
+ ```tsx
56
+ import { ThemeProvider, defaultTheme } from './theme';
57
+
58
+ function App() {
59
+ return (
60
+ <ThemeProvider theme={defaultTheme}>
61
+ <YourComponents />
62
+ </ThemeProvider>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## Available Components
68
+
69
+ ### Button
70
+
71
+ A versatile button component with multiple variants, colors, and sizes.
72
+
73
+ ```tsx
74
+ import { Button } from './components';
75
+
76
+ <Button variant="contained" color="primary" size="medium">
77
+ Click Me
78
+ </Button>
79
+ ```
80
+
81
+ **Props:**
82
+ - `variant`: 'contained' | 'outlined' | 'text'
83
+ - `color`: 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info'
84
+ - `size`: 'small' | 'medium' | 'large'
85
+ - `fullWidth`: boolean
86
+ - `disabled`: boolean
87
+
88
+ ## Available Hooks
89
+
90
+ ### useFormInput
91
+
92
+ A custom hook for managing form input state, validation, and error handling.
93
+
94
+ ```tsx
95
+ import { useFormInput } from './hooks';
96
+
97
+ const MyForm = () => {
98
+ const emailInput = useFormInput({
99
+ initialValue: '',
100
+ validate: (value) => {
101
+ if (!value) return 'Email is required';
102
+ if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format';
103
+ return null;
104
+ }
105
+ });
106
+
107
+ return (
108
+ <div>
109
+ <input
110
+ value={emailInput.value}
111
+ onChange={emailInput.onChange}
112
+ onBlur={emailInput.onBlur}
113
+ />
114
+ {emailInput.error && <span>{emailInput.error}</span>}
115
+ <button onClick={emailInput.reset}>Reset</button>
116
+ </div>
117
+ );
118
+ };
119
+ ```
120
+
121
+ **Options:**
122
+ - `initialValue`: string (default: '')
123
+ - `validate`: (value: string) => string | null
124
+ - `onChange`: (value: string) => void (optional callback)
125
+
126
+ **Returns:**
127
+ - `value`: string
128
+ - `error`: string | null
129
+ - `onChange`: (e: ChangeEvent) => void
130
+ - `onBlur`: () => void
131
+ - `setValue`: (value: string) => void
132
+ - `setError`: (error: string | null) => void
133
+ - `reset`: () => void
134
+ - `isValid`: boolean
135
+
136
+ ## Project Structure
137
+
138
+ ```
139
+ src/
140
+ ├── components/ # UI components
141
+ │ └── Button/
142
+ │ ├── Button.tsx
143
+ │ ├── Button.stories.tsx
144
+ │ └── index.ts
145
+ ├── hooks/ # Custom hooks
146
+ │ ├── useFormInput.ts
147
+ │ ├── useFormInput.stories.tsx
148
+ │ └── index.ts
149
+ ├── theme/ # Theming system
150
+ │ ├── types.ts
151
+ │ ├── defaultTheme.ts
152
+ │ ├── ThemeProvider.tsx
153
+ │ └── index.ts
154
+ └── utils/ # Utility functions
155
+ └── styles.ts
156
+ .storybook/ # Storybook configuration
157
+ ├── main.ts
158
+ ├── preview.tsx
159
+ └── global.css
160
+ ```
161
+
162
+ ## Adding New Components
163
+
164
+ To add a new component:
165
+
166
+ 1. Create a new directory in `src/components/`
167
+ 2. Add your component file (e.g., `MyComponent.tsx`)
168
+ 3. Create an `index.ts` file that exports your component
169
+ 4. Create a `MyComponent.stories.tsx` file for Storybook
170
+ 5. Export from `src/components/index.ts`
171
+
172
+ ## Adding New Hooks
173
+
174
+ To add a new hook:
175
+
176
+ 1. Create a new file in `src/hooks/` (e.g., `useMyHook.ts`)
177
+ 2. Export from `src/hooks/index.ts`
178
+ 3. Optionally create a `useMyHook.stories.tsx` file for Storybook
179
+
180
+ ## Adapting to Your Project
181
+
182
+ To use these components and hooks in your project:
183
+
184
+ 1. Copy the `src/components`, `src/hooks`, `src/theme`, and `src/utils` directories to your project
185
+ 2. Create your custom theme based on your design system
186
+ 3. Wrap your app with `ThemeProvider` using your custom theme
187
+ 4. Import and use components and hooks as needed
188
+
189
+ The theme system is designed to be flexible - you can override any part of it to match your brand colors, typography, spacing, and more.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@mohammadsalman/storybook-custom-ui",
3
+ "version": "1.0.0",
4
+ "description": "A minimal React TypeScript component library with Button component and useFormInput hook",
5
+ "main":"src/index.ts",
6
+ "module": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "keywords": [
12
+ "react",
13
+ "typescript",
14
+ "components",
15
+ "hooks",
16
+ "storybook",
17
+ "button",
18
+ "form-input"
19
+ ],
20
+ "author": "Your Name <your.email@example.com>",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/MohSalman/creative-ui-react-ui-library.git"
25
+ },
26
+ "peerDependencies": {
27
+ "react": "^18.2.0",
28
+ "react-dom": "^18.2.0"
29
+ },
30
+ "scripts": {
31
+ "storybook": "storybook dev -p 6006",
32
+ "build-storybook": "storybook build",
33
+ "prepublishOnly": "npm run build-storybook"
34
+ },
35
+ "dependencies": {},
36
+ "devDependencies": {
37
+ "react": "^18.2.0",
38
+ "react-dom": "^18.2.0",
39
+ "@babel/preset-react": "^7.28.5",
40
+ "@babel/preset-typescript": "^7.28.5",
41
+ "@storybook/addon-essentials": "^7.6.5",
42
+ "@storybook/addon-interactions": "^7.6.5",
43
+ "@storybook/addon-links": "^7.6.5",
44
+ "@storybook/blocks": "^7.6.5",
45
+ "@storybook/react": "^7.6.5",
46
+ "@storybook/react-webpack5": "^7.6.5",
47
+ "@storybook/test": "^7.6.5",
48
+ "@types/react": "^18.2.43",
49
+ "@types/react-dom": "^18.2.17",
50
+ "storybook": "^7.6.5",
51
+ "typescript": "^5.3.3"
52
+ }
53
+ }
@@ -0,0 +1,109 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { Button } from './Button';
3
+
4
+ const meta: Meta<typeof Button> = {
5
+ title: 'Components/Button',
6
+ component: Button,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ variant: {
10
+ control: { type: 'select' },
11
+ options: ['contained', 'outlined', 'text'],
12
+ },
13
+ color: {
14
+ control: { type: 'select' },
15
+ options: ['primary', 'secondary', 'success', 'error', 'warning', 'info'],
16
+ },
17
+ size: {
18
+ control: { type: 'select' },
19
+ options: ['small', 'medium', 'large'],
20
+ },
21
+ fullWidth: {
22
+ control: { type: 'boolean' },
23
+ },
24
+ disabled: {
25
+ control: { type: 'boolean' },
26
+ },
27
+ },
28
+ };
29
+
30
+ export default meta;
31
+ type Story = StoryObj<typeof Button>;
32
+
33
+ export const Contained: Story = {
34
+ args: {
35
+ children: 'Button',
36
+ variant: 'contained',
37
+ color: 'primary',
38
+ size: 'medium',
39
+ },
40
+ };
41
+
42
+ export const Outlined: Story = {
43
+ args: {
44
+ children: 'Button',
45
+ variant: 'outlined',
46
+ color: 'primary',
47
+ size: 'medium',
48
+ },
49
+ };
50
+
51
+ export const Text: Story = {
52
+ args: {
53
+ children: 'Button',
54
+ variant: 'text',
55
+ color: 'primary',
56
+ size: 'medium',
57
+ },
58
+ };
59
+
60
+ export const Colors: Story = {
61
+ render: () => (
62
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
63
+ <Button color="primary">Primary</Button>
64
+ <Button color="secondary">Secondary</Button>
65
+ <Button color="success">Success</Button>
66
+ <Button color="error">Error</Button>
67
+ <Button color="warning">Warning</Button>
68
+ <Button color="info">Info</Button>
69
+ </div>
70
+ ),
71
+ };
72
+
73
+ export const Sizes: Story = {
74
+ render: () => (
75
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
76
+ <Button size="small">Small</Button>
77
+ <Button size="medium">Medium</Button>
78
+ <Button size="large">Large</Button>
79
+ </div>
80
+ ),
81
+ };
82
+
83
+ export const Variants: Story = {
84
+ render: () => (
85
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
86
+ <Button variant="contained">Contained</Button>
87
+ <Button variant="outlined">Outlined</Button>
88
+ <Button variant="text">Text</Button>
89
+ </div>
90
+ ),
91
+ };
92
+
93
+ export const FullWidth: Story = {
94
+ args: {
95
+ children: 'Full Width Button',
96
+ fullWidth: true,
97
+ variant: 'contained',
98
+ },
99
+ };
100
+
101
+ export const Disabled: Story = {
102
+ render: () => (
103
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
104
+ <Button variant="contained" disabled>Disabled</Button>
105
+ <Button variant="outlined" disabled>Disabled</Button>
106
+ <Button variant="text" disabled>Disabled</Button>
107
+ </div>
108
+ ),
109
+ };
@@ -0,0 +1,144 @@
1
+ import React from 'react';
2
+ import { useTheme } from '../../theme';
3
+ import { spacing } from '../../utils/styles';
4
+
5
+ export type ButtonVariant = 'contained' | 'outlined' | 'text';
6
+ export type ButtonColor = 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info';
7
+ export type ButtonSize = 'small' | 'medium' | 'large';
8
+
9
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
10
+ variant?: ButtonVariant;
11
+ color?: ButtonColor;
12
+ size?: ButtonSize;
13
+ fullWidth?: boolean;
14
+ disabled?: boolean;
15
+ children: React.ReactNode;
16
+ }
17
+
18
+ export const Button: React.FC<ButtonProps> = ({
19
+ variant = 'contained',
20
+ color = 'primary',
21
+ size = 'medium',
22
+ fullWidth = false,
23
+ disabled = false,
24
+ children,
25
+ style,
26
+ ...props
27
+ }) => {
28
+ const theme = useTheme();
29
+
30
+ const getVariantStyles = (): React.CSSProperties => {
31
+ const colorValue = theme.palette[color][500];
32
+ const textColor = variant === 'contained' ? '#ffffff' : colorValue;
33
+
34
+ switch (variant) {
35
+ case 'contained':
36
+ return {
37
+ backgroundColor: colorValue,
38
+ color: textColor,
39
+ border: 'none',
40
+ boxShadow: theme.shadows[2],
41
+ };
42
+ case 'outlined':
43
+ return {
44
+ backgroundColor: 'transparent',
45
+ color: colorValue,
46
+ border: `1px solid ${colorValue}`,
47
+ boxShadow: 'none',
48
+ };
49
+ case 'text':
50
+ return {
51
+ backgroundColor: 'transparent',
52
+ color: colorValue,
53
+ border: 'none',
54
+ boxShadow: 'none',
55
+ };
56
+ default:
57
+ return {};
58
+ }
59
+ };
60
+
61
+ const getSizeStyles = (): React.CSSProperties => {
62
+ switch (size) {
63
+ case 'small':
64
+ return {
65
+ padding: spacing(theme, 0.5, 1.5),
66
+ fontSize: theme.typography.button.fontSize,
67
+ minWidth: '64px',
68
+ };
69
+ case 'medium':
70
+ return {
71
+ padding: spacing(theme, 1, 2),
72
+ fontSize: theme.typography.button.fontSize,
73
+ minWidth: '64px',
74
+ };
75
+ case 'large':
76
+ return {
77
+ padding: spacing(theme, 1.5, 3),
78
+ fontSize: '1rem',
79
+ minWidth: '64px',
80
+ };
81
+ default:
82
+ return {};
83
+ }
84
+ };
85
+
86
+ const baseStyles: React.CSSProperties = {
87
+ fontFamily: theme.typography.button.fontFamily,
88
+ fontWeight: theme.typography.button.fontWeight,
89
+ lineHeight: theme.typography.button.lineHeight,
90
+ letterSpacing: theme.typography.button.letterSpacing,
91
+ textTransform: 'uppercase',
92
+ borderRadius: `${theme.shape.borderRadius}px`,
93
+ cursor: disabled ? 'not-allowed' : 'pointer',
94
+ outline: 'none',
95
+ transition: 'background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
96
+ width: fullWidth ? '100%' : 'auto',
97
+ opacity: disabled ? 0.6 : 1,
98
+ ...getVariantStyles(),
99
+ ...getSizeStyles(),
100
+ ...style,
101
+ };
102
+
103
+ const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
104
+ if (!disabled && props.onMouseEnter) {
105
+ props.onMouseEnter(e);
106
+ }
107
+ };
108
+
109
+ const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
110
+ if (!disabled && props.onMouseLeave) {
111
+ props.onMouseLeave(e);
112
+ }
113
+ };
114
+
115
+ return (
116
+ <button
117
+ {...props}
118
+ disabled={disabled}
119
+ style={baseStyles}
120
+ onMouseEnter={handleMouseEnter}
121
+ onMouseLeave={handleMouseLeave}
122
+ onMouseOver={(e) => {
123
+ if (!disabled) {
124
+ e.currentTarget.style.backgroundColor =
125
+ variant === 'contained'
126
+ ? theme.palette[color][700]
127
+ : variant === 'outlined'
128
+ ? theme.palette.action.hover
129
+ : theme.palette.action.hover;
130
+ }
131
+ }}
132
+ onMouseOut={(e) => {
133
+ if (!disabled) {
134
+ e.currentTarget.style.backgroundColor =
135
+ variant === 'contained'
136
+ ? theme.palette[color][500]
137
+ : 'transparent';
138
+ }
139
+ }}
140
+ >
141
+ {children}
142
+ </button>
143
+ );
144
+ };
@@ -0,0 +1 @@
1
+ export * from './Button';
@@ -0,0 +1 @@
1
+ export * from './Button';
@@ -0,0 +1 @@
1
+ export * from './useFormInput';
@@ -0,0 +1,126 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { useFormInput } from './useFormInput';
3
+ import { Button } from '../components/Button';
4
+
5
+ // Demo component to showcase the hook
6
+ const FormInputDemo = ({
7
+ initialValue,
8
+ validate,
9
+ label
10
+ }: {
11
+ initialValue?: string;
12
+ validate?: (value: string) => string | null;
13
+ label?: string;
14
+ }) => {
15
+ const input = useFormInput({
16
+ initialValue,
17
+ validate,
18
+ });
19
+
20
+ return (
21
+ <div style={{ maxWidth: '400px', padding: '20px' }}>
22
+ <label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
23
+ {label || 'Input'}
24
+ </label>
25
+ <input
26
+ type="text"
27
+ value={input.value}
28
+ onChange={input.onChange}
29
+ onBlur={input.onBlur}
30
+ style={{
31
+ width: '100%',
32
+ padding: '8px 12px',
33
+ border: `1px solid ${input.error ? '#d32f2f' : '#ccc'}`,
34
+ borderRadius: '4px',
35
+ fontSize: '14px',
36
+ outline: 'none',
37
+ }}
38
+ placeholder="Type something..."
39
+ />
40
+ {input.error && (
41
+ <div style={{ color: '#d32f2f', fontSize: '12px', marginTop: '4px' }}>
42
+ {input.error}
43
+ </div>
44
+ )}
45
+ <div style={{ marginTop: '12px', display: 'flex', gap: '8px' }}>
46
+ <Button size="small" onClick={input.reset}>
47
+ Reset
48
+ </Button>
49
+ <div style={{
50
+ padding: '8px',
51
+ fontSize: '12px',
52
+ color: input.isValid ? '#2e7d32' : '#666'
53
+ }}>
54
+ Valid: {input.isValid ? 'Yes' : 'No'}
55
+ </div>
56
+ </div>
57
+ </div>
58
+ );
59
+ };
60
+
61
+ const meta: Meta<typeof FormInputDemo> = {
62
+ title: 'Hooks/useFormInput',
63
+ component: FormInputDemo,
64
+ tags: ['autodocs'],
65
+ parameters: {
66
+ docs: {
67
+ description: {
68
+ component: 'A custom hook for managing form input state, validation, and error handling.',
69
+ },
70
+ },
71
+ },
72
+ };
73
+
74
+ export default meta;
75
+ type Story = StoryObj<typeof FormInputDemo>;
76
+
77
+ export const Basic: Story = {
78
+ args: {
79
+ label: 'Basic Input',
80
+ },
81
+ };
82
+
83
+ export const WithInitialValue: Story = {
84
+ args: {
85
+ label: 'Input with Initial Value',
86
+ initialValue: 'Hello World',
87
+ },
88
+ };
89
+
90
+ export const WithValidation: Story = {
91
+ args: {
92
+ label: 'Email Input (with validation)',
93
+ initialValue: '',
94
+ validate: (value) => {
95
+ if (!value) return 'Email is required';
96
+ if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format';
97
+ return null;
98
+ },
99
+ },
100
+ };
101
+
102
+ export const PasswordValidation: Story = {
103
+ args: {
104
+ label: 'Password (min 8 characters)',
105
+ initialValue: '',
106
+ validate: (value) => {
107
+ if (!value) return 'Password is required';
108
+ if (value.length < 8) return 'Password must be at least 8 characters';
109
+ return null;
110
+ },
111
+ },
112
+ };
113
+
114
+ export const NumberValidation: Story = {
115
+ args: {
116
+ label: 'Age (must be 18+)',
117
+ initialValue: '',
118
+ validate: (value) => {
119
+ if (!value) return 'Age is required';
120
+ const num = parseInt(value, 10);
121
+ if (isNaN(num)) return 'Must be a number';
122
+ if (num < 18) return 'Must be 18 or older';
123
+ return null;
124
+ },
125
+ },
126
+ };
@@ -0,0 +1,114 @@
1
+ import { useState, useCallback, ChangeEvent } from 'react';
2
+
3
+ export interface UseFormInputOptions {
4
+ initialValue?: string;
5
+ validate?: (value: string) => string | null;
6
+ onChange?: (value: string) => void;
7
+ }
8
+
9
+ export interface UseFormInputReturn {
10
+ value: string;
11
+ error: string | null;
12
+ onChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
13
+ onBlur: () => void;
14
+ setValue: (value: string) => void;
15
+ setError: (error: string | null) => void;
16
+ reset: () => void;
17
+ isValid: boolean;
18
+ }
19
+
20
+ /**
21
+ * Custom hook for managing form input state, validation, and error handling
22
+ *
23
+ * @param options - Configuration options for the hook
24
+ * @param options.initialValue - Initial value for the input (default: '')
25
+ * @param options.validate - Validation function that returns error message or null
26
+ * @param options.onChange - Optional callback when value changes
27
+ * @returns Object containing input state and handlers
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * const emailInput = useFormInput({
32
+ * initialValue: '',
33
+ * validate: (value) => {
34
+ * if (!value) return 'Email is required';
35
+ * if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format';
36
+ * return null;
37
+ * }
38
+ * });
39
+ *
40
+ * <input
41
+ * value={emailInput.value}
42
+ * onChange={emailInput.onChange}
43
+ * onBlur={emailInput.onBlur}
44
+ * />
45
+ * {emailInput.error && <span>{emailInput.error}</span>}
46
+ * ```
47
+ */
48
+ export const useFormInput = ({
49
+ initialValue = '',
50
+ validate,
51
+ onChange: onChangeCallback,
52
+ }: UseFormInputOptions = {}): UseFormInputReturn => {
53
+ const [value, setValueState] = useState<string>(initialValue);
54
+ const [error, setErrorState] = useState<string | null>(null);
55
+ const [touched, setTouched] = useState<boolean>(false);
56
+
57
+ const handleChange = useCallback(
58
+ (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
59
+ const newValue = e.target.value;
60
+ setValueState(newValue);
61
+
62
+ // Clear error when user starts typing
63
+ if (error && touched) {
64
+ const validationError = validate ? validate(newValue) : null;
65
+ setErrorState(validationError);
66
+ }
67
+
68
+ // Call optional onChange callback
69
+ if (onChangeCallback) {
70
+ onChangeCallback(newValue);
71
+ }
72
+ },
73
+ [error, touched, validate, onChangeCallback]
74
+ );
75
+
76
+ const handleBlur = useCallback(() => {
77
+ setTouched(true);
78
+ if (validate) {
79
+ const validationError = validate(value);
80
+ setErrorState(validationError);
81
+ }
82
+ }, [value, validate]);
83
+
84
+ const setValue = useCallback((newValue: string) => {
85
+ setValueState(newValue);
86
+ if (touched && validate) {
87
+ const validationError = validate(newValue);
88
+ setErrorState(validationError);
89
+ }
90
+ }, [touched, validate]);
91
+
92
+ const setError = useCallback((errorMessage: string | null) => {
93
+ setErrorState(errorMessage);
94
+ }, []);
95
+
96
+ const reset = useCallback(() => {
97
+ setValueState(initialValue);
98
+ setErrorState(null);
99
+ setTouched(false);
100
+ }, [initialValue]);
101
+
102
+ const isValid = error === null;
103
+
104
+ return {
105
+ value,
106
+ error,
107
+ onChange: handleChange,
108
+ onBlur: handleBlur,
109
+ setValue,
110
+ setError,
111
+ reset,
112
+ isValid,
113
+ };
114
+ };
package/src/index.ts ADDED
File without changes
@@ -0,0 +1,34 @@
1
+ import React, { createContext, useContext, ReactNode } from 'react';
2
+ import { Theme } from './types';
3
+
4
+ const ThemeContext = createContext<Theme | undefined>(undefined);
5
+
6
+ export const useTheme = (): Theme => {
7
+ const theme = useContext(ThemeContext);
8
+ if (!theme) {
9
+ throw new Error('useTheme must be used within a ThemeProvider');
10
+ }
11
+ return theme;
12
+ };
13
+
14
+ interface ThemeProviderProps {
15
+ theme: Theme;
16
+ children: ReactNode;
17
+ }
18
+
19
+ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ theme, children }) => {
20
+ return (
21
+ <ThemeContext.Provider value={theme}>
22
+ <div
23
+ style={{
24
+ color: theme.palette.text.primary,
25
+ backgroundColor: theme.palette.background.default,
26
+ fontFamily: theme.typography.fontFamily,
27
+ fontSize: theme.typography.fontSize,
28
+ }}
29
+ >
30
+ {children}
31
+ </div>
32
+ </ThemeContext.Provider>
33
+ );
34
+ };
@@ -0,0 +1,182 @@
1
+ import { Theme, PaletteColor } from './types';
2
+
3
+ const createPaletteColor = (main: string, light: string, dark: string): PaletteColor => ({
4
+ 50: light,
5
+ 100: light,
6
+ 200: light,
7
+ 300: main,
8
+ 400: main,
9
+ 500: main,
10
+ 600: main,
11
+ 700: dark,
12
+ 800: dark,
13
+ 900: dark,
14
+ });
15
+
16
+ export const defaultTheme: Theme = {
17
+ mode: 'light',
18
+ palette: {
19
+ primary: createPaletteColor('#1976d2', '#42a5f5', '#1565c0'),
20
+ secondary: createPaletteColor('#9c27b0', '#ba68c8', '#7b1fa2'),
21
+ success: createPaletteColor('#2e7d32', '#66bb6a', '#1b5e20'),
22
+ error: createPaletteColor('#d32f2f', '#ef5350', '#c62828'),
23
+ warning: createPaletteColor('#ed6c02', '#ff9800', '#e65100'),
24
+ info: createPaletteColor('#0288d1', '#42a5f5', '#01579b'),
25
+ grey: {
26
+ 50: '#fafafa',
27
+ 100: '#f5f5f5',
28
+ 200: '#eeeeee',
29
+ 300: '#e0e0e0',
30
+ 400: '#bdbdbd',
31
+ 500: '#9e9e9e',
32
+ 600: '#757575',
33
+ 700: '#616161',
34
+ 800: '#424242',
35
+ 900: '#212121',
36
+ },
37
+ text: {
38
+ primary: 'rgba(0, 0, 0, 0.87)',
39
+ secondary: 'rgba(0, 0, 0, 0.6)',
40
+ disabled: 'rgba(0, 0, 0, 0.38)',
41
+ },
42
+ background: {
43
+ default: '#ffffff',
44
+ paper: '#ffffff',
45
+ },
46
+ divider: 'rgba(0, 0, 0, 0.12)',
47
+ action: {
48
+ active: 'rgba(0, 0, 0, 0.54)',
49
+ hover: 'rgba(0, 0, 0, 0.04)',
50
+ selected: 'rgba(0, 0, 0, 0.08)',
51
+ disabled: 'rgba(0, 0, 0, 0.26)',
52
+ disabledBackground: 'rgba(0, 0, 0, 0.12)',
53
+ },
54
+ },
55
+ typography: {
56
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
57
+ fontSize: 14,
58
+ fontWeightLight: 300,
59
+ fontWeightRegular: 400,
60
+ fontWeightMedium: 500,
61
+ fontWeightBold: 700,
62
+ h1: {
63
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
64
+ fontWeight: 300,
65
+ fontSize: '6rem',
66
+ lineHeight: '1.167',
67
+ letterSpacing: '-0.01562em',
68
+ },
69
+ h2: {
70
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
71
+ fontWeight: 300,
72
+ fontSize: '3.75rem',
73
+ lineHeight: '1.2',
74
+ letterSpacing: '-0.00833em',
75
+ },
76
+ h3: {
77
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
78
+ fontWeight: 400,
79
+ fontSize: '3rem',
80
+ lineHeight: '1.167',
81
+ letterSpacing: '0em',
82
+ },
83
+ h4: {
84
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
85
+ fontWeight: 400,
86
+ fontSize: '2.125rem',
87
+ lineHeight: '1.235',
88
+ letterSpacing: '0.00735em',
89
+ },
90
+ h5: {
91
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
92
+ fontWeight: 400,
93
+ fontSize: '1.5rem',
94
+ lineHeight: '1.334',
95
+ letterSpacing: '0em',
96
+ },
97
+ h6: {
98
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
99
+ fontWeight: 500,
100
+ fontSize: '1.25rem',
101
+ lineHeight: '1.6',
102
+ letterSpacing: '0.0075em',
103
+ },
104
+ subtitle1: {
105
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
106
+ fontWeight: 400,
107
+ fontSize: '1rem',
108
+ lineHeight: '1.75',
109
+ letterSpacing: '0.00938em',
110
+ },
111
+ subtitle2: {
112
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
113
+ fontWeight: 500,
114
+ fontSize: '0.875rem',
115
+ lineHeight: '1.57',
116
+ letterSpacing: '0.00714em',
117
+ },
118
+ body1: {
119
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
120
+ fontWeight: 400,
121
+ fontSize: '1rem',
122
+ lineHeight: '1.5',
123
+ letterSpacing: '0.00938em',
124
+ },
125
+ body2: {
126
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
127
+ fontWeight: 400,
128
+ fontSize: '0.875rem',
129
+ lineHeight: '1.43',
130
+ letterSpacing: '0.01071em',
131
+ },
132
+ button: {
133
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
134
+ fontWeight: 500,
135
+ fontSize: '0.875rem',
136
+ lineHeight: '1.75',
137
+ letterSpacing: '0.02857em',
138
+ },
139
+ caption: {
140
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
141
+ fontWeight: 400,
142
+ fontSize: '0.75rem',
143
+ lineHeight: '1.66',
144
+ letterSpacing: '0.03333em',
145
+ },
146
+ overline: {
147
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
148
+ fontWeight: 400,
149
+ fontSize: '0.75rem',
150
+ lineHeight: '2.66',
151
+ letterSpacing: '0.08333em',
152
+ },
153
+ },
154
+ spacing: {
155
+ unit: 8,
156
+ },
157
+ shape: {
158
+ borderRadius: 4,
159
+ },
160
+ shadows: {
161
+ 0: 'none',
162
+ 1: '0px 2px 1px -1px rgba(0,0,0,0.2), 0px 1px 1px 0px rgba(0,0,0,0.14), 0px 1px 3px 0px rgba(0,0,0,0.12)',
163
+ 2: '0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
164
+ 3: '0px 3px 3px -2px rgba(0,0,0,0.2), 0px 3px 4px 0px rgba(0,0,0,0.14), 0px 1px 8px 0px rgba(0,0,0,0.12)',
165
+ 4: '0px 2px 4px -1px rgba(0,0,0,0.2), 0px 4px 5px 0px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)',
166
+ 5: '0px 3px 5px -1px rgba(0,0,0,0.2), 0px 5px 8px 0px rgba(0,0,0,0.14), 0px 1px 14px 0px rgba(0,0,0,0.12)',
167
+ 6: '0px 3px 5px -1px rgba(0,0,0,0.2), 0px 6px 10px 0px rgba(0,0,0,0.14), 0px 1px 18px 0px rgba(0,0,0,0.12)',
168
+ 8: '0px 5px 5px -3px rgba(0,0,0,0.2), 0px 8px 10px 1px rgba(0,0,0,0.14), 0px 3px 14px 2px rgba(0,0,0,0.12)',
169
+ 12: '0px 7px 8px -4px rgba(0,0,0,0.2), 0px 12px 17px 2px rgba(0,0,0,0.14), 0px 5px 22px 4px rgba(0,0,0,0.12)',
170
+ 16: '0px 8px 10px -5px rgba(0,0,0,0.2), 0px 16px 24px 2px rgba(0,0,0,0.14), 0px 6px 30px 5px rgba(0,0,0,0.12)',
171
+ 24: '0px 11px 15px -7px rgba(0,0,0,0.2), 0px 24px 38px 3px rgba(0,0,0,0.14), 0px 9px 46px 8px rgba(0,0,0,0.12)',
172
+ },
173
+ zIndex: {
174
+ mobileStepper: 1000,
175
+ speedDial: 1050,
176
+ appBar: 1100,
177
+ drawer: 1200,
178
+ modal: 1300,
179
+ snackbar: 1400,
180
+ tooltip: 1500,
181
+ },
182
+ };
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './ThemeProvider';
3
+ export * from './defaultTheme';
@@ -0,0 +1,106 @@
1
+ export type ThemeMode = 'light' | 'dark';
2
+
3
+ export interface PaletteColor {
4
+ 50: string;
5
+ 100: string;
6
+ 200: string;
7
+ 300: string;
8
+ 400: string;
9
+ 500: string;
10
+ 600: string;
11
+ 700: string;
12
+ 800: string;
13
+ 900: string;
14
+ }
15
+
16
+ export interface Palette {
17
+ primary: PaletteColor;
18
+ secondary: PaletteColor;
19
+ success: PaletteColor;
20
+ error: PaletteColor;
21
+ warning: PaletteColor;
22
+ info: PaletteColor;
23
+ grey: PaletteColor;
24
+ text: {
25
+ primary: string;
26
+ secondary: string;
27
+ disabled: string;
28
+ };
29
+ background: {
30
+ default: string;
31
+ paper: string;
32
+ };
33
+ divider: string;
34
+ action: {
35
+ active: string;
36
+ hover: string;
37
+ selected: string;
38
+ disabled: string;
39
+ disabledBackground: string;
40
+ };
41
+ }
42
+
43
+ export interface Typography {
44
+ fontFamily: string;
45
+ fontSize: number;
46
+ fontWeightLight: number;
47
+ fontWeightRegular: number;
48
+ fontWeightMedium: number;
49
+ fontWeightBold: number;
50
+ h1: TypographyVariant;
51
+ h2: TypographyVariant;
52
+ h3: TypographyVariant;
53
+ h4: TypographyVariant;
54
+ h5: TypographyVariant;
55
+ h6: TypographyVariant;
56
+ subtitle1: TypographyVariant;
57
+ subtitle2: TypographyVariant;
58
+ body1: TypographyVariant;
59
+ body2: TypographyVariant;
60
+ button: TypographyVariant;
61
+ caption: TypographyVariant;
62
+ overline: TypographyVariant;
63
+ }
64
+
65
+ export interface TypographyVariant {
66
+ fontFamily: string;
67
+ fontWeight: number;
68
+ fontSize: string;
69
+ lineHeight: string;
70
+ letterSpacing: string;
71
+ }
72
+
73
+ export interface Spacing {
74
+ unit: number;
75
+ }
76
+
77
+ export interface Shape {
78
+ borderRadius: number;
79
+ }
80
+
81
+ export interface Shadows {
82
+ [key: number]: string;
83
+ }
84
+
85
+ export interface Theme {
86
+ mode: ThemeMode;
87
+ palette: Palette;
88
+ typography: Typography;
89
+ spacing: Spacing;
90
+ shape: Shape;
91
+ shadows: Shadows;
92
+ zIndex: {
93
+ mobileStepper: number;
94
+ speedDial: number;
95
+ appBar: number;
96
+ drawer: number;
97
+ modal: number;
98
+ snackbar: number;
99
+ tooltip: number;
100
+ };
101
+ }
102
+
103
+ export type ThemeProviderProps = {
104
+ theme: Theme;
105
+ children: React.ReactNode;
106
+ };
@@ -0,0 +1,15 @@
1
+ import { Theme } from '../theme/types';
2
+
3
+ export const spacing = (theme: Theme, ...values: number[]): string => {
4
+ return values.map(value => `${value * theme.spacing.unit}px`).join(' ');
5
+ };
6
+
7
+ export const getColor = (
8
+ theme: Theme,
9
+ color: 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info',
10
+ shade: keyof Theme['palette']['primary'] = 500
11
+ ): string => {
12
+ return theme.palette[color][shade];
13
+ };
14
+
15
+ export const css = (styles: React.CSSProperties): React.CSSProperties => styles;