From cb4764f8670f67627c407591c89b8d3637c190a7 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 25 Feb 2022 18:02:55 +0100
Subject: refactor: replace label elements with Label component

---
 src/components/ContactForm/ContactForm.tsx | 53 +++++++++++++++++++-----------
 1 file changed, 33 insertions(+), 20 deletions(-)

(limited to 'src/components/ContactForm/ContactForm.tsx')

diff --git a/src/components/ContactForm/ContactForm.tsx b/src/components/ContactForm/ContactForm.tsx
index 6ab1e2b..d0e5ebe 100644
--- a/src/components/ContactForm/ContactForm.tsx
+++ b/src/components/ContactForm/ContactForm.tsx
@@ -1,5 +1,5 @@
 import { ButtonSubmit } from '@components/Buttons';
-import { Form, FormItem, Input, TextArea } from '@components/Form';
+import { Form, FormItem, Input, Label, TextArea } from '@components/Form';
 import { sendMail } from '@services/graphql/mutations';
 import { settings } from '@utils/config';
 import { FormEvent, useState } from 'react';
@@ -83,6 +83,34 @@ const ContactForm = () => {
     );
   };
 
+  const getLabel = (
+    body: string,
+    htmlFor: string,
+    required: boolean = false
+  ) => {
+    return <Label body={body} htmlFor={htmlFor} required={required} />;
+  };
+
+  const nameLabelBody = intl.formatMessage({
+    defaultMessage: 'Name',
+    description: 'ContactForm: name field label',
+  });
+
+  const emailLabelBody = intl.formatMessage({
+    defaultMessage: 'Email',
+    description: 'ContactForm: email field label',
+  });
+
+  const subjectLabelBody = intl.formatMessage({
+    defaultMessage: 'Subject',
+    description: 'ContactForm: subject field label',
+  });
+
+  const messageLabelBody = intl.formatMessage({
+    defaultMessage: 'Message',
+    description: 'ContactForm: message field label',
+  });
+
   return (
     <>
       <Form submitHandler={submitHandler}>
@@ -92,11 +120,7 @@ const ContactForm = () => {
             name="name"
             value={name}
             setValue={setName}
-            label={intl.formatMessage({
-              defaultMessage: 'Name',
-              description: 'ContactForm: name field label',
-            })}
-            required={true}
+            label={getLabel(nameLabelBody, 'contact-name', true)}
           />
         </FormItem>
         <FormItem>
@@ -106,11 +130,7 @@ const ContactForm = () => {
             name="email"
             value={email}
             setValue={setEmail}
-            label={intl.formatMessage({
-              defaultMessage: 'Email',
-              description: 'ContactForm: email field label',
-            })}
-            required={true}
+            label={getLabel(emailLabelBody, 'contact-email', true)}
           />
         </FormItem>
         <FormItem>
@@ -119,10 +139,7 @@ const ContactForm = () => {
             name="subject"
             value={subject}
             setValue={setSubject}
-            label={intl.formatMessage({
-              defaultMessage: 'Subject',
-              description: 'ContactForm: subject field label',
-            })}
+            label={getLabel(subjectLabelBody, 'contact-subject')}
           />
         </FormItem>
         <FormItem>
@@ -131,11 +148,7 @@ const ContactForm = () => {
             name="message"
             value={message}
             setValue={setMessage}
-            label={intl.formatMessage({
-              defaultMessage: 'Message',
-              description: 'ContactForm: message field label',
-            })}
-            required={true}
+            label={getLabel(messageLabelBody, 'contact-message', true)}
           />
         </FormItem>
         <FormItem>
-- 
cgit v1.2.3


From e26d821f738525477472e631d170d9ed218c1603 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 25 Feb 2022 19:17:09 +0100
Subject: chore: combine input/textarea/select in a single component

---
 src/components/CommentForm/CommentForm.tsx         |  18 ++--
 src/components/ContactForm/ContactForm.tsx         |  16 ++--
 src/components/Form/Form.module.scss               |  70 --------------
 src/components/Form/Form.tsx                       |  25 -----
 src/components/Form/FormItem/FormItem.tsx          |   7 --
 src/components/Form/Input/Input.tsx                |  50 ----------
 src/components/Form/Label/Label.module.scss        |  22 -----
 src/components/Form/Label/Label.tsx                |  24 -----
 src/components/Form/Select/Select.module.scss      |  23 -----
 src/components/Form/Select/Select.tsx              |  56 -----------
 src/components/Form/TextArea/TextArea.tsx          |  35 -------
 src/components/Form/Toggle/Toggle.module.scss      |  75 ---------------
 src/components/Form/Toggle/Toggle.tsx              |  46 ---------
 src/components/Form/index.tsx                      |   9 --
 .../FormElements/Field/Field.module.scss           |  48 ++++++++++
 src/components/FormElements/Field/Field.tsx        | 106 +++++++++++++++++++++
 src/components/FormElements/Form/Form.module.scss  |  37 +++++++
 src/components/FormElements/Form/Form.tsx          |  27 ++++++
 .../FormElements/FormItem/FormItem.module.scss     |   4 +
 src/components/FormElements/FormItem/FormItem.tsx  |   7 ++
 .../FormElements/Label/Label.module.scss           |  22 +++++
 src/components/FormElements/Label/Label.tsx        |  24 +++++
 .../FormElements/Toggle/Toggle.module.scss         |  75 +++++++++++++++
 src/components/FormElements/Toggle/Toggle.tsx      |  46 +++++++++
 src/components/FormElements/index.tsx              |   7 ++
 src/components/SearchForm/SearchForm.tsx           |   9 +-
 .../Settings/AckeeSelect/AckeeSelect.tsx           |   5 +-
 .../Settings/PrismThemeToggle/PrismThemeToggle.tsx |   2 +-
 .../Settings/ReduceMotion/ReduceMotion.tsx         |   2 +-
 .../Settings/ThemeToggle/ThemeToggle.tsx           |   2 +-
 30 files changed, 435 insertions(+), 464 deletions(-)
 delete mode 100644 src/components/Form/Form.module.scss
 delete mode 100644 src/components/Form/Form.tsx
 delete mode 100644 src/components/Form/FormItem/FormItem.tsx
 delete mode 100644 src/components/Form/Input/Input.tsx
 delete mode 100644 src/components/Form/Label/Label.module.scss
 delete mode 100644 src/components/Form/Label/Label.tsx
 delete mode 100644 src/components/Form/Select/Select.module.scss
 delete mode 100644 src/components/Form/Select/Select.tsx
 delete mode 100644 src/components/Form/TextArea/TextArea.tsx
 delete mode 100644 src/components/Form/Toggle/Toggle.module.scss
 delete mode 100644 src/components/Form/Toggle/Toggle.tsx
 delete mode 100644 src/components/Form/index.tsx
 create mode 100644 src/components/FormElements/Field/Field.module.scss
 create mode 100644 src/components/FormElements/Field/Field.tsx
 create mode 100644 src/components/FormElements/Form/Form.module.scss
 create mode 100644 src/components/FormElements/Form/Form.tsx
 create mode 100644 src/components/FormElements/FormItem/FormItem.module.scss
 create mode 100644 src/components/FormElements/FormItem/FormItem.tsx
 create mode 100644 src/components/FormElements/Label/Label.module.scss
 create mode 100644 src/components/FormElements/Label/Label.tsx
 create mode 100644 src/components/FormElements/Toggle/Toggle.module.scss
 create mode 100644 src/components/FormElements/Toggle/Toggle.tsx
 create mode 100644 src/components/FormElements/index.tsx

(limited to 'src/components/ContactForm/ContactForm.tsx')

diff --git a/src/components/CommentForm/CommentForm.tsx b/src/components/CommentForm/CommentForm.tsx
index 3539311..c409d04 100644
--- a/src/components/CommentForm/CommentForm.tsx
+++ b/src/components/CommentForm/CommentForm.tsx
@@ -1,5 +1,5 @@
 import { ButtonSubmit } from '@components/Buttons';
-import { Form, FormItem, Input, Label, TextArea } from '@components/Form';
+import { Field, Form, FormItem, Label } from '@components/FormElements';
 import Notice from '@components/Notice/Notice';
 import Spinner from '@components/Spinner/Spinner';
 import { createComment } from '@services/graphql/mutations';
@@ -144,30 +144,32 @@ const CommentForm = (
       </h2>
       <Form
         submitHandler={submitHandler}
-        modifier={isReply ? 'centered' : undefined}
+        kind={isReply ? 'centered' : undefined}
       >
         <FormItem>
-          <Input
+          <Field
             id="commenter-name"
             name="commenter-name"
             label={getLabel(nameLabelBody, 'commenter-name', true)}
             value={name}
             setValue={setName}
+            required={true}
             ref={ref}
           />
         </FormItem>
         <FormItem>
-          <Input
+          <Field
             id="commenter-email"
             name="commenter-email"
-            type="email"
+            kind="email"
             label={getLabel(emailLabelBody, 'commenter-email', true)}
             value={email}
             setValue={setEmail}
+            required={true}
           />
         </FormItem>
         <FormItem>
-          <Input
+          <Field
             id="commenter-website"
             name="commenter-website"
             label={getLabel(websiteLabelBody, 'commenter-website')}
@@ -176,12 +178,14 @@ const CommentForm = (
           />
         </FormItem>
         <FormItem>
-          <TextArea
+          <Field
             id="commenter-comment"
             name="commenter-comment"
+            kind="textarea"
             label={getLabel(commentLabelBody, 'commenter-comment', true)}
             value={comment}
             setValue={setComment}
+            required={true}
           />
         </FormItem>
         <FormItem>
diff --git a/src/components/ContactForm/ContactForm.tsx b/src/components/ContactForm/ContactForm.tsx
index d0e5ebe..48772a0 100644
--- a/src/components/ContactForm/ContactForm.tsx
+++ b/src/components/ContactForm/ContactForm.tsx
@@ -1,5 +1,5 @@
 import { ButtonSubmit } from '@components/Buttons';
-import { Form, FormItem, Input, Label, TextArea } from '@components/Form';
+import { Field, Form, FormItem, Label } from '@components/FormElements';
 import { sendMail } from '@services/graphql/mutations';
 import { settings } from '@utils/config';
 import { FormEvent, useState } from 'react';
@@ -115,26 +115,28 @@ const ContactForm = () => {
     <>
       <Form submitHandler={submitHandler}>
         <FormItem>
-          <Input
+          <Field
             id="contact-name"
             name="name"
             value={name}
             setValue={setName}
+            required={true}
             label={getLabel(nameLabelBody, 'contact-name', true)}
           />
         </FormItem>
         <FormItem>
-          <Input
+          <Field
             id="contact-email"
-            type="email"
+            kind="email"
             name="email"
             value={email}
             setValue={setEmail}
+            required={true}
             label={getLabel(emailLabelBody, 'contact-email', true)}
           />
         </FormItem>
         <FormItem>
-          <Input
+          <Field
             id="contact-subject"
             name="subject"
             value={subject}
@@ -143,11 +145,13 @@ const ContactForm = () => {
           />
         </FormItem>
         <FormItem>
-          <TextArea
+          <Field
             id="contact-message"
+            kind="textarea"
             name="message"
             value={message}
             setValue={setMessage}
+            required={true}
             label={getLabel(messageLabelBody, 'contact-message', true)}
           />
         </FormItem>
diff --git a/src/components/Form/Form.module.scss b/src/components/Form/Form.module.scss
deleted file mode 100644
index 6ccdb11..0000000
--- a/src/components/Form/Form.module.scss
+++ /dev/null
@@ -1,70 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.wrapper {
-  width: 100%;
-
-  &--search,
-  &--toggle {
-    display: flex;
-    flex-flow: row nowrap;
-    align-items: center;
-  }
-
-  &--toggle {
-    position: relative;
-    margin: var(--spacing-sm) 0;
-  }
-
-  &--centered {
-    max-width: 45ch;
-    margin-left: auto;
-    margin-right: auto;
-  }
-}
-
-.item {
-  margin: var(--spacing-xs) 0;
-  max-width: 45ch;
-}
-
-.field {
-  width: 100%;
-  padding: var(--spacing-2xs) var(--spacing-xs);
-  background: var(--color-bg-tertiary);
-  border: fun.convert-px(2) solid var(--color-border);
-  box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow);
-  transition: all 0.25s linear 0s;
-
-  &:hover {
-    box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1)
-      var(--color-shadow);
-    transform: translate(#{fun.convert-px(-3)}, #{fun.convert-px(-3)});
-  }
-
-  &:focus {
-    background: var(--color-bg);
-    border-color: var(--color-primary-darker);
-    box-shadow: 0 0 0 0 var(--color-shadow);
-    transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)});
-    outline: none;
-    transition: all 0.2s ease-in-out 0s, transform 0.3s ease-out 0s;
-  }
-}
-
-.textarea {
-  min-height: fun.convert-px(200);
-}
-
-.wrapper--search {
-  > input {
-    padding-right: calc(var(--btn-size) + var(--spacing-2xs));
-
-    &:hover ~ button {
-      transform: translate(fun.convert-px(-3), fun.convert-px(-3));
-    }
-
-    &:focus ~ button {
-      transform: translate(fun.convert-px(3), fun.convert-px(3));
-    }
-  }
-}
diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx
deleted file mode 100644
index 44763c4..0000000
--- a/src/components/Form/Form.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { ReactNode } from 'react';
-import styles from './Form.module.scss';
-
-const Form = ({
-  children,
-  submitHandler,
-  modifier = '',
-  id,
-}: {
-  children: ReactNode;
-  submitHandler: any;
-  modifier?: string;
-  id?: string;
-}) => {
-  const withModifier = modifier ? styles[`wrapper--${modifier}`] : '';
-  const classes = `${styles.wrapper} ${withModifier}`;
-
-  return (
-    <form onSubmit={submitHandler} className={classes} id={id}>
-      {children}
-    </form>
-  );
-};
-
-export default Form;
diff --git a/src/components/Form/FormItem/FormItem.tsx b/src/components/Form/FormItem/FormItem.tsx
deleted file mode 100644
index 0f12e64..0000000
--- a/src/components/Form/FormItem/FormItem.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import styles from '../Form.module.scss';
-
-const FormItem: React.FunctionComponent = ({ children }) => {
-  return <div className={styles.item}>{children}</div>;
-};
-
-export default FormItem;
diff --git a/src/components/Form/Input/Input.tsx b/src/components/Form/Input/Input.tsx
deleted file mode 100644
index 07f0410..0000000
--- a/src/components/Form/Input/Input.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import {
-  ChangeEvent,
-  ForwardedRef,
-  forwardRef,
-  ReactElement,
-  SetStateAction,
-} from 'react';
-import styles from '../Form.module.scss';
-
-type InputType = 'text' | 'email' | 'number' | 'search';
-
-const Input = (
-  {
-    id,
-    name,
-    value,
-    setValue,
-    type = 'text',
-    label,
-  }: {
-    id: string;
-    name: string;
-    value: string;
-    setValue: (value: SetStateAction<string>) => void;
-    type?: InputType;
-    label?: ReactElement;
-  },
-  ref: ForwardedRef<HTMLInputElement>
-) => {
-  const updateValue = (e: ChangeEvent<HTMLInputElement>) => {
-    setValue(e.target.value);
-  };
-
-  return (
-    <>
-      {label}
-      <input
-        ref={ref}
-        type={type}
-        id={id}
-        name={name}
-        value={value}
-        onChange={updateValue}
-        className={styles.field}
-      />
-    </>
-  );
-};
-
-export default forwardRef(Input);
diff --git a/src/components/Form/Label/Label.module.scss b/src/components/Form/Label/Label.module.scss
deleted file mode 100644
index c527b16..0000000
--- a/src/components/Form/Label/Label.module.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.regular {
-  display: block;
-  color: var(--color-primary-darker);
-  font-size: var(--font-size-sm);
-  font-variant: small-caps;
-  font-weight: 600;
-}
-
-.settings {
-  --icon-size: #{fun.convert-px(25)};
-  --toggle-width: #{fun.convert-px(45)};
-  --toggle-height: calc(var(--toggle-width) / 2);
-
-  display: inline-flex;
-  align-items: center;
-}
-
-.required {
-  color: var(--color-secondary);
-}
diff --git a/src/components/Form/Label/Label.tsx b/src/components/Form/Label/Label.tsx
deleted file mode 100644
index baedff0..0000000
--- a/src/components/Form/Label/Label.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import styles from './Label.module.scss';
-
-type LabelKind = 'regular' | 'settings';
-
-const Label = ({
-  body,
-  htmlFor,
-  required = false,
-  kind = 'regular',
-}: {
-  body: string;
-  htmlFor: string;
-  required?: boolean;
-  kind?: LabelKind;
-}) => {
-  return (
-    <label htmlFor={htmlFor} className={styles[kind]}>
-      {body}
-      {required && <span className={styles.required}> *</span>}
-    </label>
-  );
-};
-
-export default Label;
diff --git a/src/components/Form/Select/Select.module.scss b/src/components/Form/Select/Select.module.scss
deleted file mode 100644
index d4a40eb..0000000
--- a/src/components/Form/Select/Select.module.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.wrapper {
-  padding: fun.convert-px(3) var(--spacing-xs);
-  background: var(--color-bg-tertiary);
-  border: fun.convert-px(2) solid var(--color-border);
-  box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow);
-  cursor: pointer;
-  transition: all 0.3s ease-in-out 0s;
-
-  &:hover {
-    box-shadow: fun.convert-px(4) fun.convert-px(4) 0 fun.convert-px(1)
-      var(--color-shadow);
-    transform: translate(#{fun.convert-px(-2)}, #{fun.convert-px(-2)});
-  }
-
-  &:focus {
-    background: var(--color-bg);
-    border-color: var(--color-primary);
-    box-shadow: 0 0 0 0 var(--color-shadow);
-    transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)});
-  }
-}
diff --git a/src/components/Form/Select/Select.tsx b/src/components/Form/Select/Select.tsx
deleted file mode 100644
index feab991..0000000
--- a/src/components/Form/Select/Select.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { ChangeEvent, ReactElement, SetStateAction } from 'react';
-import styles from './Select.module.scss';
-
-type SelectOptions = {
-  id: string;
-  name: string;
-  value: string;
-};
-
-const Select = ({
-  options,
-  id,
-  name,
-  value,
-  setValue,
-  required = false,
-  label,
-}: {
-  options: SelectOptions[];
-  id: string;
-  name: string;
-  value: string;
-  setValue: (value: SetStateAction<string>) => void;
-  required?: boolean;
-  label?: ReactElement;
-}) => {
-  const getOptions = () => {
-    return options.map((option) => (
-      <option key={option.id} value={option.value}>
-        {option.name}
-      </option>
-    ));
-  };
-
-  const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
-    setValue(event.target.value);
-  };
-
-  return (
-    <>
-      {label}
-      <select
-        name={name}
-        id={id}
-        value={value}
-        onChange={handleChange}
-        required={required}
-        className={styles.wrapper}
-      >
-        {getOptions()}
-      </select>
-    </>
-  );
-};
-
-export default Select;
diff --git a/src/components/Form/TextArea/TextArea.tsx b/src/components/Form/TextArea/TextArea.tsx
deleted file mode 100644
index b8894ab..0000000
--- a/src/components/Form/TextArea/TextArea.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { ChangeEvent, ReactElement, SetStateAction } from 'react';
-import styles from '../Form.module.scss';
-
-const TextArea = ({
-  id,
-  name,
-  value,
-  setValue,
-  label,
-}: {
-  id: string;
-  name: string;
-  value: string;
-  setValue: (value: SetStateAction<string>) => void;
-  label?: ReactElement;
-}) => {
-  const updateValue = (e: ChangeEvent<HTMLTextAreaElement>) => {
-    setValue(e.target.value);
-  };
-
-  return (
-    <>
-      {label}
-      <textarea
-        id={id}
-        name={name}
-        value={value}
-        onChange={updateValue}
-        className={`${styles.field} ${styles.textarea}`}
-      />
-    </>
-  );
-};
-
-export default TextArea;
diff --git a/src/components/Form/Toggle/Toggle.module.scss b/src/components/Form/Toggle/Toggle.module.scss
deleted file mode 100644
index 48c88f6..0000000
--- a/src/components/Form/Toggle/Toggle.module.scss
+++ /dev/null
@@ -1,75 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.label {
-  --icon-size: #{fun.convert-px(25)};
-  --toggle-width: #{fun.convert-px(45)};
-  --toggle-height: calc(var(--toggle-width) / 2);
-
-  display: inline-flex;
-  align-items: center;
-}
-
-.title {
-  margin-right: var(--spacing-xs);
-}
-
-.toggle {
-  display: inline-flex;
-  align-items: center;
-  width: var(--toggle-width);
-  height: var(--toggle-height);
-  background: var(--color-shadow-light);
-  border: fun.convert-px(1) solid var(--color-primary);
-  border-radius: fun.convert-px(32);
-  box-shadow: inset 0 0 fun.convert-px(3) 0 var(--color-shadow-dark);
-  margin: 0 var(--spacing-2xs);
-  position: relative;
-
-  &::after {
-    content: "";
-    display: block;
-    width: calc(var(--toggle-width) / 2);
-    height: calc(var(--toggle-width) / 2);
-    background: var(--color-primary-light);
-    border: fun.convert-px(1) solid var(--color-primary);
-    border-radius: 50%;
-    box-shadow: inset 0 0 fun.convert-px(1) fun.convert-px(1)
-        var(--color-shadow),
-      0 0 fun.convert-px(2) fun.convert-px(1) var(--color-shadow-light);
-    position: absolute;
-    left: fun.convert-px(-2);
-    transition: all 0.3s ease-in-out 0s;
-  }
-}
-
-.checkbox {
-  position: absolute;
-  opacity: 0;
-  cursor: pointer;
-
-  &:checked ~ .label {
-    .toggle::after {
-      position: absolute;
-      left: calc(100% - (var(--toggle-width) / 2) + #{fun.convert-px(2)});
-    }
-  }
-
-  &:hover,
-  &:focus {
-    ~ .label {
-      .toggle::after {
-        background: var(--color-primary-lighter);
-      }
-    }
-  }
-
-  &:focus ~ .label {
-    .title {
-      text-decoration: underline solid var(--color-primary) fun.convert-px(2);
-    }
-
-    .toggle {
-      outline: var(--color-border) solid fun.convert-px(5);
-    }
-  }
-}
diff --git a/src/components/Form/Toggle/Toggle.tsx b/src/components/Form/Toggle/Toggle.tsx
deleted file mode 100644
index 36636e4..0000000
--- a/src/components/Form/Toggle/Toggle.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { FormEvent, ReactElement } from 'react';
-import { Form } from '..';
-import styles from './Toggle.module.scss';
-
-const Toggle = ({
-  id,
-  label,
-  value,
-  changeHandler,
-  leftChoice,
-  rightChoice,
-  name,
-}: {
-  id: string;
-  label: string;
-  value: boolean;
-  changeHandler: (value: boolean) => void;
-  leftChoice: ReactElement | string;
-  rightChoice: ReactElement | string;
-  name?: string;
-}) => {
-  const onSubmit = (e: FormEvent) => {
-    e.preventDefault();
-  };
-
-  return (
-    <Form modifier="toggle" submitHandler={onSubmit}>
-      <input
-        className={styles.checkbox}
-        type="checkbox"
-        id={id}
-        name={name ? name : id}
-        checked={value}
-        onChange={() => changeHandler(!value)}
-      />
-      <label htmlFor={id} className={styles.label}>
-        <span className={styles.title}>{label}</span>
-        {leftChoice}
-        <span className={styles.toggle}></span>
-        {rightChoice}
-      </label>
-    </Form>
-  );
-};
-
-export default Toggle;
diff --git a/src/components/Form/index.tsx b/src/components/Form/index.tsx
deleted file mode 100644
index cbe1ec4..0000000
--- a/src/components/Form/index.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import Form from './Form';
-import FormItem from './FormItem/FormItem';
-import Input from './Input/Input';
-import Label from './Label/Label';
-import Select from './Select/Select';
-import TextArea from './TextArea/TextArea';
-import Toggle from './Toggle/Toggle';
-
-export { Form, FormItem, Input, Label, Select, TextArea, Toggle };
diff --git a/src/components/FormElements/Field/Field.module.scss b/src/components/FormElements/Field/Field.module.scss
new file mode 100644
index 0000000..3836856
--- /dev/null
+++ b/src/components/FormElements/Field/Field.module.scss
@@ -0,0 +1,48 @@
+@use "@styles/abstracts/functions" as fun;
+
+.field {
+  background: var(--color-bg-tertiary);
+  border: fun.convert-px(2) solid var(--color-border);
+  box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow);
+  transition: all 0.25s linear 0s;
+
+  &:not(.select) {
+    width: 100%;
+    padding: var(--spacing-2xs) var(--spacing-xs);
+  }
+
+  &:hover {
+    box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1)
+      var(--color-shadow);
+    transform: translate(#{fun.convert-px(-3)}, #{fun.convert-px(-3)});
+  }
+
+  &:focus {
+    background: var(--color-bg);
+    border-color: var(--color-primary);
+    box-shadow: 0 0 0 0 var(--color-shadow);
+    transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)});
+    outline: none;
+    transition: all 0.2s ease-in-out 0s, transform 0.3s ease-out 0s;
+  }
+}
+
+.select {
+  padding: fun.convert-px(3) var(--spacing-xs);
+  cursor: pointer;
+
+  &:hover {
+    box-shadow: fun.convert-px(4) fun.convert-px(4) 0 fun.convert-px(1)
+      var(--color-shadow);
+    transform: translate(#{fun.convert-px(-2)}, #{fun.convert-px(-2)});
+  }
+
+  &:focus {
+    box-shadow: 0 0 0 0 var(--color-shadow);
+    transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)});
+  }
+}
+
+.textarea {
+  min-height: fun.convert-px(200);
+}
diff --git a/src/components/FormElements/Field/Field.tsx b/src/components/FormElements/Field/Field.tsx
new file mode 100644
index 0000000..c8df0f9
--- /dev/null
+++ b/src/components/FormElements/Field/Field.tsx
@@ -0,0 +1,106 @@
+import {
+  ChangeEvent,
+  ForwardedRef,
+  forwardRef,
+  ReactElement,
+  SetStateAction,
+} from 'react';
+import styles from './Field.module.scss';
+
+type FieldType = 'email' | 'number' | 'search' | 'select' | 'text' | 'textarea';
+type SelectOptions = {
+  id: string;
+  name: string;
+  value: string;
+};
+
+const Field = (
+  {
+    id,
+    name,
+    value,
+    setValue,
+    required = false,
+    kind = 'text',
+    label,
+    options,
+  }: {
+    id: string;
+    name: string;
+    value: string;
+    setValue: (value: SetStateAction<string>) => void;
+    required?: boolean;
+    kind?: FieldType;
+    label?: ReactElement;
+    options?: SelectOptions[];
+  },
+  ref: ForwardedRef<HTMLInputElement | HTMLTextAreaElement>
+) => {
+  const updateValue = (
+    e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
+  ) => {
+    setValue(e.target.value);
+  };
+
+  const getOptions = () => {
+    return options
+      ? options.map((option) => (
+          <option key={option.id} value={option.value}>
+            {option.name}
+          </option>
+        ))
+      : '';
+  };
+
+  const getField = () => {
+    switch (kind) {
+      case 'select':
+        return (
+          <select
+            name={name}
+            id={id}
+            value={value}
+            onChange={updateValue}
+            required={required}
+            className={`${styles.field} ${styles.select}`}
+          >
+            {getOptions()}
+          </select>
+        );
+      case 'textarea':
+        return (
+          <textarea
+            ref={ref as ForwardedRef<HTMLTextAreaElement>}
+            id={id}
+            name={name}
+            value={value}
+            required={required}
+            onChange={updateValue}
+            className={`${styles.field} ${styles.textarea}`}
+          />
+        );
+      default:
+        return (
+          <input
+            ref={ref as ForwardedRef<HTMLInputElement>}
+            type={kind}
+            id={id}
+            name={name}
+            value={value}
+            required={required}
+            onChange={updateValue}
+            className={styles.field}
+          />
+        );
+    }
+  };
+
+  return (
+    <>
+      {label}
+      {getField()}
+    </>
+  );
+};
+
+export default forwardRef(Field);
diff --git a/src/components/FormElements/Form/Form.module.scss b/src/components/FormElements/Form/Form.module.scss
new file mode 100644
index 0000000..0f7c437
--- /dev/null
+++ b/src/components/FormElements/Form/Form.module.scss
@@ -0,0 +1,37 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+  width: 100%;
+}
+
+.centered {
+  max-width: 45ch;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.search {
+  display: flex;
+  flex-flow: row nowrap;
+  align-items: center;
+
+  > input {
+    padding-right: calc(var(--btn-size) + var(--spacing-2xs));
+
+    &:hover ~ button {
+      transform: translate(fun.convert-px(-3), fun.convert-px(-3));
+    }
+
+    &:focus ~ button {
+      transform: translate(fun.convert-px(3), fun.convert-px(3));
+    }
+  }
+}
+
+.settings {
+  display: flex;
+  flex-flow: row nowrap;
+  align-items: center;
+  margin: var(--spacing-sm) 0;
+  position: relative;
+}
diff --git a/src/components/FormElements/Form/Form.tsx b/src/components/FormElements/Form/Form.tsx
new file mode 100644
index 0000000..10fdcdf
--- /dev/null
+++ b/src/components/FormElements/Form/Form.tsx
@@ -0,0 +1,27 @@
+import { ReactNode } from 'react';
+import styles from './Form.module.scss';
+
+type FormKind = 'centered' | 'search' | 'settings';
+
+const Form = ({
+  children,
+  submitHandler,
+  kind,
+  id,
+}: {
+  children: ReactNode;
+  submitHandler: any;
+  kind?: FormKind;
+  id?: string;
+}) => {
+  const kindStyles = kind ? styles[kind] : '';
+  const classes = `${styles.wrapper} ${kindStyles}`;
+
+  return (
+    <form onSubmit={submitHandler} className={classes} id={id}>
+      {children}
+    </form>
+  );
+};
+
+export default Form;
diff --git a/src/components/FormElements/FormItem/FormItem.module.scss b/src/components/FormElements/FormItem/FormItem.module.scss
new file mode 100644
index 0000000..07ef56f
--- /dev/null
+++ b/src/components/FormElements/FormItem/FormItem.module.scss
@@ -0,0 +1,4 @@
+.wrapper {
+  margin: var(--spacing-xs) 0;
+  max-width: 45ch;
+}
diff --git a/src/components/FormElements/FormItem/FormItem.tsx b/src/components/FormElements/FormItem/FormItem.tsx
new file mode 100644
index 0000000..8d674f1
--- /dev/null
+++ b/src/components/FormElements/FormItem/FormItem.tsx
@@ -0,0 +1,7 @@
+import styles from './FormItem.module.scss';
+
+const FormItem: React.FunctionComponent = ({ children }) => {
+  return <div className={styles.wrapper}>{children}</div>;
+};
+
+export default FormItem;
diff --git a/src/components/FormElements/Label/Label.module.scss b/src/components/FormElements/Label/Label.module.scss
new file mode 100644
index 0000000..c527b16
--- /dev/null
+++ b/src/components/FormElements/Label/Label.module.scss
@@ -0,0 +1,22 @@
+@use "@styles/abstracts/functions" as fun;
+
+.regular {
+  display: block;
+  color: var(--color-primary-darker);
+  font-size: var(--font-size-sm);
+  font-variant: small-caps;
+  font-weight: 600;
+}
+
+.settings {
+  --icon-size: #{fun.convert-px(25)};
+  --toggle-width: #{fun.convert-px(45)};
+  --toggle-height: calc(var(--toggle-width) / 2);
+
+  display: inline-flex;
+  align-items: center;
+}
+
+.required {
+  color: var(--color-secondary);
+}
diff --git a/src/components/FormElements/Label/Label.tsx b/src/components/FormElements/Label/Label.tsx
new file mode 100644
index 0000000..baedff0
--- /dev/null
+++ b/src/components/FormElements/Label/Label.tsx
@@ -0,0 +1,24 @@
+import styles from './Label.module.scss';
+
+type LabelKind = 'regular' | 'settings';
+
+const Label = ({
+  body,
+  htmlFor,
+  required = false,
+  kind = 'regular',
+}: {
+  body: string;
+  htmlFor: string;
+  required?: boolean;
+  kind?: LabelKind;
+}) => {
+  return (
+    <label htmlFor={htmlFor} className={styles[kind]}>
+      {body}
+      {required && <span className={styles.required}> *</span>}
+    </label>
+  );
+};
+
+export default Label;
diff --git a/src/components/FormElements/Toggle/Toggle.module.scss b/src/components/FormElements/Toggle/Toggle.module.scss
new file mode 100644
index 0000000..48c88f6
--- /dev/null
+++ b/src/components/FormElements/Toggle/Toggle.module.scss
@@ -0,0 +1,75 @@
+@use "@styles/abstracts/functions" as fun;
+
+.label {
+  --icon-size: #{fun.convert-px(25)};
+  --toggle-width: #{fun.convert-px(45)};
+  --toggle-height: calc(var(--toggle-width) / 2);
+
+  display: inline-flex;
+  align-items: center;
+}
+
+.title {
+  margin-right: var(--spacing-xs);
+}
+
+.toggle {
+  display: inline-flex;
+  align-items: center;
+  width: var(--toggle-width);
+  height: var(--toggle-height);
+  background: var(--color-shadow-light);
+  border: fun.convert-px(1) solid var(--color-primary);
+  border-radius: fun.convert-px(32);
+  box-shadow: inset 0 0 fun.convert-px(3) 0 var(--color-shadow-dark);
+  margin: 0 var(--spacing-2xs);
+  position: relative;
+
+  &::after {
+    content: "";
+    display: block;
+    width: calc(var(--toggle-width) / 2);
+    height: calc(var(--toggle-width) / 2);
+    background: var(--color-primary-light);
+    border: fun.convert-px(1) solid var(--color-primary);
+    border-radius: 50%;
+    box-shadow: inset 0 0 fun.convert-px(1) fun.convert-px(1)
+        var(--color-shadow),
+      0 0 fun.convert-px(2) fun.convert-px(1) var(--color-shadow-light);
+    position: absolute;
+    left: fun.convert-px(-2);
+    transition: all 0.3s ease-in-out 0s;
+  }
+}
+
+.checkbox {
+  position: absolute;
+  opacity: 0;
+  cursor: pointer;
+
+  &:checked ~ .label {
+    .toggle::after {
+      position: absolute;
+      left: calc(100% - (var(--toggle-width) / 2) + #{fun.convert-px(2)});
+    }
+  }
+
+  &:hover,
+  &:focus {
+    ~ .label {
+      .toggle::after {
+        background: var(--color-primary-lighter);
+      }
+    }
+  }
+
+  &:focus ~ .label {
+    .title {
+      text-decoration: underline solid var(--color-primary) fun.convert-px(2);
+    }
+
+    .toggle {
+      outline: var(--color-border) solid fun.convert-px(5);
+    }
+  }
+}
diff --git a/src/components/FormElements/Toggle/Toggle.tsx b/src/components/FormElements/Toggle/Toggle.tsx
new file mode 100644
index 0000000..4db7d43
--- /dev/null
+++ b/src/components/FormElements/Toggle/Toggle.tsx
@@ -0,0 +1,46 @@
+import { FormEvent, ReactElement } from 'react';
+import { Form } from '..';
+import styles from './Toggle.module.scss';
+
+const Toggle = ({
+  id,
+  label,
+  value,
+  changeHandler,
+  leftChoice,
+  rightChoice,
+  name,
+}: {
+  id: string;
+  label: string;
+  value: boolean;
+  changeHandler: (value: boolean) => void;
+  leftChoice: ReactElement | string;
+  rightChoice: ReactElement | string;
+  name?: string;
+}) => {
+  const onSubmit = (e: FormEvent) => {
+    e.preventDefault();
+  };
+
+  return (
+    <Form kind="settings" submitHandler={onSubmit}>
+      <input
+        className={styles.checkbox}
+        type="checkbox"
+        id={id}
+        name={name ? name : id}
+        checked={value}
+        onChange={() => changeHandler(!value)}
+      />
+      <label htmlFor={id} className={styles.label}>
+        <span className={styles.title}>{label}</span>
+        {leftChoice}
+        <span className={styles.toggle}></span>
+        {rightChoice}
+      </label>
+    </Form>
+  );
+};
+
+export default Toggle;
diff --git a/src/components/FormElements/index.tsx b/src/components/FormElements/index.tsx
new file mode 100644
index 0000000..8ca69b4
--- /dev/null
+++ b/src/components/FormElements/index.tsx
@@ -0,0 +1,7 @@
+import Field from './Field/Field';
+import Form from './Form/Form';
+import FormItem from './FormItem/FormItem';
+import Label from './Label/Label';
+import Toggle from './Toggle/Toggle';
+
+export { Field, Form, FormItem, Label, Toggle };
diff --git a/src/components/SearchForm/SearchForm.tsx b/src/components/SearchForm/SearchForm.tsx
index fa3ad07..fe809a3 100644
--- a/src/components/SearchForm/SearchForm.tsx
+++ b/src/components/SearchForm/SearchForm.tsx
@@ -1,5 +1,5 @@
 import { ButtonSubmit } from '@components/Buttons';
-import { Form, Input } from '@components/Form';
+import { Field, Form } from '@components/FormElements';
 import { SearchIcon } from '@components/Icons';
 import { useRouter } from 'next/router';
 import { FormEvent, useEffect, useRef, useState } from 'react';
@@ -34,20 +34,21 @@ const SearchForm = ({ isOpened }: { isOpened: boolean }) => {
           description: 'SearchForm : form title',
         })}
       </div>
-      <Form submitHandler={launchSearch} modifier="search" id="search">
+      <Form submitHandler={launchSearch} kind="search" id="search">
         <label htmlFor="search-query" className="screen-reader-text">
           {intl.formatMessage({
             defaultMessage: 'Keywords:',
             description: 'SearchForm: search field label',
           })}
         </label>
-        <Input
+        <Field
           ref={inputRef}
           id="search-query"
           name="search-query"
-          type="search"
+          kind="search"
           value={query}
           setValue={setQuery}
+          required={true}
         />
         <ButtonSubmit modifier="search">
           <SearchIcon />
diff --git a/src/components/Settings/AckeeSelect/AckeeSelect.tsx b/src/components/Settings/AckeeSelect/AckeeSelect.tsx
index 43110c4..e1a8ed8 100644
--- a/src/components/Settings/AckeeSelect/AckeeSelect.tsx
+++ b/src/components/Settings/AckeeSelect/AckeeSelect.tsx
@@ -1,4 +1,4 @@
-import { Label, Select } from '@components/Form';
+import { Field, Label } from '@components/FormElements';
 import Tooltip from '@components/Tooltip/Tooltip';
 import { LocalStorage } from '@services/local-storage';
 import { useAckeeTracker } from '@utils/providers/ackee';
@@ -67,9 +67,10 @@ const AckeeSelect = () => {
 
   return (
     <div className={styles.wrapper}>
-      <Select
+      <Field
         id="ackee-settings"
         name="ackee-settings"
+        kind="select"
         label={label}
         options={options}
         value={value}
diff --git a/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx b/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx
index 06f7ac8..6f42f86 100644
--- a/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx
+++ b/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx
@@ -1,4 +1,4 @@
-import { Toggle } from '@components/Form';
+import { Toggle } from '@components/FormElements';
 import { MoonIcon, SunIcon } from '@components/Icons';
 import Spinner from '@components/Spinner/Spinner';
 import { usePrismTheme } from '@utils/providers/prism';
diff --git a/src/components/Settings/ReduceMotion/ReduceMotion.tsx b/src/components/Settings/ReduceMotion/ReduceMotion.tsx
index c7b5775..d0f7980 100644
--- a/src/components/Settings/ReduceMotion/ReduceMotion.tsx
+++ b/src/components/Settings/ReduceMotion/ReduceMotion.tsx
@@ -1,4 +1,4 @@
-import { Toggle } from '@components/Form';
+import { Toggle } from '@components/FormElements';
 import { LocalStorage } from '@services/local-storage';
 import { useEffect, useState } from 'react';
 import { useIntl } from 'react-intl';
diff --git a/src/components/Settings/ThemeToggle/ThemeToggle.tsx b/src/components/Settings/ThemeToggle/ThemeToggle.tsx
index 5b7a34d..77b2797 100644
--- a/src/components/Settings/ThemeToggle/ThemeToggle.tsx
+++ b/src/components/Settings/ThemeToggle/ThemeToggle.tsx
@@ -1,4 +1,4 @@
-import { Toggle } from '@components/Form';
+import { Toggle } from '@components/FormElements';
 import { MoonIcon, SunIcon } from '@components/Icons';
 import Spinner from '@components/Spinner/Spinner';
 import { useTheme } from 'next-themes';
-- 
cgit v1.2.3

