@datability/8ui 1.0.0 → 1.1.0

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 (84) hide show
  1. package/.prettierrc +8 -0
  2. package/.vscode/extensions.json +6 -0
  3. package/README.md +1 -3
  4. package/declaration.d.ts +10 -0
  5. package/docker-compose.yml +20 -0
  6. package/eslint.config.js +23 -0
  7. package/{dist/index.html → index.html} +1 -2
  8. package/package.json +13 -14
  9. package/src/App.tsx +370 -0
  10. package/src/components/assets/closed.svg +4 -0
  11. package/src/components/assets/expand-arrow.svg +3 -0
  12. package/src/components/assets/visibility-off.svg +5 -0
  13. package/src/components/assets/visibility.svg +5 -0
  14. package/src/components/blackdrop/index.scss +16 -0
  15. package/src/components/blackdrop/index.tsx +18 -0
  16. package/src/components/blackdrop/index.type.ts +7 -0
  17. package/src/components/button/index.scss +40 -0
  18. package/src/components/button/index.tsx +44 -0
  19. package/src/components/button/index.type.ts +13 -0
  20. package/src/components/chip/index.scss +32 -0
  21. package/src/components/chip/index.tsx +39 -0
  22. package/src/components/chip/index.type.ts +12 -0
  23. package/src/components/context.tsx +26 -0
  24. package/src/components/divider/index.scss +4 -0
  25. package/src/components/divider/index.tsx +13 -0
  26. package/src/components/index.ts +62 -0
  27. package/src/components/input/extend.scss +19 -0
  28. package/src/components/input/input-auto-complete/index.scss +53 -0
  29. package/src/components/input/input-auto-complete/index.tsx +140 -0
  30. package/src/components/input/input-auto-complete/index.type.tsx +13 -0
  31. package/src/components/input/input-base/index.scss +14 -0
  32. package/src/components/input/input-base/index.tsx +39 -0
  33. package/src/components/input/input-base/index.type.tsx +13 -0
  34. package/src/components/input/input-basic/index.scss +9 -0
  35. package/src/components/input/input-basic/index.tsx +47 -0
  36. package/src/components/input/input-basic/index.type.tsx +8 -0
  37. package/src/components/input/input-checkbox/index.scss +30 -0
  38. package/src/components/input/input-checkbox/index.tsx +69 -0
  39. package/src/components/input/input-checkbox/index.type.tsx +11 -0
  40. package/src/components/input/input-date/index.scss +102 -0
  41. package/src/components/input/input-date/index.tsx +354 -0
  42. package/src/components/input/input-date/index.type.tsx +11 -0
  43. package/src/components/input/input-date-range/index.scss +101 -0
  44. package/src/components/input/input-date-range/index.tsx +284 -0
  45. package/src/components/input/input-date-range/index.type.tsx +11 -0
  46. package/src/components/input/input-date-time/index.scss +179 -0
  47. package/src/components/input/input-date-time/index.tsx +367 -0
  48. package/src/components/input/input-date-time/index.type.tsx +11 -0
  49. package/src/components/input/input-number/index.scss +9 -0
  50. package/src/components/input/input-number/index.tsx +118 -0
  51. package/src/components/input/input-number/index.type.tsx +11 -0
  52. package/src/components/input/input-password/index.scss +22 -0
  53. package/src/components/input/input-password/index.tsx +60 -0
  54. package/src/components/input/input-password/index.type.tsx +8 -0
  55. package/src/components/input/input-radio/index.scss +35 -0
  56. package/src/components/input/input-radio/index.tsx +72 -0
  57. package/src/components/input/input-radio/index.type.tsx +12 -0
  58. package/src/components/input/input-select/index.scss +81 -0
  59. package/src/components/input/input-select/index.tsx +113 -0
  60. package/src/components/input/input-select/index.type.tsx +15 -0
  61. package/src/components/input/input-switch/index.scss +84 -0
  62. package/src/components/input/input-switch/index.tsx +44 -0
  63. package/src/components/input/input-switch/index.type.tsx +4 -0
  64. package/src/components/input/input-textarea/index.scss +10 -0
  65. package/src/components/input/input-textarea/index.tsx +48 -0
  66. package/src/components/input/input-textarea/index.type.tsx +10 -0
  67. package/src/components/menu/index.scss +30 -0
  68. package/src/components/menu/index.tsx +136 -0
  69. package/src/components/menu/index.type.ts +8 -0
  70. package/src/components/modal/index.scss +33 -0
  71. package/src/components/modal/index.tsx +99 -0
  72. package/src/components/modal/index.type.tsx +8 -0
  73. package/src/index.scss +44 -0
  74. package/src/index.ts +62 -0
  75. package/src/logoDownload.svg +3 -0
  76. package/src/main.tsx +9 -0
  77. package/tsconfig.app.json +28 -0
  78. package/tsconfig.json +42 -0
  79. package/tsconfig.node.json +29 -0
  80. package/vite.config.d.ts +2 -0
  81. package/vite.config.ts +35 -0
  82. package/dist/assets/index-BYmsRLQS.js +0 -48
  83. package/dist/assets/index-QxCDX2bt.css +0 -1
  84. /package/{dist → public}/vite.svg +0 -0
@@ -0,0 +1,72 @@
1
+ // Lib
2
+ import React from "react"
3
+ import { Controller, useFormContext } from "react-hook-form"
4
+
5
+ // Include in project
6
+ import "./index.scss"
7
+ import InputBase from "../input-base"
8
+ import type { PropsInputRadio } from "./index.type"
9
+
10
+ const InputRadio: React.FC<PropsInputRadio> = ({
11
+ name,
12
+ label,
13
+ disabled = false,
14
+ require = false,
15
+ fullWidth = false,
16
+ isVertical = false,
17
+ options,
18
+ }) => {
19
+ const { control } = useFormContext()
20
+
21
+ return (
22
+ <Controller
23
+ name={name}
24
+ control={control}
25
+ render={({ field, fieldState }) => {
26
+ const { value, onChange } = field
27
+ const { invalid, error } = fieldState
28
+
29
+ return (
30
+ <InputBase
31
+ name={name}
32
+ label={label}
33
+ require={require}
34
+ fullWidth={fullWidth}
35
+ isInvalid={invalid}
36
+ errorMessage={error?.message}
37
+ >
38
+ <div className="DBui-wrapInputRadioList" data-vertical={isVertical}>
39
+ {options.map((data, index) => {
40
+ const isChecked = value === data.value
41
+
42
+ return (
43
+ <div key={index} className="DBui-wrapInputRadio">
44
+ <input
45
+ className="DBui-inputRadio"
46
+ type="radio"
47
+ disabled={disabled}
48
+ checked={isChecked}
49
+ onClick={() => {
50
+ // คลิกซ้ำ = uncheck
51
+ if (isChecked) {
52
+ onChange("")
53
+ } else {
54
+ onChange(data.value)
55
+ }
56
+ }}
57
+ />
58
+ <p className="DBui-labelRadio" data-invalid={invalid}>
59
+ <small>{data.label}</small>
60
+ </p>
61
+ </div>
62
+ )
63
+ })}
64
+ </div>
65
+ </InputBase>
66
+ )
67
+ }}
68
+ />
69
+ )
70
+ }
71
+
72
+ export default InputRadio
@@ -0,0 +1,12 @@
1
+ export type PropsInputRadio = {
2
+ name: string
3
+ label?: string
4
+ disabled?: boolean
5
+ require?: boolean
6
+ fullWidth?: boolean
7
+ isVertical?: boolean
8
+ options: {
9
+ label: string
10
+ value: string | number
11
+ }[]
12
+ }
@@ -0,0 +1,81 @@
1
+ @import '../extend.scss';
2
+
3
+ .DBui-wrapperInputSelect {
4
+ cursor: pointer;
5
+ position: relative;
6
+ height: 35px;
7
+ width: 100%;
8
+ padding: 0.3em 35px 0.3em 0.5em;
9
+ border-radius: 8px;
10
+ border: 1px solid #e3e3e8;
11
+ background-color: #ffffff;
12
+ color: #000000;
13
+ display: flex;
14
+
15
+ &[data-invalid='true'] {
16
+ border: 1px solid #eb5757;
17
+ }
18
+
19
+ &[data-disabled='true'] {
20
+ @extend .DBui-disabled;
21
+ }
22
+
23
+ .DBui-wrapperLabel {
24
+ text-overflow: ellipsis;
25
+ overflow: hidden;
26
+ display: -webkit-box;
27
+ -webkit-line-clamp: 1;
28
+ -webkit-box-orient: vertical;
29
+ }
30
+
31
+ .DBui-placeholder {
32
+ color: #c3c3c3;
33
+ }
34
+
35
+ .DBui-wrapperLabelInputSelect {
36
+ display: flex;
37
+ gap: 0.3em;
38
+ align-items: center;
39
+ width: 100%;
40
+
41
+ .DBui-clearIconInputSelect {
42
+ width: 20px;
43
+
44
+ &[data-hidden='true'] {
45
+ display: none !important;
46
+ }
47
+
48
+ &:hover {
49
+ opacity: 0.8;
50
+ }
51
+ }
52
+ }
53
+
54
+ .DBui-arrowIconInputSelect {
55
+ position: absolute;
56
+ right: 5px;
57
+ top: 50%;
58
+ transform: translateY(-50%);
59
+ transition: transform 0.2s;
60
+
61
+ &[data-checked='false'] {
62
+ transform: rotate(180deg) translateY(50%);
63
+ }
64
+ }
65
+ }
66
+
67
+ .DBui-menuItemInputSelect {
68
+ background-color: #ffffff;
69
+ color: #000000;
70
+ padding: 0.3em 0.5em;
71
+ cursor: pointer;
72
+
73
+ &:hover {
74
+ background-color: #fafafa;
75
+ }
76
+
77
+ &[data-checked='true'] {
78
+ background-color: #0045a5;
79
+ color: #ffffff;
80
+ }
81
+ }
@@ -0,0 +1,113 @@
1
+ // Lib
2
+ import React from "react"
3
+ import { Controller, useFormContext } from "react-hook-form"
4
+
5
+ // Images
6
+ import expandArrowSVG from "../../assets/expand-arrow.svg"
7
+ import closedSVG from "../../assets/closed.svg"
8
+
9
+ // Include in project
10
+ import "./index.scss"
11
+ import InputBase from "../input-base"
12
+ import Menu from "../../menu"
13
+ import type { TOption, TValueOption } from "../.."
14
+ import type { PropsInputSelect } from "./index.type"
15
+
16
+ const InputSelect: React.FC<PropsInputSelect> = ({
17
+ name,
18
+ label,
19
+ disabled = false,
20
+ require = false,
21
+ fullWidth = false,
22
+ options,
23
+ onChange,
24
+ isHideClearIcon = true,
25
+ placeholder,
26
+ isInModal = false,
27
+ }) => {
28
+ const { control } = useFormContext()
29
+
30
+ return (
31
+ <Controller
32
+ name={name}
33
+ control={control}
34
+ render={({ field, fieldState }) => {
35
+ const { value, onChange: setValue } = field
36
+ const { invalid, error } = fieldState
37
+
38
+ const selected = options?.find((e) => e.value === value) as TOption | undefined
39
+
40
+ const handleSelect = (val: TValueOption, close: () => void) => {
41
+ setValue(val)
42
+ onChange?.(val)
43
+ close()
44
+ }
45
+
46
+ const handleClear = (e: React.MouseEvent<HTMLImageElement>) => {
47
+ e.stopPropagation()
48
+ setValue(null)
49
+ onChange?.(null)
50
+ }
51
+
52
+ return (
53
+ <InputBase
54
+ name={name}
55
+ label={label}
56
+ require={require}
57
+ fullWidth={fullWidth}
58
+ isInvalid={invalid}
59
+ errorMessage={error?.message}
60
+ >
61
+ <Menu
62
+ isInModal={isInModal}
63
+ disabled={disabled}
64
+ trigger={({ isOpen }) => (
65
+ <div className="DBui-wrapperInputSelect" data-invalid={invalid} data-disabled={disabled}>
66
+ <div className="DBui-wrapperLabelInputSelect">
67
+ {selected?.label ? (
68
+ <p className="DBui-wrapperLabel">
69
+ <small>{selected.label}</small>
70
+ </p>
71
+ ) : (
72
+ <p className="DBui-placeholder">
73
+ <small>{placeholder}</small>
74
+ </p>
75
+ )}
76
+
77
+ <img
78
+ src={closedSVG}
79
+ className="DBui-clearIconInputSelect"
80
+ onClick={handleClear}
81
+ data-hidden={!value || disabled || isHideClearIcon}
82
+ />
83
+ </div>
84
+
85
+ <img
86
+ src={expandArrowSVG}
87
+ className="DBui-arrowIconInputSelect"
88
+ data-checked={disabled ? true : !isOpen}
89
+ />
90
+ </div>
91
+ )}
92
+ >
93
+ {({ close }) =>
94
+ options.map((data, index) => (
95
+ <p
96
+ key={index}
97
+ className="DBui-menuItemInputSelect"
98
+ onClick={() => handleSelect(data.value, close)}
99
+ data-checked={value === data.value}
100
+ >
101
+ {data.label}
102
+ </p>
103
+ ))
104
+ }
105
+ </Menu>
106
+ </InputBase>
107
+ )
108
+ }}
109
+ />
110
+ )
111
+ }
112
+
113
+ export default InputSelect
@@ -0,0 +1,15 @@
1
+ import type { TOption, TValueOption } from "../.."
2
+
3
+ export type PropsInputSelect = {
4
+ name: string
5
+ label?: string
6
+ placeholder?: string
7
+ disabled?: boolean
8
+ require?: boolean
9
+ fullWidth?: boolean
10
+ options: TOption[]
11
+ onChange?: (value: TValueOption | null) => void
12
+ value?: string
13
+ isHideClearIcon?: boolean
14
+ isInModal?: boolean
15
+ }
@@ -0,0 +1,84 @@
1
+ .DBui-switch {
2
+ position: relative;
3
+ display: inline-block;
4
+ width: 51px;
5
+ height: 28px;
6
+
7
+ input {
8
+ opacity: 0;
9
+ width: 0;
10
+ height: 0;
11
+ }
12
+
13
+ .DBui-minus {
14
+ position: absolute;
15
+ height: 9px;
16
+ width: 1px;
17
+ left: 18px;
18
+ bottom: 9px;
19
+ background-color: #1C1C1E;
20
+ z-index: 1;
21
+ border-radius: 999px;
22
+ }
23
+
24
+ .DBui-miniCircle {
25
+ position: absolute;
26
+ border-radius: 100%;
27
+ height: 7px;
28
+ width: 7px;
29
+ right: 11px;
30
+ bottom: 10px;
31
+ box-shadow: 0 0 0px 1px #000000;
32
+ z-index: 1;
33
+ }
34
+
35
+ .DBui-slider {
36
+ border-radius: 34px;
37
+
38
+ position: absolute;
39
+ cursor: pointer;
40
+ top: 0;
41
+ left: 0;
42
+ right: 0;
43
+ bottom: 0;
44
+ background-color: #AAAAAA;
45
+ -webkit-transition: 0.3s cubic-bezier(0.61, -0.01, 0.2, 1.37);
46
+ transition: 0.3s cubic-bezier(0.61, -0.01, 0.2, 1.37);
47
+
48
+ &:before {
49
+ border-radius: 50%;
50
+ z-index: 2;
51
+
52
+ position: absolute;
53
+ content: '';
54
+ height: 20px;
55
+ width: 20px;
56
+ left: 4px;
57
+ bottom: 4px;
58
+ background-color: #1C1C1E;
59
+ box-shadow: 0px 0.31px 10px 0px #00000004;
60
+ -webkit-transition: 0.3s cubic-bezier(0.61, -0.01, 0.2, 1.37);
61
+ transition: 0.3s cubic-bezier(0.61, -0.01, 0.2, 1.37);
62
+ }
63
+ }
64
+
65
+ input:checked+.DBui-slider {
66
+ background-color: #477cee;
67
+ }
68
+
69
+ input:checked+.DBui-slider:before {
70
+ -webkit-transform: translateX(22px);
71
+ -ms-transform: translateX(22px);
72
+ transform: translateX(22px);
73
+ }
74
+
75
+ .DBui-wrapInputList {
76
+ display: flex;
77
+ flex-direction: row;
78
+
79
+ &[data-disabled='true'] {
80
+ opacity: 0.6;
81
+ pointer-events: none;
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,44 @@
1
+ // Lib
2
+ import React from "react"
3
+ import { Controller, useFormContext } from "react-hook-form"
4
+
5
+ // Include in project
6
+ import "./index.scss"
7
+ import type { PropsInputSwitch } from "./index.type"
8
+
9
+ const InputSwitch: React.FC<PropsInputSwitch> = ({ name, disabled }) => {
10
+ const { control } = useFormContext()
11
+
12
+ return (
13
+ <Controller
14
+ name={name}
15
+ control={control}
16
+ render={({ field }) => {
17
+ const { value, onChange } = field
18
+
19
+ return (
20
+ <div className="DBui-wrapInputList" data-disabled={disabled}>
21
+ <label className="DBui-switch">
22
+ <span className="DBui-minus"></span>
23
+ <span className="DBui-miniCircle"></span>
24
+
25
+ <input
26
+ type="checkbox"
27
+ checked={Boolean(value)}
28
+ disabled={disabled}
29
+ onChange={(e) => {
30
+ e.stopPropagation()
31
+ onChange(!value)
32
+ }}
33
+ />
34
+
35
+ <span className="DBui-slider"></span>
36
+ </label>
37
+ </div>
38
+ )
39
+ }}
40
+ />
41
+ )
42
+ }
43
+
44
+ export default InputSwitch
@@ -0,0 +1,4 @@
1
+ export type PropsInputSwitch = {
2
+ name: string
3
+ disabled?: boolean
4
+ }
@@ -0,0 +1,10 @@
1
+ @import '../extend.scss';
2
+
3
+ .DBui-inputTextarea {
4
+ @extend .DBui-input;
5
+ resize: none;
6
+
7
+ &:disabled {
8
+ @extend .DBui-disabled;
9
+ }
10
+ }
@@ -0,0 +1,48 @@
1
+ // Lib
2
+ import React from "react"
3
+ import { useFormContext, useFormState } from "react-hook-form"
4
+
5
+ // Include in project
6
+ import "./index.scss"
7
+ import InputBase from "../input-base"
8
+ import type { PropsInputTextarea } from "./index.type"
9
+
10
+ const InputTextarea: React.FC<PropsInputTextarea> = ({
11
+ name,
12
+ label,
13
+ placeholder,
14
+ disabled = false,
15
+ require = false,
16
+ fullWidth = false,
17
+ rows = 2,
18
+ cols = 50,
19
+ }) => {
20
+ const { register, control } = useFormContext()
21
+ const { errors } = useFormState({ control, name })
22
+
23
+ const error = errors?.[name]
24
+ const isInvalid = Boolean(error)
25
+
26
+ return (
27
+ <InputBase
28
+ name={name}
29
+ label={label}
30
+ require={require}
31
+ fullWidth={fullWidth}
32
+ isInvalid={isInvalid}
33
+ errorMessage={error?.message}
34
+ >
35
+ <textarea
36
+ {...register(name)}
37
+ className="DBui-inputTextarea"
38
+ placeholder={placeholder}
39
+ disabled={disabled}
40
+ rows={rows}
41
+ cols={cols}
42
+ data-invalid={isInvalid}
43
+ />
44
+ </InputBase>
45
+ )
46
+ }
47
+
48
+ export default InputTextarea
@@ -0,0 +1,10 @@
1
+ export type PropsInputTextarea = {
2
+ name: string
3
+ label?: string
4
+ placeholder?: string
5
+ disabled?: boolean
6
+ require?: boolean
7
+ fullWidth?: boolean
8
+ rows?: number
9
+ cols?: number
10
+ }
@@ -0,0 +1,30 @@
1
+ .DBui-wrapperMenu {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ cursor: pointer;
5
+ }
6
+
7
+ .DBui-wrapperMenuItem {
8
+ position: fixed;
9
+ background: #fff;
10
+ color: #000;
11
+ border-radius: 0.3em;
12
+ min-width: 140px;
13
+ max-width: 320px;
14
+ max-height: 45%;
15
+ box-shadow: rgba(0, 0, 0, 0.15) 0px 4px 12px;
16
+ overflow-y: auto;
17
+ overflow-x: hidden;
18
+ transition: opacity 0.1s ease;
19
+ z-index: var(--z-index-menu);
20
+
21
+ &[data-hidden='true'] {
22
+ opacity: 0;
23
+ visibility: hidden;
24
+ pointer-events: none;
25
+ }
26
+
27
+ &[data-is-in-modal='true'] {
28
+ z-index: var(--z-index-menu-in-modal);
29
+ }
30
+ }
@@ -0,0 +1,136 @@
1
+ // Lib
2
+ import React, { useEffect, useRef, useState } from "react"
3
+ import { createPortal } from "react-dom"
4
+ import "./index.scss"
5
+ import type { PropsMenu } from "./index.type"
6
+
7
+ const Menu: React.FC<PropsMenu> = ({ children, trigger, disabled, isInModal = false }) => {
8
+ const [isOpen, setIsOpen] = useState(false)
9
+ const [menuStyle, setMenuStyle] = useState({})
10
+
11
+ const triggerRef = useRef<HTMLDivElement>(null)
12
+ const menuRef = useRef<HTMLDivElement>(null)
13
+
14
+ // Toggle dropdown
15
+ const toggleMenu = () => {
16
+ if (!disabled) setIsOpen((prev) => !prev)
17
+ }
18
+
19
+ // ------------ MAIN POSITIONING FUNCTION ------------
20
+ const calculateMenuPosition = () => {
21
+ if (!triggerRef.current || !menuRef.current) return
22
+
23
+ const triggerRect = triggerRef.current.getBoundingClientRect()
24
+ const menu = menuRef.current
25
+
26
+ const viewportH = window.innerHeight
27
+ const viewportW = window.innerWidth
28
+ const menuWidth = menu.offsetWidth
29
+
30
+ const spaceAbove = triggerRect.top
31
+ const spaceBelow = viewportH - triggerRect.bottom
32
+
33
+ // วางล่างถ้าพื้นที่ล่างมากกว่า
34
+ const placeBottom = spaceBelow >= spaceAbove
35
+
36
+ let top = placeBottom
37
+ ? triggerRect.bottom // show below
38
+ : triggerRect.top - menu.offsetHeight // show above (height กำหนดจาก CSS max-height)
39
+
40
+ // เปลี่ยน logic ตรงนี้ → กันเมนูล้นจอจาก CSS max-height
41
+ // ถ้าด้านบนพื้นที่ไม่พอ ให้ clamp
42
+ if (!placeBottom) {
43
+ const minTop = 8
44
+ if (top < minTop) top = minTop
45
+ }
46
+
47
+ let left = triggerRect.left
48
+ if (left + menuWidth > viewportW - 8) {
49
+ left = viewportW - menuWidth - 8
50
+ }
51
+ if (left < 8) left = 8
52
+
53
+ setMenuStyle({
54
+ position: "fixed",
55
+ top,
56
+ left,
57
+ })
58
+ }
59
+
60
+ // ------------ Double RAF (ให้ DOM render ก่อน 100%) ------------
61
+ useEffect(() => {
62
+ if (!isOpen) return
63
+
64
+ requestAnimationFrame(() => {
65
+ requestAnimationFrame(() => {
66
+ calculateMenuPosition()
67
+ })
68
+ })
69
+ }, [isOpen, children])
70
+
71
+ // ------------ Reposition when scroll/resize/orientation ------------
72
+ useEffect(() => {
73
+ if (!isOpen) return
74
+
75
+ const handler = () => calculateMenuPosition()
76
+
77
+ window.addEventListener("scroll", handler, true)
78
+ window.addEventListener("resize", handler)
79
+ window.addEventListener("orientationchange", handler)
80
+
81
+ return () => {
82
+ window.removeEventListener("scroll", handler, true)
83
+ window.removeEventListener("resize", handler)
84
+ window.removeEventListener("orientationchange", handler)
85
+ }
86
+ }, [isOpen, children])
87
+
88
+ // ------------ Close On Click Outside ------------
89
+ useEffect(() => {
90
+ if (!isOpen) return
91
+
92
+ const handleClick = (e: MouseEvent) => {
93
+ const t = triggerRef.current
94
+ const m = menuRef.current
95
+ if (!t || !m) return
96
+
97
+ if (!t.contains(e.target as Node) && !m.contains(e.target as Node)) {
98
+ setIsOpen(false)
99
+ }
100
+ }
101
+
102
+ document.addEventListener("mousedown", handleClick)
103
+ return () => document.removeEventListener("mousedown", handleClick)
104
+ }, [isOpen])
105
+
106
+ // Portal root
107
+ const portalRoot = document.getElementById("root") || document.getElementById("__next") || document.body
108
+
109
+ return (
110
+ <>
111
+ <div ref={triggerRef} className="DBui-wrapperMenu" onClick={toggleMenu}>
112
+ {trigger({ isOpen })}
113
+ </div>
114
+
115
+ {isOpen &&
116
+ createPortal(
117
+ <div
118
+ ref={menuRef}
119
+ className="DBui-wrapperMenuItem"
120
+ style={menuStyle}
121
+ data-hidden={!isOpen}
122
+ data-is-in-modal={isInModal}
123
+ >
124
+ {typeof children === "function"
125
+ ? children({
126
+ close: () => setIsOpen(false),
127
+ })
128
+ : children}
129
+ </div>,
130
+ portalRoot,
131
+ )}
132
+ </>
133
+ )
134
+ }
135
+
136
+ export default Menu
@@ -0,0 +1,8 @@
1
+ import type { JSX } from "react"
2
+
3
+ export type PropsMenu = {
4
+ trigger: (props: { isOpen: boolean }) => JSX.Element
5
+ children: (args: { close: () => void }) => JSX.Element | JSX.Element[]
6
+ disabled?: boolean
7
+ isInModal?: boolean
8
+ }