@cxxi-lab/hub-ui 0.0.0-stage → 0.1.1

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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/fonts/MerriweatherSans/MerriweatherSans-Bold.woff +0 -0
  3. package/fonts/MerriweatherSans/MerriweatherSans-Bold.woff2 +0 -0
  4. package/fonts/MerriweatherSans/MerriweatherSans-BoldItalic.woff +0 -0
  5. package/fonts/MerriweatherSans/MerriweatherSans-BoldItalic.woff2 +0 -0
  6. package/fonts/MerriweatherSans/MerriweatherSans-ExtraBold.woff +0 -0
  7. package/fonts/MerriweatherSans/MerriweatherSans-ExtraBold.woff2 +0 -0
  8. package/fonts/MerriweatherSans/MerriweatherSans-ExtraBoldItalic.woff +0 -0
  9. package/fonts/MerriweatherSans/MerriweatherSans-ExtraBoldItalic.woff2 +0 -0
  10. package/fonts/MerriweatherSans/MerriweatherSans-Italic.woff +0 -0
  11. package/fonts/MerriweatherSans/MerriweatherSans-Italic.woff2 +0 -0
  12. package/fonts/MerriweatherSans/MerriweatherSans-Light.woff +0 -0
  13. package/fonts/MerriweatherSans/MerriweatherSans-Light.woff2 +0 -0
  14. package/fonts/MerriweatherSans/MerriweatherSans-LightItalic.woff +0 -0
  15. package/fonts/MerriweatherSans/MerriweatherSans-LightItalic.woff2 +0 -0
  16. package/fonts/MerriweatherSans/MerriweatherSans-Medium.woff +0 -0
  17. package/fonts/MerriweatherSans/MerriweatherSans-Medium.woff2 +0 -0
  18. package/fonts/MerriweatherSans/MerriweatherSans-MediumItalic.woff +0 -0
  19. package/fonts/MerriweatherSans/MerriweatherSans-MediumItalic.woff2 +0 -0
  20. package/fonts/MerriweatherSans/MerriweatherSans-Regular.woff +0 -0
  21. package/fonts/MerriweatherSans/MerriweatherSans-Regular.woff2 +0 -0
  22. package/fonts/MerriweatherSans/MerriweatherSans-SemiBold.woff +0 -0
  23. package/fonts/MerriweatherSans/MerriweatherSans-SemiBold.woff2 +0 -0
  24. package/fonts/MerriweatherSans/MerriweatherSans-SemiBoldItalic.woff +0 -0
  25. package/fonts/MerriweatherSans/MerriweatherSans-SemiBoldItalic.woff2 +0 -0
  26. package/fonts/Sofia/Sofia-Regular.woff +0 -0
  27. package/fonts/Sofia/Sofia-Regular.woff2 +0 -0
  28. package/package.json +45 -5
  29. package/src/Brand/Brand.tsx +23 -0
  30. package/src/Brand/Brand.types.ts +6 -0
  31. package/src/Brand/Brand.variants.ts +9 -0
  32. package/src/Brand/HubLogo.tsx +22 -0
  33. package/src/Brand/index.ts +2 -0
  34. package/src/back.ts +52 -0
  35. package/src/buttons/Button/Button.tsx +38 -0
  36. package/src/buttons/Button/Button.types.ts +18 -0
  37. package/src/buttons/Button/Button.variants.ts +36 -0
  38. package/src/buttons/Button/index.ts +2 -0
  39. package/src/buttons/FavButton/FavButton.tsx +29 -0
  40. package/src/buttons/FavButton/FavButton.types.ts +10 -0
  41. package/src/buttons/FavButton/FavButton.variants.ts +12 -0
  42. package/src/buttons/FavButton/index.ts +2 -0
  43. package/src/buttons/IconButton/IconButton.tsx +40 -0
  44. package/src/buttons/IconButton/IconButton.types.ts +23 -0
  45. package/src/buttons/IconButton/IconButton.variants.ts +56 -0
  46. package/src/buttons/IconButton/index.ts +2 -0
  47. package/src/buttons/index.ts +3 -0
  48. package/src/contexts/FormContext.tsx +57 -0
  49. package/src/forms/CheckboxField/Checkbox.tsx +17 -0
  50. package/src/forms/CheckboxField/CheckboxField.tsx +39 -0
  51. package/src/forms/CheckboxField/CheckboxField.types.ts +5 -0
  52. package/src/forms/CheckboxField/CheckboxField.variants.ts +36 -0
  53. package/src/forms/CheckboxField/index.ts +3 -0
  54. package/src/forms/DropdownField/DropdownField.tsx +72 -0
  55. package/src/forms/DropdownField/DropdownField.types.ts +11 -0
  56. package/src/forms/DropdownField/DropdownField.variants.ts +37 -0
  57. package/src/forms/DropdownField/index.ts +2 -0
  58. package/src/forms/Form/Form.tsx +41 -0
  59. package/src/forms/Form/Form.types.ts +11 -0
  60. package/src/forms/Form/Form.variants.ts +8 -0
  61. package/src/forms/Form/index.ts +2 -0
  62. package/src/forms/FormField/FormField.tsx +26 -0
  63. package/src/forms/FormField/FormField.types.ts +10 -0
  64. package/src/forms/FormField/FormField.variants.ts +44 -0
  65. package/src/forms/FormField/index.ts +3 -0
  66. package/src/forms/InputField/InputField.tsx +33 -0
  67. package/src/forms/InputField/InputField.types.ts +14 -0
  68. package/src/forms/InputField/InputField.variants.ts +14 -0
  69. package/src/forms/InputField/index.ts +2 -0
  70. package/src/forms/SpoilerField/SpoilerField.tsx +48 -0
  71. package/src/forms/SpoilerField/SpoilerField.types.ts +8 -0
  72. package/src/forms/SpoilerField/SpoilerField.variants.ts +31 -0
  73. package/src/forms/SpoilerField/index.ts +2 -0
  74. package/src/forms/SwitchField/ChoiceSwitch.tsx +43 -0
  75. package/src/forms/SwitchField/Switch.tsx +18 -0
  76. package/src/forms/SwitchField/SwitchField.tsx +43 -0
  77. package/src/forms/SwitchField/SwitchField.types.ts +5 -0
  78. package/src/forms/SwitchField/SwitchField.variants.ts +62 -0
  79. package/src/forms/SwitchField/index.ts +4 -0
  80. package/src/forms/TagsField/TagsField.tsx +109 -0
  81. package/src/forms/TagsField/TagsField.types.ts +7 -0
  82. package/src/forms/TagsField/TagsField.variants.ts +51 -0
  83. package/src/forms/TagsField/index.ts +2 -0
  84. package/src/forms/TextareaField/TextareaField.tsx +33 -0
  85. package/src/forms/TextareaField/TextareaField.types.ts +9 -0
  86. package/src/forms/TextareaField/TextareaField.variants.ts +14 -0
  87. package/src/forms/TextareaField/index.ts +2 -0
  88. package/src/forms/UploadField/UploadField.tsx +70 -0
  89. package/src/forms/UploadField/UploadField.types.ts +7 -0
  90. package/src/forms/UploadField/UploadField.variants.ts +23 -0
  91. package/src/forms/UploadField/index.ts +2 -0
  92. package/src/forms/index.ts +10 -0
  93. package/src/index.ts +15 -0
  94. package/src/menus/Dropdown/Dropdown.tsx +39 -0
  95. package/src/menus/Dropdown/Dropdown.types.ts +17 -0
  96. package/src/menus/Dropdown/Dropdown.variants.ts +44 -0
  97. package/src/menus/Dropdown/index.ts +2 -0
  98. package/src/menus/index.ts +1 -0
  99. package/src/modals/ConfirmationModal/ConfirmationModal.tsx +44 -0
  100. package/src/modals/ConfirmationModal/ConfirmationModal.types.ts +14 -0
  101. package/src/modals/ConfirmationModal/ConfirmationModal.variants.ts +6 -0
  102. package/src/modals/ConfirmationModal/index.ts +2 -0
  103. package/src/modals/FormModal/FormModal.tsx +46 -0
  104. package/src/modals/FormModal/FormModal.types.ts +14 -0
  105. package/src/modals/FormModal/FormModal.variants.ts +5 -0
  106. package/src/modals/FormModal/index.ts +2 -0
  107. package/src/modals/InfoModal/InfoModal.tsx +21 -0
  108. package/src/modals/InfoModal/InfoModal.types.ts +10 -0
  109. package/src/modals/InfoModal/InfoModal.variants.ts +6 -0
  110. package/src/modals/InfoModal/index.ts +2 -0
  111. package/src/modals/Modal/Modal.tsx +34 -0
  112. package/src/modals/Modal/Modal.types.ts +10 -0
  113. package/src/modals/Modal/Modal.variants.ts +48 -0
  114. package/src/modals/Modal/index.ts +2 -0
  115. package/src/modals/index.ts +4 -0
  116. package/src/navs/BreadCrumb/BreadCrumb.tsx +19 -0
  117. package/src/navs/BreadCrumb/BreadCrumb.types.ts +11 -0
  118. package/src/navs/BreadCrumb/BreadCrumb.variants.ts +7 -0
  119. package/src/navs/BreadCrumb/index.ts +2 -0
  120. package/src/navs/index.ts +1 -0
  121. package/src/tables/Table/Table.tsx +6 -0
  122. package/src/tables/Table/Table.types.ts +6 -0
  123. package/src/tables/Table/Table.variants.ts +5 -0
  124. package/src/tables/Table/index.ts +2 -0
  125. package/src/tables/TableCell/TableCell.tsx +27 -0
  126. package/src/tables/TableCell/TableCell.types.ts +9 -0
  127. package/src/tables/TableCell/TableCell.variants.ts +26 -0
  128. package/src/tables/TableCell/index.ts +2 -0
  129. package/src/tables/TableFootGroup/TableFootGroup.tsx +10 -0
  130. package/src/tables/TableFootGroup/TableFootGroup.variants.ts +6 -0
  131. package/src/tables/TableFootGroup/index.ts +1 -0
  132. package/src/tables/TableHead/TableHead.tsx +22 -0
  133. package/src/tables/TableHead/TableHead.types.ts +9 -0
  134. package/src/tables/TableHead/TableHead.variants.ts +16 -0
  135. package/src/tables/TableHead/index.ts +2 -0
  136. package/src/tables/TableHeadGroup/TableHeadGroup.tsx +10 -0
  137. package/src/tables/TableHeadGroup/TableHeadGroup.variants.ts +6 -0
  138. package/src/tables/TableHeadGroup/index.ts +1 -0
  139. package/src/tables/TableRow/TableRow.tsx +7 -0
  140. package/src/tables/TableRow/TableRow.types.ts +7 -0
  141. package/src/tables/TableRow/TableRow.variants.ts +12 -0
  142. package/src/tables/TableRow/index.ts +2 -0
  143. package/src/tables/TableRowGroup/TableRowGroup.tsx +6 -0
  144. package/src/tables/TableRowGroup/TableRowGroup.variants.ts +5 -0
  145. package/src/tables/TableRowGroup/index.ts +1 -0
  146. package/src/tables/index.ts +7 -0
  147. package/src/utils/index.ts +4 -0
  148. package/src/utils/snakeCase.ts +6 -0
  149. package/src/utils/useClickOutside.ts +16 -0
  150. package/src/utils/useMediaQuery.ts +11 -0
  151. package/src/utils/withLoading.ts +13 -0
  152. package/styles/fonts.css +116 -0
  153. package/styles/theme.css +78 -0
  154. package/README.md +0 -3
@@ -0,0 +1,39 @@
1
+ import { useFormContext } from '../../contexts/FormContext'
2
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3
+ import { faCheck } from '@fortawesome/free-solid-svg-icons'
4
+ import { snakeCase } from '../../utils'
5
+ import { type CheckboxFieldProps } from './CheckboxField.types'
6
+ import { styles } from './CheckboxField.variants'
7
+
8
+ export function CheckboxField({ name, label, required = false }: CheckboxFieldProps) {
9
+ const { data, setField } = useFormContext()
10
+ const id = `field_${snakeCase(name)}`
11
+ const checked = Boolean(data[name])
12
+
13
+ const toggle = () => setField(name, !checked)
14
+
15
+ return (
16
+ <div className={styles.field()} onClick={toggle} aria-pressed={checked} aria-controls={id}>
17
+ <div>
18
+ <button type="button" className={styles.check({ checked })}>
19
+ <FontAwesomeIcon icon={faCheck} />
20
+ </button>
21
+ <input
22
+ id={id}
23
+ name={name}
24
+ type="checkbox"
25
+ checked={checked}
26
+ className="hidden"
27
+ readOnly
28
+ required={required}
29
+ />
30
+ </div>
31
+
32
+ {label && (
33
+ <label htmlFor={id} className={styles.label({ required })}>
34
+ {label}
35
+ </label>
36
+ )}
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,5 @@
1
+ export interface CheckboxFieldProps {
2
+ name: string
3
+ label?: string
4
+ required?: boolean
5
+ }
@@ -0,0 +1,36 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ field: tv({ base: 'flex items-center gap-2 mb-4 cursor-pointer' }),
5
+
6
+ label: tv({
7
+ base: 'text-sm font-normal pointer-events-none',
8
+ variants: {
9
+ required: {
10
+ true: "after:content-['*'] after:pl-[2px] after:text-red-100"
11
+ }
12
+ }
13
+ }),
14
+
15
+ check: tv({
16
+ base: [
17
+ 'size-[14px]',
18
+ 'text-[9px]',
19
+ 'flex',
20
+ 'justify-center',
21
+ 'items-center',
22
+ 'rounded-xs',
23
+ 'border',
24
+ 'cursor-pointer'
25
+ ],
26
+ variants: {
27
+ checked: {
28
+ true: 'bg-primary-700 text-primary-500 border-primary-500',
29
+ false: 'bg-transparent text-transparent border-steel-300'
30
+ }
31
+ },
32
+ defaultVariants: {
33
+ checked: false
34
+ }
35
+ })
36
+ }
@@ -0,0 +1,3 @@
1
+ export { CheckboxField } from './CheckboxField'
2
+ export * from './CheckboxField.types'
3
+ export { Checkbox } from './Checkbox'
@@ -0,0 +1,72 @@
1
+ import { useEffect, useMemo } from 'react'
2
+ import { useFormContext } from '../../contexts/FormContext'
3
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4
+ import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
5
+ import { FormField } from '../FormField'
6
+ import { Dropdown } from '../../menus'
7
+ import { type DropdownFieldProps } from './DropdownField.types'
8
+ import { styles } from './DropdownField.variants'
9
+
10
+ export function DropdownField({ name, label, choices, required = false }: DropdownFieldProps) {
11
+ const { data, setField } = useFormContext()
12
+
13
+ const allChoices = useMemo(
14
+ () => (required ? choices : [{ value: '', label: 'No selection' }, ...choices]),
15
+ [choices, required]
16
+ )
17
+
18
+ useEffect(() => {
19
+ if (data[name] === undefined) {
20
+ setField(name, allChoices[0]?.value ?? '')
21
+ }
22
+ }, [allChoices, data, name, setField])
23
+
24
+ const selected = data[name] as string | undefined
25
+ const selectedLabel = allChoices.find(c => c.value === selected)?.label ?? 'Select an option'
26
+
27
+ const handleSelection = (value: string) => setField(name, value)
28
+
29
+ const dropdownHeader = (isOpen: boolean) => (
30
+ <div className={styles.inputWrapper({ open: isOpen })}>
31
+ <div className={styles.input({ open: isOpen })}>
32
+ <span className={styles.inputText()}>{selectedLabel}</span>
33
+ <FontAwesomeIcon icon={faChevronDown} className={styles.inputIcon()} />
34
+ </div>
35
+ </div>
36
+ )
37
+
38
+ return (
39
+ <FormField name={name} label={label} required={required}>
40
+ {id => (
41
+ <div className={styles.field()}>
42
+ <select
43
+ id={id}
44
+ name={name}
45
+ value={selected ?? ''}
46
+ required={required}
47
+ className={styles.htmlSelect()}
48
+ onChange={e => handleSelection(e.target.value)}
49
+ >
50
+ {allChoices.map(c => (
51
+ <option key={c.value} value={c.value}>{c.label}</option>
52
+ ))}
53
+ </select>
54
+
55
+ <Dropdown side="center" color="field" header={dropdownHeader}>
56
+ <ul className={styles.choices()}>
57
+ {allChoices.map(c => (
58
+ <li
59
+ key={c.value}
60
+ className={styles.choice()}
61
+ onClick={() => handleSelection(c.value)}
62
+ >
63
+ {c.label}
64
+ </li>
65
+ ))}
66
+ </ul>
67
+ </Dropdown>
68
+ </div>
69
+ )}
70
+ </FormField>
71
+ )
72
+ }
@@ -0,0 +1,11 @@
1
+ export interface DropdownChoice {
2
+ value: string
3
+ label: string
4
+ }
5
+
6
+ export interface DropdownFieldProps {
7
+ name: string
8
+ label?: string
9
+ choices: DropdownChoice[]
10
+ required?: boolean
11
+ }
@@ -0,0 +1,37 @@
1
+ import { styles as fieldStyles } from '../FormField'
2
+ import { tv } from 'tailwind-variants'
3
+
4
+ export const styles = {
5
+ field: tv({ base: 'relative w-full pb-1' }),
6
+
7
+ htmlSelect: tv({ base: 'hidden absolute w-full' }),
8
+
9
+ inputWrapper: tv({
10
+ base: ['w-full', 'rounded-xs', 'border', 'border-transparent'],
11
+ variants: {
12
+ open: { true: 'border-primary-300' }
13
+ }
14
+ }),
15
+
16
+ input: tv({
17
+ extend: fieldStyles.input,
18
+ base: [
19
+ 'relative',
20
+ 'w-full',
21
+ 'flex',
22
+ 'items-center',
23
+ 'h-11',
24
+ 'bg-steel-800',
25
+ 'cursor-pointer'
26
+ ],
27
+ variants: {
28
+ open: { true: 'border-primary-300' }
29
+ }
30
+ }),
31
+
32
+ inputText: tv({ base: 'flex-1' }),
33
+ inputIcon: tv({ base: 'mr-1' }),
34
+
35
+ choices: tv({ base: 'py-1 my-2' }),
36
+ choice: tv({ base: 'py-1 px-3 text-sm hover:bg-steel-700 cursor-pointer' })
37
+ }
@@ -0,0 +1,2 @@
1
+ export { DropdownField } from './DropdownField'
2
+ export * from './DropdownField.types'
@@ -0,0 +1,41 @@
1
+ import { useState, type FormEvent } from 'react'
2
+ import { FormContextProvider, useFormContext } from '../../contexts/FormContext'
3
+ import { withLoading } from '../../utils'
4
+ import { type FormProps } from './Form.types'
5
+ import { styles } from './Form.variants'
6
+
7
+ export function Form({ initialData, children, ...props }: FormProps) {
8
+ return (
9
+ <FormContextProvider initialData={initialData}>
10
+ <InnerForm {...props}>{children}</InnerForm>
11
+ </FormContextProvider>
12
+ )
13
+ }
14
+
15
+ type InnerProps = Omit<FormProps, 'initialData'>
16
+
17
+ function InnerForm({ title, actions, onSubmit, children, className }: InnerProps) {
18
+ const { data } = useFormContext()
19
+ const [, setLoading] = useState(false)
20
+
21
+ const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
22
+ e.preventDefault()
23
+ await withLoading(setLoading, () => Promise.resolve(onSubmit(data)))
24
+ }
25
+
26
+ return (
27
+ <form onSubmit={handleSubmit} className={className}>
28
+ {title && (
29
+ <header className={styles.header()}>
30
+ <h1 className={styles.title()}>{title}</h1>
31
+ </header>
32
+ )}
33
+
34
+ <div className={styles.content()}>{children}</div>
35
+
36
+ {actions && (
37
+ <footer className={styles.footer()}>{actions}</footer>
38
+ )}
39
+ </form>
40
+ )
41
+ }
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from 'react'
2
+ import { type FormData } from '../../contexts/FormContext'
3
+
4
+ export interface FormProps {
5
+ initialData?: FormData
6
+ title?: string
7
+ actions?: ReactNode
8
+ onSubmit: (data: FormData) => void | Promise<void>
9
+ children: ReactNode
10
+ className?: string
11
+ }
@@ -0,0 +1,8 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ header: tv({ base: 'pb-6' }),
5
+ title: tv({ base: 'text-xl font-medium' }),
6
+ footer: tv({ base: 'flex justify-end items-center gap-2' }),
7
+ content: tv({ base: 'mb-2' })
8
+ }
@@ -0,0 +1,2 @@
1
+ export { Form } from './Form'
2
+ export * from './Form.types'
@@ -0,0 +1,26 @@
1
+ import { useFormContext } from '../../contexts/FormContext'
2
+ import { snakeCase } from '../../utils'
3
+ import { type FormFieldProps } from './FormField.types'
4
+ import { styles } from './FormField.variants'
5
+
6
+ export function FormField({ name, label, required = false, children }: FormFieldProps) {
7
+ const { errors } = useFormContext()
8
+ const id = `field_${snakeCase(name)}`
9
+ const hasError = Boolean(errors[name])
10
+
11
+ return (
12
+ <div className={styles.field()}>
13
+ {label && (
14
+ <label htmlFor={id} className={styles.label({ required })}>
15
+ {label}
16
+ </label>
17
+ )}
18
+
19
+ <div className={styles.inputWrapper()}>
20
+ {children(id, hasError)}
21
+ </div>
22
+
23
+ {hasError && <p className={styles.error()}>{errors[name]}</p>}
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ export type FormFieldRenderer = (id: string, hasError: boolean) => ReactNode
4
+
5
+ export interface FormFieldProps {
6
+ name: string
7
+ label?: string
8
+ required?: boolean
9
+ children: FormFieldRenderer
10
+ }
@@ -0,0 +1,44 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ field: tv({ base: 'flex flex-col mb-4' }),
5
+
6
+ label: tv({
7
+ base: 'text-xs font-semibold mb-1',
8
+ variants: {
9
+ required: {
10
+ true: "after:content-['*'] after:pl-[2px] after:text-red-100"
11
+ }
12
+ }
13
+ }),
14
+
15
+ inputWrapper: tv({
16
+ base: [
17
+ 'w-full',
18
+ 'rounded-xs',
19
+ 'border',
20
+ 'border-transparent',
21
+ 'focus-within:border-primary-300'
22
+ ]
23
+ }),
24
+
25
+ input: tv({
26
+ base: [
27
+ 'w-full',
28
+ 'min-w-3xs',
29
+ 'py-2',
30
+ 'px-[6px]',
31
+ 'rounded-xs',
32
+ 'border',
33
+ 'border-steel-300',
34
+ 'focus:border-primary-300',
35
+ 'outline-none',
36
+ 'text-sm',
37
+ 'bg-transparent',
38
+ 'text-white',
39
+ 'placeholder:text-steel-300'
40
+ ]
41
+ }),
42
+
43
+ error: tv({ base: 'pt-1 text-red-500 text-xs' })
44
+ }
@@ -0,0 +1,3 @@
1
+ export { FormField } from './FormField'
2
+ export * from './FormField.types'
3
+ export { styles } from './FormField.variants'
@@ -0,0 +1,33 @@
1
+ import { useFormContext } from '../../contexts/FormContext'
2
+ import { FormField } from '../FormField'
3
+ import { type InputFieldProps } from './InputField.types'
4
+ import { styles } from './InputField.variants'
5
+
6
+ export function InputField({
7
+ name,
8
+ label,
9
+ type = 'text',
10
+ required = false,
11
+ placeholder = '',
12
+ ...rest
13
+ }: InputFieldProps) {
14
+ const { data, setField } = useFormContext()
15
+
16
+ return (
17
+ <FormField name={name} label={label} required={required}>
18
+ {(id, hasError) => (
19
+ <input
20
+ id={id}
21
+ name={name}
22
+ type={type}
23
+ value={(data[name] as string | number | undefined) ?? ''}
24
+ placeholder={placeholder}
25
+ className={styles.input({ error: hasError })}
26
+ onChange={e => setField(name, e.target.value)}
27
+ required={required}
28
+ {...rest}
29
+ />
30
+ )}
31
+ </FormField>
32
+ )
33
+ }
@@ -0,0 +1,14 @@
1
+ import { type HTMLInputTypeAttribute } from 'react'
2
+
3
+ export interface InputFieldProps {
4
+ name: string
5
+ label?: string
6
+ type?: HTMLInputTypeAttribute
7
+ required?: boolean
8
+ placeholder?: string
9
+ autoComplete?: string
10
+ autoFocus?: boolean
11
+ min?: number | string
12
+ max?: number | string
13
+ step?: number | string
14
+ }
@@ -0,0 +1,14 @@
1
+ import { styles as fieldStyles } from '../FormField'
2
+ import { tv } from 'tailwind-variants'
3
+
4
+ export const styles = {
5
+ input: tv({
6
+ extend: fieldStyles.input,
7
+ base: 'h-11',
8
+ variants: {
9
+ error: {
10
+ true: 'border-red-500!'
11
+ }
12
+ }
13
+ })
14
+ }
@@ -0,0 +1,2 @@
1
+ export { InputField } from './InputField'
2
+ export * from './InputField.types'
@@ -0,0 +1,48 @@
1
+ import { useState } from 'react'
2
+ import { useFormContext } from '../../contexts/FormContext'
3
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4
+ import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
5
+ import { FormField } from '../FormField'
6
+ import { type SpoilerFieldProps } from './SpoilerField.types'
7
+ import { styles } from './SpoilerField.variants'
8
+
9
+ export function SpoilerField({
10
+ name,
11
+ label,
12
+ required = false,
13
+ placeholder = '',
14
+ ...rest
15
+ }: SpoilerFieldProps) {
16
+ const { data, setField } = useFormContext()
17
+ const [revealed, setRevealed] = useState(false)
18
+
19
+ return (
20
+ <FormField name={name} label={label} required={required}>
21
+ {(id, hasError) => (
22
+ <div className={styles.field()}>
23
+ <input
24
+ id={id}
25
+ name={name}
26
+ type={revealed ? 'text' : 'password'}
27
+ value={(data[name] as string | undefined) ?? ''}
28
+ placeholder={placeholder}
29
+ className={styles.input({ error: hasError })}
30
+ onChange={e => setField(name, e.target.value)}
31
+ required={required}
32
+ {...rest}
33
+ />
34
+
35
+ <button
36
+ type="button"
37
+ onClick={() => setRevealed(r => !r)}
38
+ className={styles.revealer()}
39
+ tabIndex={-1}
40
+ aria-label={revealed ? 'Hide value' : 'Reveal value'}
41
+ >
42
+ <FontAwesomeIcon icon={revealed ? faEyeSlash : faEye} />
43
+ </button>
44
+ </div>
45
+ )}
46
+ </FormField>
47
+ )
48
+ }
@@ -0,0 +1,8 @@
1
+ export interface SpoilerFieldProps {
2
+ name: string
3
+ label?: string
4
+ required?: boolean
5
+ placeholder?: string
6
+ autoComplete?: string
7
+ autoFocus?: boolean
8
+ }
@@ -0,0 +1,31 @@
1
+ import { styles as fieldStyles } from '../FormField'
2
+ import { tv } from 'tailwind-variants'
3
+
4
+ export const styles = {
5
+ field: tv({ base: 'relative' }),
6
+
7
+ input: tv({
8
+ extend: fieldStyles.input,
9
+ base: 'h-11 pr-8',
10
+ variants: {
11
+ error: {
12
+ true: 'border-red-500!'
13
+ }
14
+ }
15
+ }),
16
+
17
+ revealer: tv({
18
+ base: [
19
+ 'absolute',
20
+ 'right-2',
21
+ 'top-1/2',
22
+ '-translate-y-1/2',
23
+ 'text-steel-300',
24
+ 'hover:text-primary-500',
25
+ 'focus:outline-none',
26
+ 'cursor-pointer',
27
+ 'bg-transparent',
28
+ 'border-0'
29
+ ]
30
+ })
31
+ }
@@ -0,0 +1,2 @@
1
+ export { SpoilerField } from './SpoilerField'
2
+ export * from './SpoilerField.types'
@@ -0,0 +1,43 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ /**
4
+ * A switch between two choices (this || that, not on/off): both labels sit in the track, the
5
+ * thumb slides under the picked one. SwitchField's look, blue on both sides.
6
+ */
7
+ export function ChoiceSwitch<V extends string>({ value, options, label, onChange }: {
8
+ value: V
9
+ /** `count`: a number shown after the label (e.g. results waiting there) */
10
+ options: [{ value: V, label: string, icon?: ReactNode, count?: number }, { value: V, label: string, icon?: ReactNode, count?: number }]
11
+ /** said to screen readers */
12
+ label: string
13
+ onChange: (value: V) => void
14
+ }) {
15
+ const second = value === options[1].value
16
+ return (
17
+ <div role="radiogroup" aria-label={label} className="relative inline-grid grid-cols-2 h-8 p-0.5 border border-steel-300 rounded-xs">
18
+ <span
19
+ aria-hidden
20
+ className={`absolute top-0.5 bottom-0.5 left-0.5 w-[calc(50%-2px)] rounded-xs bg-primary-500 transition-transform duration-200 ${second ? 'translate-x-full' : 'translate-x-0'}`}
21
+ />
22
+ {options.map(o => {
23
+ const picked = o.value === value
24
+ return (
25
+ <button
26
+ key={o.value}
27
+ type="button"
28
+ role="radio"
29
+ aria-checked={picked}
30
+ onClick={() => onChange(o.value)}
31
+ className={`relative z-10 flex items-center justify-center gap-1.5 px-3 text-xs font-semibold bg-transparent border-0 cursor-pointer transition-colors duration-200 ${picked ? 'text-steel-950' : 'text-steel-100 hover:text-white'}`}
32
+ >
33
+ {o.icon}
34
+ {o.label}
35
+ {o.count !== undefined && (
36
+ <span className={`min-w-5 h-4 px-1 rounded-full text-[10px] leading-4 tabular-nums ${picked ? 'bg-steel-950/25 text-steel-950' : 'bg-steel-800 text-steel-100'}`}>{o.count}</span>
37
+ )}
38
+ </button>
39
+ )
40
+ })}
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,18 @@
1
+ import { styles } from './SwitchField.variants'
2
+
3
+ /** The SwitchField switch on its own, outside a form: controlled by `checked`. */
4
+ export function Switch({ checked, onChange, title }: { checked: boolean, onChange: (checked: boolean) => void, title?: string }) {
5
+ return (
6
+ <button
7
+ type="button"
8
+ role="switch"
9
+ aria-checked={checked}
10
+ aria-label={title}
11
+ title={title}
12
+ onClick={() => onChange(!checked)}
13
+ className={`${styles.switch({ checked })} shrink-0 p-0 bg-clip-padding`}
14
+ >
15
+ <span className={styles.thumb({ checked })} />
16
+ </button>
17
+ )
18
+ }
@@ -0,0 +1,43 @@
1
+ import { useFormContext } from '../../contexts/FormContext'
2
+ import { snakeCase } from '../../utils'
3
+ import { type SwitchFieldProps } from './SwitchField.types'
4
+ import { styles } from './SwitchField.variants'
5
+
6
+ export function SwitchField({ name, label, required = false }: SwitchFieldProps) {
7
+ const { data, setField } = useFormContext()
8
+ const id = `field_${snakeCase(name)}`
9
+ const checked = Boolean(data[name])
10
+
11
+ const toggle = () => setField(name, !checked)
12
+
13
+ return (
14
+ <div className={styles.field()} onClick={toggle} aria-pressed={checked} aria-controls={id}>
15
+ {label && (
16
+ <label htmlFor={id} className={styles.label({ required })}>
17
+ {label}
18
+ </label>
19
+ )}
20
+
21
+ <div>
22
+ <button
23
+ type="button"
24
+ className={styles.switch({ checked })}
25
+ role="switch"
26
+ aria-checked={checked}
27
+ >
28
+ <span className={styles.thumb({ checked })} />
29
+ </button>
30
+
31
+ <input
32
+ id={id}
33
+ name={name}
34
+ type="checkbox"
35
+ checked={checked}
36
+ className="hidden"
37
+ readOnly
38
+ required={required}
39
+ />
40
+ </div>
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,5 @@
1
+ export interface SwitchFieldProps {
2
+ name: string
3
+ label?: string
4
+ required?: boolean
5
+ }