From 61676a968ee1d4e031f35b702462c8ae8ec46a4e Mon Sep 17 00:00:00 2001 From: rozwader Date: Sun, 28 Jun 2026 14:33:34 +0200 Subject: [PATCH] feat: added label highlighting on input focus --- .storybook/input.stories.tsx | 61 ++++++++++++++++++++++++++++++++++ src/components/Button/types.ts | 4 +-- src/components/Input/Input.tsx | 20 +++++++++++ src/components/Input/styles.ts | 27 +++++++++++++++ src/components/Input/types.ts | 6 ++++ src/components/Link/types.ts | 4 +-- src/index.tsx | 1 + src/theme.ts | 1 + 8 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 .storybook/input.stories.tsx create mode 100644 src/components/Input/Input.tsx create mode 100644 src/components/Input/styles.ts create mode 100644 src/components/Input/types.ts diff --git a/.storybook/input.stories.tsx b/.storybook/input.stories.tsx new file mode 100644 index 0000000..8b8fdc9 --- /dev/null +++ b/.storybook/input.stories.tsx @@ -0,0 +1,61 @@ +import { ThemeProvider } from 'styled-components'; +import { Input } from '../src/index'; +import { theme } from '../src/theme'; +import { GlobalStyles } from '../src/globalStyles'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import React from 'react'; +import ArrowForwardIos from '@mui/icons-material/ArrowForwardIos'; + +const meta = { + component: Input, + decorators: [ + (Story) => ( +
+ + + + +
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + type: 'text', + placeholder: 'Value...', + }, +}; + +export const WithLabel: Story = { + args: { + type: 'text', + placeholder: 'Value...', + $labelContent: 'Text', + }, +}; + +export const Example: Story = { + args: { + type: 'text', + placeholder: 'eg. Joe Dough', + $labelContent: 'Username', + }, +}; + +export const Multiple: Story = { + render: () => ( + <> + + + + + ), +}; diff --git a/src/components/Button/types.ts b/src/components/Button/types.ts index a8d65cc..5aa4a9a 100644 --- a/src/components/Button/types.ts +++ b/src/components/Button/types.ts @@ -1,6 +1,6 @@ -import type { ComponentPropsWithoutRef, ReactElement } from 'react'; +import type { ComponentPropsWithRef, ReactElement } from 'react'; -export interface ButtonProps extends ComponentPropsWithoutRef<'button'> { +export interface ButtonProps extends ComponentPropsWithRef<'button'> { $variant?: ButtonVariant; $size?: ButtonSize; $icon?: ReactElement; diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx new file mode 100644 index 0000000..57ae9b3 --- /dev/null +++ b/src/components/Input/Input.tsx @@ -0,0 +1,20 @@ +import { InputComponent, InputLabelComponent } from './styles.js'; +import type { InputProps } from './types.js'; +import React from 'react'; + +export const Input = ({ + $fullWidth = false, + $labelContent = undefined, + ...props +}: InputProps) => { + return ( + <> + {$labelContent !== undefined && ( + + {$labelContent} + + )} + + + ); +}; diff --git a/src/components/Input/styles.ts b/src/components/Input/styles.ts new file mode 100644 index 0000000..03dbbf3 --- /dev/null +++ b/src/components/Input/styles.ts @@ -0,0 +1,27 @@ +import { styled } from 'styled-components'; +import { theme } from '../../theme.js'; + +export const InputLabelComponent = styled.label<{ $fullWidth: boolean }>` + ${({ $fullWidth }) => $fullWidth && `width: 100%;`}; + font-size: 10pt; + padding: 8px; + color: ${theme.colors.lightGray}; + + &:has(+ input:focus) { + color: ${theme.colors.primary}; + text-decoration: underline; + } +`; + +export const InputComponent = styled.input<{ $fullWidth: boolean }>` + ${({ $fullWidth }) => ($fullWidth ? `width: 100%;` : `width: fit-content;`)} + padding: 8px; + border: 1px solid ${theme.colors.lightGray}; + border-radius: 6px; + + &:focus { + border: 1px solid ${theme.colors.primary}; + color: ${theme.colors.primary}; + outline: none; + } +`; diff --git a/src/components/Input/types.ts b/src/components/Input/types.ts new file mode 100644 index 0000000..036de0f --- /dev/null +++ b/src/components/Input/types.ts @@ -0,0 +1,6 @@ +import type { ComponentPropsWithRef } from 'react'; + +export interface InputProps extends ComponentPropsWithRef<'input'> { + $fullWidth?: boolean; + $labelContent?: string; +} diff --git a/src/components/Link/types.ts b/src/components/Link/types.ts index 324d59d..41eede4 100644 --- a/src/components/Link/types.ts +++ b/src/components/Link/types.ts @@ -1,7 +1,7 @@ -import type { ComponentPropsWithoutRef, ReactElement } from 'react'; +import type { ComponentPropsWithRef, ReactElement } from 'react'; import type { IconSide } from '../Button/types.js'; -export interface LinkProps extends ComponentPropsWithoutRef<'a'> { +export interface LinkProps extends ComponentPropsWithRef<'a'> { $color?: string; $icon?: ReactElement; $iconSide?: IconSide; diff --git a/src/index.tsx b/src/index.tsx index ba71a90..8964e4a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,2 +1,3 @@ export { Button } from './components/Button/Button.js'; export { Link } from './components/Link/Link.js'; +export { Input } from './components/Input/Input.js'; diff --git a/src/theme.ts b/src/theme.ts index 8d7c425..c6f6c76 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -10,5 +10,6 @@ export const theme = createTheme({ primaryDimmed: '#5b489a', secondary: '#8D86C9', secondaryDimmed: '#746ea8', + lightGray: '#a0a0a0', }, });