@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,14 @@
1
+ import { type ReactNode } from 'react'
2
+ import { type FormData } from '../../contexts/FormContext'
3
+ import { type ModalSize } from '../Modal/Modal.types'
4
+
5
+ export interface FormModalProps {
6
+ title: ReactNode
7
+ size?: ModalSize
8
+ initialData?: FormData
9
+ submitLabel?: string
10
+ cancelLabel?: string
11
+ onSubmit: (data: FormData) => void | Promise<void>
12
+ onClose: () => void
13
+ children: ReactNode
14
+ }
@@ -0,0 +1,5 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ wrapper: tv({ base: '' })
5
+ }
@@ -0,0 +1,2 @@
1
+ export { FormModal } from './FormModal'
2
+ export * from './FormModal.types'
@@ -0,0 +1,21 @@
1
+ import { Button } from '../../buttons'
2
+ import { Modal } from '../Modal'
3
+ import { type InfoModalProps } from './InfoModal.types'
4
+ import { styles } from './InfoModal.variants'
5
+
6
+ export function InfoModal({
7
+ title,
8
+ size,
9
+ dismissLabel = 'OK',
10
+ onClose,
11
+ children
12
+ }: InfoModalProps) {
13
+ return (
14
+ <Modal title={title} size={size} onClose={onClose}>
15
+ <div className={styles.content()}>{children}</div>
16
+ <footer className={styles.footer()}>
17
+ <Button label={dismissLabel} onClick={onClose} />
18
+ </footer>
19
+ </Modal>
20
+ )
21
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react'
2
+ import { type ModalSize } from '../Modal/Modal.types'
3
+
4
+ export interface InfoModalProps {
5
+ title: ReactNode
6
+ size?: ModalSize
7
+ dismissLabel?: string
8
+ onClose: () => void
9
+ children: ReactNode
10
+ }
@@ -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' })
6
+ }
@@ -0,0 +1,2 @@
1
+ export { InfoModal } from './InfoModal'
2
+ export * from './InfoModal.types'
@@ -0,0 +1,34 @@
1
+ import { useEffect } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { faXmark } from '@fortawesome/free-solid-svg-icons'
4
+ import { IconButton } from '../../buttons'
5
+ import { type ModalProps } from './Modal.types'
6
+ import { styles } from './Modal.variants'
7
+ import { useBackHandler } from '../../back'
8
+
9
+ export function Modal({ title, size, onClose, children }: ModalProps) {
10
+ // the phone's back button closes it
11
+ useBackHandler(true, onClose)
12
+ useEffect(() => {
13
+ const onKey = (e: KeyboardEvent) => {
14
+ if (e.key === 'Escape') onClose()
15
+ }
16
+ window.addEventListener('keydown', onKey)
17
+ return () => window.removeEventListener('keydown', onKey)
18
+ }, [onClose])
19
+
20
+ return createPortal(
21
+ <div className={styles.backdrop()} onMouseDown={e => e.target === e.currentTarget && onClose()}>
22
+ <div className={styles.modal({ size })} role="dialog" aria-modal="true">
23
+ {title !== undefined && (
24
+ <header className={styles.header()}>
25
+ <h2 className={styles.title()}>{title}</h2>
26
+ <IconButton color="secondary" icon={faXmark} onClick={onClose} aria-label="Close" />
27
+ </header>
28
+ )}
29
+ <div className={styles.content()}>{children}</div>
30
+ </div>
31
+ </div>,
32
+ document.body
33
+ )
34
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full'
4
+
5
+ export interface ModalProps {
6
+ title?: ReactNode
7
+ size?: ModalSize
8
+ onClose: () => void
9
+ children: ReactNode
10
+ }
@@ -0,0 +1,48 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ backdrop: tv({
5
+ base: [
6
+ 'fixed inset-0',
7
+ 'flex',
8
+ 'items-start',
9
+ 'justify-center',
10
+ // phone: the modal is the whole screen (no margin below sm)
11
+ 'sm:p-4',
12
+ 'bg-black-overlay',
13
+ 'backdrop-blur-xs',
14
+ 'pretty-scrollbar',
15
+ 'z-999'
16
+ ]
17
+ }),
18
+
19
+ modal: tv({
20
+ base: [
21
+ 'bg-steel-800',
22
+ 'text-steel-100',
23
+ 'sm:rounded-sm',
24
+ 'shadow',
25
+ 'sm:my-4',
26
+ 'min-h-full sm:min-h-0',
27
+ 'h-auto',
28
+ 'w-full',
29
+ 'relative'
30
+ ],
31
+ variants: {
32
+ size: {
33
+ sm: 'sm:max-w-lg',
34
+ md: 'sm:max-w-2xl',
35
+ lg: 'sm:max-w-4xl',
36
+ xl: 'sm:max-w-6xl',
37
+ full: 'max-w-none w-screen h-screen my-0'
38
+ }
39
+ },
40
+ defaultVariants: {
41
+ size: 'sm'
42
+ }
43
+ }),
44
+
45
+ header: tv({ base: 'flex justify-between items-center gap-4 p-6 pb-4' }),
46
+ title: tv({ base: 'text-xl font-semibold' }),
47
+ content: tv({ base: 'px-6 pb-6' })
48
+ }
@@ -0,0 +1,2 @@
1
+ export { Modal } from './Modal'
2
+ export * from './Modal.types'
@@ -0,0 +1,4 @@
1
+ export * from './Modal'
2
+ export * from './ConfirmationModal'
3
+ export * from './FormModal'
4
+ export * from './InfoModal'
@@ -0,0 +1,19 @@
1
+ import { Fragment } from 'react'
2
+ import { Link } from 'react-router'
3
+ import { type BreadCrumbProps } from './BreadCrumb.types'
4
+ import { styles } from './BreadCrumb.variants'
5
+
6
+ export function BreadCrumb({ crumbs, separator = '/' }: BreadCrumbProps) {
7
+ return (
8
+ <ul className={styles.container()}>
9
+ {crumbs.map((crumb, i) => (
10
+ <Fragment key={`crumb-${i}`}>
11
+ {i > 0 && <li className={styles.separator()}>{separator}</li>}
12
+ <li className={styles.crumb()}>
13
+ <Link to={crumb.path}>{crumb.label}</Link>
14
+ </li>
15
+ </Fragment>
16
+ ))}
17
+ </ul>
18
+ )
19
+ }
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ export interface Crumb {
4
+ path: string
5
+ label: ReactNode
6
+ }
7
+
8
+ export interface BreadCrumbProps {
9
+ crumbs: Crumb[]
10
+ separator?: ReactNode
11
+ }
@@ -0,0 +1,7 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ container: tv({ base: 'flex gap-2 text-xs mb-1' }),
5
+ crumb: tv({ base: 'hover:underline underline-offset-3 text-steel-100' }),
6
+ separator: tv({ base: 'text-steel-300' })
7
+ }
@@ -0,0 +1,2 @@
1
+ export { BreadCrumb } from './BreadCrumb'
2
+ export * from './BreadCrumb.types'
@@ -0,0 +1 @@
1
+ export * from './BreadCrumb'
@@ -0,0 +1,6 @@
1
+ import { type TableProps } from './Table.types'
2
+ import { styles } from './Table.variants'
3
+
4
+ export function Table({ children, className }: TableProps) {
5
+ return <table className={className ?? styles.table()}>{children}</table>
6
+ }
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ export interface TableProps {
4
+ children: ReactNode
5
+ className?: string
6
+ }
@@ -0,0 +1,5 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ table: tv({ base: 'w-full text-sm text-left' })
5
+ }
@@ -0,0 +1,2 @@
1
+ export { Table } from './Table'
2
+ export * from './Table.types'
@@ -0,0 +1,27 @@
1
+ import { Link } from 'react-router'
2
+ import { type TableCellProps } from './TableCell.types'
3
+ import { styles } from './TableCell.variants'
4
+
5
+ export function TableCell({
6
+ to,
7
+ textAlign = 'left',
8
+ mobile = true,
9
+ className,
10
+ children,
11
+ ...rest
12
+ }: TableCellProps) {
13
+ const computed = styles.td({
14
+ textAlign,
15
+ hiddenOnMobile: !mobile,
16
+ isLink: Boolean(to)
17
+ })
18
+
19
+ return (
20
+ <td className={`${computed} ${className ?? ''}`.trim()} {...rest}>
21
+ {to
22
+ ? <Link to={to} className={styles.link()}>{children}</Link>
23
+ : <span>{children}</span>
24
+ }
25
+ </td>
26
+ )
27
+ }
@@ -0,0 +1,9 @@
1
+ import { type ReactNode, type TdHTMLAttributes } from 'react'
2
+ import { type TextAlign } from '../TableHead/TableHead.types'
3
+
4
+ export interface TableCellProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'children'> {
5
+ to?: string
6
+ textAlign?: TextAlign
7
+ mobile?: boolean
8
+ children?: ReactNode
9
+ }
@@ -0,0 +1,26 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ td: tv({
5
+ base: 'px-4 leading-none',
6
+ variants: {
7
+ isLink: { false: 'py-2' },
8
+ textAlign: {
9
+ left: 'text-left',
10
+ center: 'text-center',
11
+ right: 'text-right'
12
+ },
13
+ hiddenOnMobile: { true: 'hidden md:table-cell' }
14
+ }
15
+ }),
16
+ link: tv({
17
+ base: [
18
+ 'inline-block',
19
+ 'w-full',
20
+ 'py-2',
21
+ 'hover:text-primary-500',
22
+ 'hover:underline',
23
+ 'underline-offset-3'
24
+ ]
25
+ })
26
+ }
@@ -0,0 +1,2 @@
1
+ export { TableCell } from './TableCell'
2
+ export * from './TableCell.types'
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react'
2
+ import { styles } from './TableFootGroup.variants'
3
+
4
+ export function TableFootGroup({ children }: { children: ReactNode }) {
5
+ return (
6
+ <tfoot className={styles.tfoot()}>
7
+ <tr className={styles.rowFoot()}>{children}</tr>
8
+ </tfoot>
9
+ )
10
+ }
@@ -0,0 +1,6 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ tfoot: tv({ base: '' }),
5
+ rowFoot: tv({ base: '' })
6
+ }
@@ -0,0 +1 @@
1
+ export { TableFootGroup } from './TableFootGroup'
@@ -0,0 +1,22 @@
1
+ import { Children } from 'react'
2
+ import { type TableHeadProps } from './TableHead.types'
3
+ import { styles } from './TableHead.variants'
4
+
5
+ export function TableHead({
6
+ textAlign = 'left',
7
+ mobile = true,
8
+ className,
9
+ children,
10
+ ...rest
11
+ }: TableHeadProps) {
12
+ const computed = styles.th({
13
+ textAlign,
14
+ empty: Children.count(children) === 0,
15
+ hiddenOnMobile: !mobile
16
+ })
17
+ return (
18
+ <th className={`${computed} ${className ?? ''}`.trim()} {...rest}>
19
+ {children}
20
+ </th>
21
+ )
22
+ }
@@ -0,0 +1,9 @@
1
+ import { type ReactNode, type ThHTMLAttributes } from 'react'
2
+
3
+ export type TextAlign = 'left' | 'center' | 'right'
4
+
5
+ export interface TableHeadProps extends Omit<ThHTMLAttributes<HTMLTableCellElement>, 'children'> {
6
+ textAlign?: TextAlign
7
+ mobile?: boolean
8
+ children?: ReactNode
9
+ }
@@ -0,0 +1,16 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ th: tv({
5
+ base: 'py-2',
6
+ variants: {
7
+ textAlign: {
8
+ left: 'text-left',
9
+ center: 'text-center',
10
+ right: 'text-right'
11
+ },
12
+ empty: { false: 'px-4' },
13
+ hiddenOnMobile: { true: 'hidden md:table-cell' }
14
+ }
15
+ })
16
+ }
@@ -0,0 +1,2 @@
1
+ export { TableHead } from './TableHead'
2
+ export * from './TableHead.types'
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react'
2
+ import { styles } from './TableHeadGroup.variants'
3
+
4
+ export function TableHeadGroup({ children }: { children: ReactNode }) {
5
+ return (
6
+ <thead className={styles.thead()}>
7
+ <tr className={styles.rowHead()}>{children}</tr>
8
+ </thead>
9
+ )
10
+ }
@@ -0,0 +1,6 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ thead: tv({ base: 'border-b-2 border-steel-800' }),
5
+ rowHead: tv({ base: 'text-xs font-medium text-steel-300' })
6
+ }
@@ -0,0 +1 @@
1
+ export { TableHeadGroup } from './TableHeadGroup'
@@ -0,0 +1,7 @@
1
+ import { type TableRowProps } from './TableRow.types'
2
+ import { styles } from './TableRow.variants'
3
+
4
+ export function TableRow({ children, highlighted = false, className }: TableRowProps) {
5
+ const computed = styles.tr({ highlighted })
6
+ return <tr className={`${computed} ${className ?? ''}`.trim()}>{children}</tr>
7
+ }
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ export interface TableRowProps {
4
+ children: ReactNode
5
+ highlighted?: boolean
6
+ className?: string
7
+ }
@@ -0,0 +1,12 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ tr: tv({
5
+ base: 'hover:bg-primary-700/40 transition-colors',
6
+ variants: {
7
+ highlighted: {
8
+ true: 'bg-product-pink/15 hover:bg-product-pink/25'
9
+ }
10
+ }
11
+ })
12
+ }
@@ -0,0 +1,2 @@
1
+ export { TableRow } from './TableRow'
2
+ export * from './TableRow.types'
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from 'react'
2
+ import { styles } from './TableRowGroup.variants'
3
+
4
+ export function TableRowGroup({ children }: { children: ReactNode }) {
5
+ return <tbody className={styles.tbody()}>{children}</tbody>
6
+ }
@@ -0,0 +1,5 @@
1
+ import { tv } from 'tailwind-variants'
2
+
3
+ export const styles = {
4
+ tbody: tv({ base: 'border-b-2 border-steel-800' })
5
+ }
@@ -0,0 +1 @@
1
+ export { TableRowGroup } from './TableRowGroup'
@@ -0,0 +1,7 @@
1
+ export * from './Table'
2
+ export * from './TableHead'
3
+ export * from './TableHeadGroup'
4
+ export * from './TableRow'
5
+ export * from './TableRowGroup'
6
+ export * from './TableCell'
7
+ export * from './TableFootGroup'
@@ -0,0 +1,4 @@
1
+ export * from './withLoading'
2
+ export * from './snakeCase'
3
+ export * from './useClickOutside'
4
+ export * from './useMediaQuery'
@@ -0,0 +1,6 @@
1
+ export function snakeCase(input: string): string {
2
+ return input
3
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
4
+ .replace(/[-\s]+/g, '_')
5
+ .toLowerCase()
6
+ }
@@ -0,0 +1,16 @@
1
+ import { useEffect, type RefObject } from 'react'
2
+
3
+ type Refs = ReadonlyArray<RefObject<HTMLElement | null>>
4
+
5
+ export function useClickOutside(refs: Refs, callback: () => void): void {
6
+ useEffect(() => {
7
+ const handler = (e: MouseEvent) => {
8
+ const target = e.target as Node
9
+ if (refs.every(ref => ref.current && !ref.current.contains(target))) {
10
+ callback()
11
+ }
12
+ }
13
+ document.addEventListener('mousedown', handler)
14
+ return () => document.removeEventListener('mousedown', handler)
15
+ }, [refs, callback])
16
+ }
@@ -0,0 +1,11 @@
1
+ import { useCallback, useSyncExternalStore } from 'react'
2
+
3
+ /** Whether `query` (a CSS media query) currently matches; follows its changes. */
4
+ export function useMediaQuery(query: string): boolean {
5
+ const subscribe = useCallback((onChange: () => void) => {
6
+ const list = window.matchMedia(query)
7
+ list.addEventListener('change', onChange)
8
+ return () => list.removeEventListener('change', onChange)
9
+ }, [query])
10
+ return useSyncExternalStore(subscribe, () => window.matchMedia(query).matches)
11
+ }
@@ -0,0 +1,13 @@
1
+ export async function withLoading(
2
+ setLoading: (loading: boolean) => void,
3
+ fn?: () => Promise<void> | void
4
+ ): Promise<void> {
5
+ if (!fn) return
6
+ setLoading(true)
7
+ try {
8
+ await fn()
9
+ }
10
+ finally {
11
+ setLoading(false)
12
+ }
13
+ }