@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,62 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ field: tv({
5
+ base: [
6
+ 'w-full',
7
+ 'h-11',
8
+ 'flex',
9
+ 'items-center',
10
+ 'justify-between',
11
+ 'cursor-pointer',
12
+ 'select-none'
13
+ ]
14
+ }),
15
+
16
+ switch: tv({
17
+ base: [
18
+ 'relative',
19
+ 'w-10',
20
+ 'h-5',
21
+ 'border',
22
+ 'rounded-xs',
23
+ 'transition-colors',
24
+ 'duration-200',
25
+ 'cursor-pointer'
26
+ ],
27
+ variants: {
28
+ checked: {
29
+ true: 'border-steel-300 bg-primary-700',
30
+ false: 'border-steel-300 bg-transparent'
31
+ }
32
+ }
33
+ }),
34
+
35
+ thumb: tv({
36
+ base: [
37
+ 'absolute',
38
+ 'top-0.5',
39
+ 'left-0.5',
40
+ 'size-[14px]',
41
+ 'rounded-xs',
42
+ 'transition-transform',
43
+ 'duration-200',
44
+ 'cursor-pointer'
45
+ ],
46
+ variants: {
47
+ checked: {
48
+ true: 'translate-x-5 bg-primary-500',
49
+ false: 'translate-x-0 bg-steel-300'
50
+ }
51
+ }
52
+ }),
53
+
54
+ label: tv({
55
+ base: 'text-sm',
56
+ variants: {
57
+ required: {
58
+ true: 'after:content-["*"] after:ml-1 after:text-red-500'
59
+ }
60
+ }
61
+ })
62
+ }
@@ -0,0 +1,4 @@
1
+ export { SwitchField } from './SwitchField'
2
+ export * from './SwitchField.types'
3
+ export { ChoiceSwitch } from './ChoiceSwitch'
4
+ export { Switch } from './Switch'
@@ -0,0 +1,109 @@
1
+ import { useRef, useState, type KeyboardEvent, type ChangeEvent } from 'react'
2
+ import { useFormContext } from '../../contexts/FormContext'
3
+ import { faXmark } from '@fortawesome/free-solid-svg-icons'
4
+ import { IconButton } from '../../buttons'
5
+ import { FormField } from '../FormField'
6
+ import { type TagsFieldProps } from './TagsField.types'
7
+ import { styles } from './TagsField.variants'
8
+
9
+ export function TagsField({
10
+ name,
11
+ label,
12
+ tagLimit = 10,
13
+ required = false,
14
+ placeholder = ''
15
+ }: TagsFieldProps) {
16
+ const { data, setField } = useFormContext()
17
+ const [limitWarning, setLimitWarning] = useState<string | null>(null)
18
+ const [input, setInput] = useState('')
19
+ const inputRef = useRef<HTMLInputElement>(null)
20
+
21
+ const tags = Array.isArray(data[name]) ? (data[name] as string[]) : []
22
+ const isFull = Boolean(limitWarning)
23
+
24
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
25
+ if (e.key === ' ' && input.length === 0) {
26
+ e.preventDefault()
27
+ return
28
+ }
29
+ if ((e.key === ' ' || e.key === 'Enter') && input.trim() !== '') {
30
+ e.preventDefault()
31
+ const next = input.trim()
32
+
33
+ if (tags.length >= tagLimit) {
34
+ if (!limitWarning) setLimitWarning(`Tag limit reached. A maximum of ${tagLimit} tags is allowed.`)
35
+ return
36
+ }
37
+ if (limitWarning) setLimitWarning(null)
38
+
39
+ if (!tags.includes(next)) {
40
+ setField(name, [...tags, next])
41
+ }
42
+ setInput('')
43
+ }
44
+ }
45
+
46
+ const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
47
+ const sanitized = e.target.value
48
+ .toLowerCase()
49
+ .replace(/_/g, '-')
50
+ .replace(/[^a-z0-9-]/g, '')
51
+ setInput(sanitized)
52
+ }
53
+
54
+ const handleRemove = (index: number) => {
55
+ const next = [...tags]
56
+ next.splice(index, 1)
57
+ setField(name, next)
58
+ if (limitWarning) setLimitWarning(null)
59
+ }
60
+
61
+ const handleBlur = () => {
62
+ if (input.trim() !== '') setInput('')
63
+ }
64
+
65
+ const focusInput = () => inputRef.current?.focus()
66
+
67
+ return (
68
+ <>
69
+ <FormField name={name} label={label} required={required}>
70
+ {id => (
71
+ <ul className={styles.tags({ isFull })} onClick={focusInput}>
72
+ {tags.map((tag, i) => (
73
+ <li key={i} className={styles.tag({ persisted: true })}>
74
+ <span className={styles.tagText()} style={{ width: `${tag.length + 0.5}ch` }}>
75
+ {tag}
76
+ </span>
77
+ <IconButton
78
+ type="button"
79
+ icon={faXmark}
80
+ color="secondary"
81
+ size="xxs"
82
+ onClick={() => handleRemove(i)}
83
+ aria-label={`Remove ${tag}`}
84
+ />
85
+ </li>
86
+ ))}
87
+ <li className={styles.tag({ persisted: false })}>
88
+ <input
89
+ ref={inputRef}
90
+ id={id}
91
+ type="text"
92
+ className={styles.input({ isFull })}
93
+ maxLength={24}
94
+ value={input}
95
+ onChange={handleChange}
96
+ onBlur={handleBlur}
97
+ onKeyDown={handleKeyDown}
98
+ placeholder={placeholder}
99
+ required={required && tags.length === 0}
100
+ />
101
+ </li>
102
+ </ul>
103
+ )}
104
+ </FormField>
105
+
106
+ {limitWarning && <p className={styles.tagLimit()}>{limitWarning}</p>}
107
+ </>
108
+ )
109
+ }
@@ -0,0 +1,7 @@
1
+ export interface TagsFieldProps {
2
+ name: string
3
+ label?: string
4
+ tagLimit?: number
5
+ required?: boolean
6
+ placeholder?: string
7
+ }
@@ -0,0 +1,51 @@
1
+ import { styles as fieldStyles } from '../FormField'
2
+ import { tv } from 'tailwind-variants'
3
+
4
+ export const styles = {
5
+ tags: tv({
6
+ extend: fieldStyles.input,
7
+ base: [
8
+ 'min-h-11',
9
+ 'w-full',
10
+ 'flex',
11
+ 'flex-wrap',
12
+ 'gap-y-2',
13
+ 'items-center',
14
+ 'focus-within:border-primary-300'
15
+ ],
16
+ variants: {
17
+ isFull: { true: 'cursor-not-allowed' }
18
+ }
19
+ }),
20
+
21
+ tag: tv({
22
+ base: 'h-full flex items-center font-mono',
23
+ variants: {
24
+ persisted: {
25
+ true: [
26
+ 'flex-[initial]',
27
+ 'mr-2',
28
+ 'border',
29
+ 'border-steel-700',
30
+ 'rounded-sm',
31
+ 'p-[2px]',
32
+ 'pl-2',
33
+ 'leading-none'
34
+ ],
35
+ false: 'flex-1'
36
+ }
37
+ },
38
+ defaultVariants: { persisted: false }
39
+ }),
40
+
41
+ tagText: tv({ base: 'text-xs leading-[115%]' }),
42
+
43
+ input: tv({
44
+ base: 'outline-none border-0 w-auto grow shrink bg-transparent text-white text-sm',
45
+ variants: {
46
+ isFull: { true: 'cursor-not-allowed' }
47
+ }
48
+ }),
49
+
50
+ tagLimit: tv({ base: '-mt-4 pt-1 text-xs text-red-300' })
51
+ }
@@ -0,0 +1,2 @@
1
+ export { TagsField } from './TagsField'
2
+ export * from './TagsField.types'
@@ -0,0 +1,33 @@
1
+ import { useFormContext } from '../../contexts/FormContext'
2
+ import { FormField } from '../FormField'
3
+ import { type TextareaFieldProps } from './TextareaField.types'
4
+ import { styles } from './TextareaField.variants'
5
+
6
+ export function TextareaField({
7
+ name,
8
+ label,
9
+ rows = 4,
10
+ required = false,
11
+ placeholder = '',
12
+ ...rest
13
+ }: TextareaFieldProps) {
14
+ const { data, setField } = useFormContext()
15
+
16
+ return (
17
+ <FormField name={name} label={label} required={required}>
18
+ {(id, hasError) => (
19
+ <textarea
20
+ id={id}
21
+ name={name}
22
+ rows={rows}
23
+ value={(data[name] as string | 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,9 @@
1
+ export interface TextareaFieldProps {
2
+ name: string
3
+ label?: string
4
+ rows?: number
5
+ required?: boolean
6
+ placeholder?: string
7
+ autoComplete?: string
8
+ autoFocus?: boolean
9
+ }
@@ -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: 'min-h-11 resize-y',
8
+ variants: {
9
+ error: {
10
+ true: 'border-red-500!'
11
+ }
12
+ }
13
+ })
14
+ }
@@ -0,0 +1,2 @@
1
+ export { TextareaField } from './TextareaField'
2
+ export * from './TextareaField.types'
@@ -0,0 +1,70 @@
1
+ import { useRef, useState, type ChangeEvent, type DragEvent } from 'react'
2
+ import { useFormContext } from '../../contexts/FormContext'
3
+ import { FormField } from '../FormField'
4
+ import { type UploadFieldProps } from './UploadField.types'
5
+ import { styles } from './UploadField.variants'
6
+
7
+ export function UploadField({
8
+ name,
9
+ label,
10
+ required = false,
11
+ multiple = false,
12
+ accept = 'image/*'
13
+ }: UploadFieldProps) {
14
+ const { data, setField } = useFormContext()
15
+ const inputRef = useRef<HTMLInputElement>(null)
16
+ const [dragActive, setDragActive] = useState(false)
17
+
18
+ const value = data[name]
19
+ const files: File[] = value
20
+ ? (multiple ? (value as File[]) : [value as File])
21
+ : []
22
+
23
+ const apply = (incoming: FileList | File[]) => {
24
+ const arr = Array.from(incoming)
25
+ if (arr.length === 0) return
26
+ setField(name, multiple ? arr : arr[0])
27
+ }
28
+
29
+ const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
30
+ if (e.target.files) apply(e.target.files)
31
+ }
32
+
33
+ const handleDrop = (e: DragEvent<HTMLDivElement>) => {
34
+ e.preventDefault()
35
+ setDragActive(false)
36
+ if (e.dataTransfer.files) apply(e.dataTransfer.files)
37
+ }
38
+
39
+ return (
40
+ <FormField name={name} label={label} required={required}>
41
+ {(id, hasError) => (
42
+ <div
43
+ id={id}
44
+ className={styles.dropzone({ dragActive, error: hasError })}
45
+ onClick={() => inputRef.current?.click()}
46
+ onDragOver={e => { e.preventDefault(); setDragActive(true) }}
47
+ onDragLeave={() => setDragActive(false)}
48
+ onDrop={handleDrop}
49
+ >
50
+ <input
51
+ ref={inputRef}
52
+ type="file"
53
+ name={name}
54
+ accept={accept}
55
+ multiple={multiple}
56
+ required={required && files.length === 0}
57
+ className="hidden"
58
+ onChange={handleChange}
59
+ />
60
+ <p>{dragActive ? 'Drop the file here…' : 'Drag a file or click to select'}</p>
61
+ {files.length > 0 && (
62
+ <ul className={styles.previews()}>
63
+ {files.map((f, i) => <li key={i}>{f.name}</li>)}
64
+ </ul>
65
+ )}
66
+ </div>
67
+ )}
68
+ </FormField>
69
+ )
70
+ }
@@ -0,0 +1,7 @@
1
+ export interface UploadFieldProps {
2
+ name: string
3
+ label?: string
4
+ required?: boolean
5
+ multiple?: boolean
6
+ accept?: string
7
+ }
@@ -0,0 +1,23 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ dropzone: tv({
5
+ base: [
6
+ 'flex flex-col items-center justify-center gap-2',
7
+ 'min-h-24',
8
+ 'p-4',
9
+ 'border-2 border-dashed border-steel-700',
10
+ 'rounded-xs',
11
+ 'cursor-pointer',
12
+ 'transition',
13
+ 'text-sm text-steel-300',
14
+ 'hover:border-primary-500 hover:text-primary-500'
15
+ ],
16
+ variants: {
17
+ dragActive: { true: 'border-primary-500! text-primary-500!' },
18
+ error: { true: 'border-red-500!' }
19
+ }
20
+ }),
21
+
22
+ previews: tv({ base: 'mt-3 text-xs text-steel-100 list-disc list-inside' })
23
+ }
@@ -0,0 +1,2 @@
1
+ export { UploadField } from './UploadField'
2
+ export * from './UploadField.types'
@@ -0,0 +1,10 @@
1
+ export * from './Form'
2
+ export * from './FormField'
3
+ export * from './InputField'
4
+ export * from './TextareaField'
5
+ export * from './SwitchField'
6
+ export * from './CheckboxField'
7
+ export * from './SpoilerField'
8
+ export * from './DropdownField'
9
+ export * from './TagsField'
10
+ export * from './UploadField'
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @cxxi-lab/hub-ui — hub's design system: the components, the design tokens (theme.css), the fonts
3
+ * (fonts.css) and the few helpers the components rely on (the back-button stack…).
4
+ */
5
+ export * from './buttons'
6
+ export * from './forms'
7
+ export * from './menus'
8
+ export * from './modals'
9
+ export * from './navs'
10
+ export * from './tables'
11
+ export * from './Brand'
12
+ export type { FormData, FormErrors, FormContextValue } from './contexts/FormContext'
13
+ export { useFormContext } from './contexts/FormContext'
14
+ export { useBackHandler, handleBack, onBackDepth } from './back'
15
+ export * from './utils'
@@ -0,0 +1,39 @@
1
+ import { useRef, useState } from 'react'
2
+ import { useClickOutside } from '../../utils/useClickOutside'
3
+ import { useBackHandler } from '../../back'
4
+ import { type DropdownProps } from './Dropdown.types'
5
+ import { styles } from './Dropdown.variants'
6
+
7
+ export function Dropdown({
8
+ header,
9
+ children,
10
+ color,
11
+ side,
12
+ isOpen: controlledOpen,
13
+ onOpenChange,
14
+ className
15
+ }: DropdownProps) {
16
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(false)
17
+ const isControlled = controlledOpen !== undefined
18
+ const open = isControlled ? controlledOpen : uncontrolledOpen
19
+
20
+ const setOpen = (next: boolean) => {
21
+ if (!isControlled) setUncontrolledOpen(next)
22
+ onOpenChange?.(next)
23
+ }
24
+
25
+ const containerRef = useRef<HTMLDivElement>(null)
26
+ const menuRef = useRef<HTMLDivElement>(null)
27
+
28
+ useClickOutside([containerRef, menuRef], () => setOpen(false))
29
+ useBackHandler(open, () => setOpen(false))
30
+
31
+ return (
32
+ <div ref={containerRef} className={styles.container({ class: className })} onClick={() => setOpen(!open)}>
33
+ {header && <div>{typeof header === 'function' ? header(open) : header}</div>}
34
+ <div ref={menuRef} className={styles.menu({ open, color, side })}>
35
+ <div>{children}</div>
36
+ </div>
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ export type DropdownColor = 'menu' | 'field'
4
+ export type DropdownSide = 'left' | 'center' | 'right'
5
+
6
+ export type DropdownHeader = ReactNode | ((isOpen: boolean) => ReactNode)
7
+
8
+ export interface DropdownProps {
9
+ header?: DropdownHeader
10
+ children: ReactNode
11
+ color?: DropdownColor
12
+ side?: DropdownSide
13
+ isOpen?: boolean
14
+ onOpenChange?: (open: boolean) => void
15
+ /** extra classes on the root (e.g. to size it inside a flex row) */
16
+ className?: string
17
+ }
@@ -0,0 +1,44 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ container: tv({ base: 'relative' }),
5
+
6
+ menu: tv({
7
+ base: [
8
+ 'absolute',
9
+ 'w-fit',
10
+ 'rounded-xs',
11
+ 'border',
12
+ 'origin-top',
13
+ 'transition-transform',
14
+ 'transition-opacity',
15
+ 'z-50'
16
+ ],
17
+
18
+ variants: {
19
+ color: {
20
+ menu: 'bg-steel-800 border-steel-700 shadow-out',
21
+ field: 'bg-steel-800 border-steel-300 shadow'
22
+ },
23
+ side: {
24
+ left: 'start-0',
25
+ center: 'start-0 w-full end-0',
26
+ right: 'end-0'
27
+ },
28
+ open: {
29
+ true: 'scale-y-100 opacity-100 pointer-events-auto',
30
+ false: 'scale-y-0 opacity-0 pointer-events-none'
31
+ }
32
+ },
33
+
34
+ defaultVariants: {
35
+ color: 'menu',
36
+ side: 'left'
37
+ },
38
+
39
+ compoundVariants: [
40
+ { side: ['left', 'right'], color: 'menu', class: 'top-[calc(100%+8px)]' },
41
+ { side: 'center', color: 'field', class: 'top-12' }
42
+ ]
43
+ })
44
+ }
@@ -0,0 +1,2 @@
1
+ export { Dropdown } from './Dropdown'
2
+ export * from './Dropdown.types'
@@ -0,0 +1 @@
1
+ export * from './Dropdown'
@@ -0,0 +1,44 @@
1
+ import { useState } from 'react'
2
+ import { withLoading } from '../../utils'
3
+ import { Button } from '../../buttons'
4
+ import { Modal } from '../Modal'
5
+ import { type ConfirmationModalProps } from './ConfirmationModal.types'
6
+ import { styles } from './ConfirmationModal.variants'
7
+
8
+ export function ConfirmationModal({
9
+ title,
10
+ size,
11
+ confirmLabel = 'Confirm',
12
+ confirmColor,
13
+ cancelLabel = 'Cancel',
14
+ onConfirm,
15
+ onClose,
16
+ children
17
+ }: ConfirmationModalProps) {
18
+ const [loading, setLoading] = useState(false)
19
+
20
+ const handleConfirm = async () => {
21
+ await withLoading(setLoading, () => Promise.resolve(onConfirm()))
22
+ onClose()
23
+ }
24
+
25
+ return (
26
+ <Modal title={title} size={size} onClose={onClose}>
27
+ <div className={styles.content()}>{children}</div>
28
+ <footer className={styles.footer()}>
29
+ <Button
30
+ label={cancelLabel}
31
+ color="secondary"
32
+ onClick={onClose}
33
+ isDisabled={loading}
34
+ />
35
+ <Button
36
+ label={confirmLabel}
37
+ color={confirmColor}
38
+ onClick={handleConfirm}
39
+ isLoading={loading}
40
+ />
41
+ </footer>
42
+ </Modal>
43
+ )
44
+ }
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from 'react'
2
+ import { type ButtonColor } from '../../buttons/Button/Button.types'
3
+ import { type ModalSize } from '../Modal/Modal.types'
4
+
5
+ export interface ConfirmationModalProps {
6
+ title: ReactNode
7
+ size?: ModalSize
8
+ confirmLabel?: string
9
+ confirmColor?: ButtonColor
10
+ cancelLabel?: string
11
+ onConfirm: () => void | Promise<void>
12
+ onClose: () => void
13
+ children: ReactNode
14
+ }
@@ -0,0 +1,6 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ content: tv({ base: 'mb-2' }),
5
+ footer: tv({ base: 'pt-6 flex justify-end items-center gap-2' })
6
+ }
@@ -0,0 +1,2 @@
1
+ export { ConfirmationModal } from './ConfirmationModal'
2
+ export * from './ConfirmationModal.types'
@@ -0,0 +1,46 @@
1
+ import { useState } from 'react'
2
+ import { withLoading } from '../../utils'
3
+ import { type FormData } from '../../contexts/FormContext'
4
+ import { Button } from '../../buttons'
5
+ import { Form } from '../../forms'
6
+ import { Modal } from '../Modal'
7
+ import { type FormModalProps } from './FormModal.types'
8
+
9
+ export function FormModal({
10
+ title,
11
+ size,
12
+ initialData,
13
+ submitLabel = 'Submit',
14
+ cancelLabel = 'Cancel',
15
+ onSubmit,
16
+ onClose,
17
+ children
18
+ }: FormModalProps) {
19
+ const [loading, setLoading] = useState(false)
20
+
21
+ const handleSubmit = async (data: FormData) => {
22
+ await withLoading(setLoading, () => Promise.resolve(onSubmit(data)))
23
+ onClose()
24
+ }
25
+
26
+ const actions = (
27
+ <>
28
+ <Button
29
+ type="button"
30
+ color="secondary"
31
+ label={cancelLabel}
32
+ onClick={onClose}
33
+ isDisabled={loading}
34
+ />
35
+ <Button type="submit" label={submitLabel} isLoading={loading} />
36
+ </>
37
+ )
38
+
39
+ return (
40
+ <Modal title={title} size={size} onClose={onClose}>
41
+ <Form initialData={initialData} onSubmit={handleSubmit} actions={actions}>
42
+ {children}
43
+ </Form>
44
+ </Modal>
45
+ )
46
+ }