From 76c3900315a5b34810f9d6e89a27c193e1b8bde2 Mon Sep 17 00:00:00 2001 From: Joseph Young Date: Wed, 13 Mar 2024 23:24:13 -0500 Subject: [PATCH] Add Input component --- src/components/Input/Input.jsx | 70 ++++++++++++++++++ src/components/Input/Input.stories.jsx | 32 +++++++++ src/theme/components/Input/Input.theme.js | 27 +++++++ .../components/Input/InputFloat.theme.js | 72 +++++++++++++++++++ src/theme/index.js | 2 + 5 files changed, 203 insertions(+) create mode 100644 src/components/Input/Input.jsx create mode 100644 src/components/Input/Input.stories.jsx create mode 100644 src/theme/components/Input/Input.theme.js create mode 100644 src/theme/components/Input/InputFloat.theme.js diff --git a/src/components/Input/Input.jsx b/src/components/Input/Input.jsx new file mode 100644 index 0000000..9e1bfc0 --- /dev/null +++ b/src/components/Input/Input.jsx @@ -0,0 +1,70 @@ +import { useTheme } from "../../context/ThemeContext"; +import { twMerge } from "tailwind-merge"; +import lookupOptions from "../../util/lookupOptions"; +import mapObjectToString from "../../util/mapObjectToString"; + +const Input = ({ containerProps, labelProps, label, placeholder, typography, color, radius, variant, className, ...args }) => { + const { input, typography: typographyOptions, themeColor } = useTheme(); + const { defaultOptions, styles } = input; + const { initial, radii, variants } = styles; + + const resolvedColor = color || themeColor || defaultOptions.color; + const resolvedRadius = radius || defaultOptions.radius; + const resolvedVariant = variant || defaultOptions.variant; + const resolvedTypography = typography || themeColor || typographyOptions.defaultOptions.variant; + + const initialContainerClasses = mapObjectToString( + lookupOptions(initial, 'container', 'container'), + lookupOptions(typographyOptions.styles.variants, resolvedTypography, typographyOptions.defaultOptions.variant), + ); + const initialInputClasses = mapObjectToString( + lookupOptions(initial, 'input', 'input'), + lookupOptions(radii, resolvedRadius, defaultOptions.radius), + ); + const initialLabelClasses = mapObjectToString( + lookupOptions(initial, 'label', 'label'), + ); + const inputVariant = mapObjectToString( + lookupOptions(variants, resolvedVariant, defaultOptions.variant)['input']['base'], + lookupOptions(variants, resolvedVariant, defaultOptions.variant)['input']['style'][resolvedColor], + ); + const labelVariant = mapObjectToString( + lookupOptions(variants, resolvedVariant, defaultOptions.variant)['label']['base'], + lookupOptions(variants, resolvedVariant, defaultOptions.variant)['label']['style'][resolvedColor], + ); + + const containerClasses = twMerge( + ...initialContainerClasses, + containerProps?.className + ); + const inputClasses = twMerge( + ...initialInputClasses, + ...inputVariant, + className + ); + const labelClasses = twMerge( + ...initialLabelClasses, + ...labelVariant, + labelProps?.className + ); + + return ( +
+ + +
+ ) +}; + +export default Input; \ No newline at end of file diff --git a/src/components/Input/Input.stories.jsx b/src/components/Input/Input.stories.jsx new file mode 100644 index 0000000..28326fc --- /dev/null +++ b/src/components/Input/Input.stories.jsx @@ -0,0 +1,32 @@ +import Input from "./Input.jsx" + +export default { + args: { + label: 'Username', + }, + argTypes: { + color: { + defaultValue: 'default', + options: ['default', 'primary', 'secondary', 'tertiary', 'neutral'], + control: { type: 'inline-radio' } + }, + radius: { + defaultValue: 'round', + options: ['round', 'sharp'], + control: { type: 'inline-radio' } + }, + variant: { + defaultValue: 'floating', + options: ['floating', 'stacked'], + control: { type: 'inline-radio' } + } + } +}; + +export const InputStory = ({ ...args }) => ( +
+ +
+); + +InputStory.storyName = "Input"; \ No newline at end of file diff --git a/src/theme/components/Input/Input.theme.js b/src/theme/components/Input/Input.theme.js new file mode 100644 index 0000000..411af68 --- /dev/null +++ b/src/theme/components/Input/Input.theme.js @@ -0,0 +1,27 @@ +// theme options +import FloatOptions from "./InputFloat.theme"; + +export const input = { + defaultOptions: { + color: 'default', + radius: 'round', + variant: 'floating', + }, + styles: { + initial: { + container: { + position: 'relative' + } + }, + radii: { + round: 'rounded-md', + sharp: 'rounded-none' + }, + variants: { + floating: FloatOptions, + stacked: '', + } + } +}; + +export default input; \ No newline at end of file diff --git a/src/theme/components/Input/InputFloat.theme.js b/src/theme/components/Input/InputFloat.theme.js new file mode 100644 index 0000000..19496c0 --- /dev/null +++ b/src/theme/components/Input/InputFloat.theme.js @@ -0,0 +1,72 @@ +const FloatOptions = { + input: { + base: { + initial: 'peer block w-full bg-transparent border-2', + animation: 'motion-safe:transition-all', + reset: 'focus:ring-0 focus-visible:outline-0', + light: 'border-gray-300 bg-white', + dark: 'dark:bg-zinc-900 dark:border-zinc-800', + }, + style: { + default: { + light: 'focus-within:border-inherit', + dark: 'dark:focus-within:border-inherit' + }, + primary: { + light: 'focus-within:border-primary-500', + dark: 'dark:focus-within:border-primary-500' + }, + secondary: { + light: 'focus-within:border-secondary-500', + dark: 'dark:focus-within:border-secondary-500' + }, + tertiary: { + light: 'focus-within:border-tertiary-500', + dark: 'dark:focus-within:border-tertiary-500' + } + } + }, + label: { + base: { + initial: 'px-1 scale-75 select-none pointer-events-none', + position: 'absolute z-[1] left-3 top-0 -translate-y-1/2 origin-[0]', + placeholder: 'peer-placeholder-shown:scale-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2', + peerfocus: 'peer-focus-within:scale-75 peer-focus-within:top-0 peer-focus-within:-translate-y-1/2', + animation: 'motion-safe:transition-all', + light: 'bg-white', + dark: 'dark:bg-zinc-900', + }, + style: { + default: { + light: 'peer-focus-within:text-inherit', + }, + primary: { + light: 'peer-focus-within:text-primary-500', + }, + secondary: { + light: 'peer-focus-within:text-secondary-500', + }, + tertiary: { + light: 'peer-focus-within:text-tertiary-500', + } + } + }, +}; + +export default FloatOptions; + +// const OverlapOptions = { +// input: { +// base: 'block w-full px-4 bg-transparent peer', +// animation: 'transition-all', +// reset: 'border-none focus:ring-0 focus-visible:outline-0', +// }, +// label: { +// base: 'w-full pointer-events-none', +// position: 'absolute top-1/2 left-3 -translate-y-1/2', +// focus: 'peer-focus:-top-2.5 peer-focus:text-sm peer-focus:bg-white peer-focus:translate-y-[unset] peer-focus:px-1', +// animation: 'transition-all', +// }, +// }; + +// export default OverlapOptions; \ No newline at end of file diff --git a/src/theme/index.js b/src/theme/index.js index b33012d..a8faa7e 100644 --- a/src/theme/index.js +++ b/src/theme/index.js @@ -2,6 +2,7 @@ import accordion from "./components/Accordion/Accordion.theme"; import avatar from "./components/Avatar/Avatar.theme"; import button from "./components/Button/Button.theme"; import card from "./components/Card/Card.theme"; +import input from "./components/Input/Input.theme"; import menu from "./components/Menu/Menu.theme"; import typography from "./components/Typography/Typography.theme"; @@ -10,6 +11,7 @@ const theme = { avatar, button, card, + input, menu, typography }