nitro-web 0.0.16 → 0.0.18
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.
- package/_example/client/css/index.css +1 -1
- package/_example/client/index.ts +1 -2
- package/_example/tailwind.config.js +14 -14
- package/client/index.ts +6 -5
- package/components/auth/reset.tsx +4 -4
- package/components/auth/signin.tsx +3 -3
- package/components/auth/signup.tsx +5 -5
- package/components/partials/element/button.tsx +4 -3
- package/components/partials/element/calendar.tsx +123 -0
- package/components/partials/element/dropdown.tsx +6 -2
- package/components/partials/element/modal.tsx +54 -196
- package/components/partials/element/sidebar.tsx +2 -2
- package/components/partials/form/checkbox.tsx +1 -1
- package/components/partials/form/input-color.tsx +21 -20
- package/components/partials/form/input-currency.tsx +51 -35
- package/components/partials/form/input-date.tsx +137 -166
- package/components/partials/form/input.tsx +123 -92
- package/components/partials/form/select.tsx +4 -4
- package/components/partials/styleguide.tsx +89 -42
- package/components/settings/settings-account.tsx +6 -6
- package/components/settings/settings-business.tsx +12 -12
- package/components/settings/settings-team--member.tsx +8 -8
- package/package.json +8 -4
- package/readme.md +11 -7
- package/types/util.d.ts +19 -10
- package/types/util.d.ts.map +1 -1
- package/types.ts +5 -3
- package/util.js +22 -14
package/_example/client/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import 'nitro-web/client/globals'
|
|
3
2
|
import { setupApp } from 'nitro-web'
|
|
4
3
|
|
|
@@ -6,5 +5,5 @@ import './css/index.css'
|
|
|
6
5
|
import config from './config'
|
|
7
6
|
import { Layout1, Layout2 } from '../components/partials/layouts'
|
|
8
7
|
|
|
9
|
-
// Auto-import page components,
|
|
8
|
+
// Auto-import page components, initialise app, and run config.beforeApp
|
|
10
9
|
setupApp(config, [Layout1, Layout2])
|
|
@@ -4,6 +4,7 @@ import path from 'path'
|
|
|
4
4
|
import Color from 'color'
|
|
5
5
|
|
|
6
6
|
const lighten = (clr, val) => Color(clr).lighten(val).rgb().string()
|
|
7
|
+
const darken = (clr, val) => Color(clr).darken(val).rgb().string()
|
|
7
8
|
const nitroDir = path.dirname(require.resolve('nitro-web'))
|
|
8
9
|
|
|
9
10
|
export default {
|
|
@@ -25,13 +26,11 @@ export default {
|
|
|
25
26
|
},
|
|
26
27
|
colors: {
|
|
27
28
|
// Main colors
|
|
28
|
-
'primary':
|
|
29
|
-
'primary-dark':
|
|
30
|
-
'primary-
|
|
31
|
-
'primary-hover': lighten(colors.indigo[500], 0.05),
|
|
29
|
+
'primary': '#4c50f9',
|
|
30
|
+
'primary-dark': darken('#4c50f9', 0.05),
|
|
31
|
+
'primary-hover': lighten('#4c50f9', 0.05),
|
|
32
32
|
'secondary': colors.green[500],
|
|
33
33
|
'secondary-dark': colors.green[600],
|
|
34
|
-
'secondary-light': colors.green[400],
|
|
35
34
|
'secondary-hover': lighten(colors.green[500], 0.05),
|
|
36
35
|
'label': colors.gray[900],
|
|
37
36
|
'link': colors.black,
|
|
@@ -40,6 +39,7 @@ export default {
|
|
|
40
39
|
'light': colors.gray[100],
|
|
41
40
|
'dark': colors.gray[900],
|
|
42
41
|
// Alert colors
|
|
42
|
+
'critical': '#ff0000',
|
|
43
43
|
'danger': '#ff0000',
|
|
44
44
|
'danger-dark': colors.red[800],
|
|
45
45
|
'info': colors.blue[500],
|
|
@@ -54,15 +54,15 @@ export default {
|
|
|
54
54
|
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
|
55
55
|
},
|
|
56
56
|
fontSize: {
|
|
57
|
-
'2xs': ['
|
|
58
|
-
'xs': ['
|
|
59
|
-
'sm
|
|
60
|
-
'
|
|
61
|
-
'base': ['
|
|
62
|
-
'lg': ['
|
|
63
|
-
'xl': ['
|
|
64
|
-
'2xl': ['
|
|
65
|
-
'3xl': ['
|
|
57
|
+
'2xs': ['12px', { lineHeight: '1.5' }],
|
|
58
|
+
'xs': ['13px', { lineHeight: '1.5' }],
|
|
59
|
+
'sm': ['13.5px', { lineHeight: '1.5' }],
|
|
60
|
+
'md': ['14px', { lineHeight: '1.5' }],
|
|
61
|
+
'base': ['15.5px', { lineHeight: '1.5' }],
|
|
62
|
+
'lg': ['18px', { lineHeight: '1.75' }],
|
|
63
|
+
'xl': ['20px', { lineHeight: '1.75' }],
|
|
64
|
+
'2xl': ['22.5px', { lineHeight: '1.75' }],
|
|
65
|
+
'3xl': ['30px', { lineHeight: '1.75' }],
|
|
66
66
|
},
|
|
67
67
|
spacing: {
|
|
68
68
|
'input-before': '0.625rem',
|
package/client/index.ts
CHANGED
|
@@ -22,11 +22,12 @@ export { Styleguide } from '../components/partials/styleguide'
|
|
|
22
22
|
export { Accordion } from '../components/partials/element/accordion'
|
|
23
23
|
export { Avatar } from '../components/partials/element/avatar'
|
|
24
24
|
export { Button } from '../components/partials/element/button'
|
|
25
|
+
export { Calendar, type CalendarProps } from '../components/partials/element/calendar'
|
|
25
26
|
export { Dropdown } from '../components/partials/element/dropdown'
|
|
26
27
|
export { GithubLink } from '../components/partials/element/github-link'
|
|
27
28
|
export { Initials } from '../components/partials/element/initials'
|
|
28
29
|
export { Message } from '../components/partials/element/message'
|
|
29
|
-
|
|
30
|
+
export { Modal } from '../components/partials/element/modal'
|
|
30
31
|
export { Sidebar, type SidebarProps } from '../components/partials/element/sidebar'
|
|
31
32
|
export { Tooltip } from '../components/partials/element/tooltip'
|
|
32
33
|
export { Topbar } from '../components/partials/element/topbar'
|
|
@@ -36,10 +37,10 @@ export { Checkbox } from '../components/partials/form/checkbox'
|
|
|
36
37
|
export { Drop } from '../components/partials/form/drop'
|
|
37
38
|
export { DropHandler } from '../components/partials/form/drop-handler'
|
|
38
39
|
export { FormError } from '../components/partials/form/form-error'
|
|
39
|
-
export {
|
|
40
|
-
export {
|
|
41
|
-
export {
|
|
42
|
-
|
|
40
|
+
export { Field } from '../components/partials/form/input'
|
|
41
|
+
export { FieldColor, type FieldColorProps } from '../components/partials/form/input-color'
|
|
42
|
+
export { FieldCurrency, type FieldCurrencyProps } from '../components/partials/form/input-currency'
|
|
43
|
+
export { FieldDate, type FieldDateProps } from '../components/partials/form/input-date'
|
|
43
44
|
export { Location } from '../components/partials/form/location'
|
|
44
45
|
export { Select, getSelectStyle } from '../components/partials/form/select'
|
|
45
46
|
export { Toggle } from '../components/partials/form/toggle'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Topbar,
|
|
1
|
+
import { Topbar, Field, FormError, Button, util } from 'nitro-web'
|
|
2
2
|
import { Errors } from 'types'
|
|
3
3
|
|
|
4
4
|
export function ResetInstructions() {
|
|
@@ -24,7 +24,7 @@ export function ResetInstructions() {
|
|
|
24
24
|
<form onSubmit={onSubmit}>
|
|
25
25
|
<div>
|
|
26
26
|
<label for="email">Email Address</label>
|
|
27
|
-
<
|
|
27
|
+
<Field name="email" type="email" state={state} onChange={onChange.bind(setState)} placeholder="Your email address..." />
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
<div class="mb-14">
|
|
@@ -67,11 +67,11 @@ export function ResetPassword() {
|
|
|
67
67
|
<form onSubmit={onSubmit}>
|
|
68
68
|
<div>
|
|
69
69
|
<label for="password">Your New Password</label>
|
|
70
|
-
<
|
|
70
|
+
<Field name="password" type="password" state={state} onChange={onChange.bind(setState)} />
|
|
71
71
|
</div>
|
|
72
72
|
<div>
|
|
73
73
|
<label for="password2">Repeat Your New Password</label>
|
|
74
|
-
<
|
|
74
|
+
<Field name="password2" type="password" state={state} onChange={onChange.bind(setState)} />
|
|
75
75
|
</div>
|
|
76
76
|
|
|
77
77
|
<div class="mb-14">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Topbar,
|
|
1
|
+
import { Topbar, Field, Button, FormError, util } from 'nitro-web'
|
|
2
2
|
import { Config, Errors } from 'types'
|
|
3
3
|
|
|
4
4
|
export function Signin({ config }: { config: Config }) {
|
|
@@ -50,14 +50,14 @@ export function Signin({ config }: { config: Config }) {
|
|
|
50
50
|
<form onSubmit={onSubmit}>
|
|
51
51
|
<div>
|
|
52
52
|
<label for="email">Email Address</label>
|
|
53
|
-
<
|
|
53
|
+
<Field name="email" type="email" state={state} onChange={onChange.bind(setState)} placeholder="Your email address..." />
|
|
54
54
|
</div>
|
|
55
55
|
<div>
|
|
56
56
|
<div class="flex justify-between">
|
|
57
57
|
<label for="password">Password</label>
|
|
58
58
|
<Link to="/reset" class="label underline2">Forgot?</Link>
|
|
59
59
|
</div>
|
|
60
|
-
<
|
|
60
|
+
<Field name="password" type="password" state={state} onChange={onChange.bind(setState)}/>
|
|
61
61
|
</div>
|
|
62
62
|
|
|
63
63
|
<div class="mb-14">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Button,
|
|
1
|
+
import { Button, Field, FormError, Topbar, util } from 'nitro-web'
|
|
2
2
|
import { Config, Errors } from 'types'
|
|
3
3
|
|
|
4
4
|
export function Signup({ config }: { config: Config}) {
|
|
@@ -32,20 +32,20 @@ export function Signup({ config }: { config: Config}) {
|
|
|
32
32
|
<div class="grid grid-cols-2 gap-6">
|
|
33
33
|
<div>
|
|
34
34
|
<label for="name">Your Name</label>
|
|
35
|
-
<
|
|
35
|
+
<Field name="name" placeholder="E.g. Bruce Wayne" state={state} onChange={onChange.bind(setState)} />
|
|
36
36
|
</div>
|
|
37
37
|
<div>
|
|
38
38
|
<label for="business.name">Company Name</label>
|
|
39
|
-
<
|
|
39
|
+
<Field name="business.name" placeholder="E.g. Wayne Enterprises" state={state} onChange={onChange.bind(setState)} />
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
42
42
|
<div>
|
|
43
43
|
<label for="email">Email Address</label>
|
|
44
|
-
<
|
|
44
|
+
<Field name="email" type="email" state={state} onChange={onChange.bind(setState)} placeholder="Your email address..." />
|
|
45
45
|
</div>
|
|
46
46
|
<div>
|
|
47
47
|
<label for="password">Password</label>
|
|
48
|
-
<
|
|
48
|
+
<Field name="password" type="password" state={state} onChange={onChange.bind(setState)}/>
|
|
49
49
|
</div>
|
|
50
50
|
|
|
51
51
|
<div class="mb-14">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { twMerge } from 'tailwind-merge'
|
|
2
|
-
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
|
2
|
+
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid'
|
|
3
3
|
|
|
4
4
|
type Props = {
|
|
5
5
|
color?: 'primary'|'secondary'|'white'
|
|
@@ -16,7 +16,7 @@ type Props = {
|
|
|
16
16
|
export function Button({ color='primary', size='md', className, isLoading, IconLeft, IconRight, IconRight2, children, ...props }: Props) {
|
|
17
17
|
// const size = (color.match(/xs|sm|md|lg/)?.[0] || 'md') as 'xs'|'sm'|'md'|'lg'
|
|
18
18
|
const iconPosition = IconLeft ? 'left' : IconRight ? 'right' : IconRight2 ? 'right2' : 'none'
|
|
19
|
-
const base = 'relative inline-block font-
|
|
19
|
+
const base = 'relative inline-block font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'
|
|
20
20
|
|
|
21
21
|
// Button types
|
|
22
22
|
const primary = 'bg-primary text-white shadow-sm hover:bg-primary-hover focus-visible:outline-primary'
|
|
@@ -48,7 +48,8 @@ export function Button({ color='primary', size='md', className, isLoading, IconL
|
|
|
48
48
|
const loading = isLoading ? '[&>*]:opacity-0 text-opacity-0' : ''
|
|
49
49
|
|
|
50
50
|
function getIcon(Icon: React.ReactNode | 'v') {
|
|
51
|
-
if (Icon == 'v') return <ChevronDownIcon className="size-
|
|
51
|
+
if (Icon == 'v' || Icon == 'down') return <ChevronDownIcon className="size-5 -my-6 -mx-1" />
|
|
52
|
+
if (Icon == '^' || Icon == 'up') return <ChevronUpIcon className="size-5 -my-6 -mx-1" />
|
|
52
53
|
else return Icon
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { DayPicker, getDefaultClassNames } from 'react-day-picker'
|
|
2
|
+
import { isValid } from 'date-fns'
|
|
3
|
+
import 'react-day-picker/style.css'
|
|
4
|
+
import { IsFirstRender } from 'nitro-web'
|
|
5
|
+
|
|
6
|
+
type Mode = 'single'|'multiple'|'range'
|
|
7
|
+
type ModeSelection<T extends Mode> = (
|
|
8
|
+
T extends 'single' ? Date | undefined
|
|
9
|
+
: T extends 'multiple' ? Date[]
|
|
10
|
+
: { from?: Date; to?: Date }
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export type CalendarProps = {
|
|
14
|
+
mode?: Mode
|
|
15
|
+
onChange?: (mode: Mode, value: null|number|(null|number)[]) => void
|
|
16
|
+
value?: null|number|string|(null|number|string)[]
|
|
17
|
+
numberOfMonths?: number
|
|
18
|
+
month?: number // the value may be updated from an outside source, thus the month may have changed
|
|
19
|
+
className?: string
|
|
20
|
+
preserveTime?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Calendar({ mode='single', onChange, value, numberOfMonths, month: monthProp, className, preserveTime }: CalendarProps) {
|
|
24
|
+
const isFirstRender = IsFirstRender()
|
|
25
|
+
const isRange = mode == 'range'
|
|
26
|
+
|
|
27
|
+
// Convert the value to an array of valid* dates
|
|
28
|
+
const dates = useMemo(() => {
|
|
29
|
+
const _dates = Array.isArray(value) ? value : [value]
|
|
30
|
+
return _dates.map(date => isValid(date) ? new Date(date as number) : undefined) ////change to null
|
|
31
|
+
}, [value])
|
|
32
|
+
|
|
33
|
+
// Hold the month in state to control the calendar when the input changes
|
|
34
|
+
const [month, setMonth] = useState(dates[0])
|
|
35
|
+
|
|
36
|
+
// Update the month if its changed from an outside source
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!isFirstRender && monthProp) setMonth(new Date(monthProp))
|
|
39
|
+
}, [monthProp])
|
|
40
|
+
|
|
41
|
+
function handleDayPickerSelect<T extends Mode>(newDate: ModeSelection<T>) {
|
|
42
|
+
switch (mode as T) {
|
|
43
|
+
case 'single': {
|
|
44
|
+
const date = newDate as ModeSelection<'single'>
|
|
45
|
+
preserveTimeFn(date)
|
|
46
|
+
onChange?.(mode, date?.getTime() ?? null)
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
case 'range': {
|
|
50
|
+
const { from, to } = (newDate ?? {}) as ModeSelection<'range'>
|
|
51
|
+
onChange?.(mode, from ? [from.getTime() || null, to?.getTime() || null] : null)
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
case 'multiple': {
|
|
55
|
+
const dates = (newDate as ModeSelection<'multiple'>)?.filter(Boolean) ?? []
|
|
56
|
+
onChange?.(mode, dates.map((d) => d.getTime()))
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function preserveTimeFn(date?: Date) {
|
|
63
|
+
// Preserve time from the original date if needed
|
|
64
|
+
if (preserveTime && dates[0] && date) {
|
|
65
|
+
const originalDate = dates[0]
|
|
66
|
+
date.setHours(
|
|
67
|
+
originalDate.getHours(),
|
|
68
|
+
originalDate.getMinutes(),
|
|
69
|
+
originalDate.getSeconds(),
|
|
70
|
+
originalDate.getMilliseconds()
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const d = getDefaultClassNames()
|
|
76
|
+
const common = {
|
|
77
|
+
month: month,
|
|
78
|
+
onMonthChange: setMonth,
|
|
79
|
+
onSelect: handleDayPickerSelect,
|
|
80
|
+
numberOfMonths: numberOfMonths || (isRange ? 2 : 1),
|
|
81
|
+
modifiersClassNames: {
|
|
82
|
+
// Add a class without _, TW seems to replace this with a space in the css definition, e.g. &:not(.range middle)
|
|
83
|
+
range_middle: `${d.range_middle} rangemiddle`,
|
|
84
|
+
},
|
|
85
|
+
classNames: {
|
|
86
|
+
root: `${d.root} flex`,
|
|
87
|
+
months: `${d.months} flex-nowrap`,
|
|
88
|
+
month_caption: `${d.month_caption} text-2xs pl-2`,
|
|
89
|
+
caption_label: `${d.caption_label} z-auto`,
|
|
90
|
+
button_previous: `${d.button_previous} size-8`,// [&:hover>svg]:fill-primary-dark`,
|
|
91
|
+
button_next: `${d.button_next} size-8`,// [&:hover>svg]:fill-primary-dark`,
|
|
92
|
+
chevron: `${d.chevron} fill-black size-[18px]`,
|
|
93
|
+
|
|
94
|
+
// Days
|
|
95
|
+
weekday: `${d.weekday} text-[11px] font-bold uppercase`,
|
|
96
|
+
day: `${d.day} size-[33px]`,
|
|
97
|
+
day_button: `${d.day_button} size-[33px] text-sm`,
|
|
98
|
+
|
|
99
|
+
// States
|
|
100
|
+
focused: `${d.focused} [&>button]:bg-gray-200 [&>button]:border-gray-200`,
|
|
101
|
+
range_start: `${d.range_start} [&>button]:!bg-primary-dark [&>button]:!border-primary-dark`,
|
|
102
|
+
range_end: `${d.range_end} [&>button]:!bg-primary-dark [&>button]:!border-primary-dark`,
|
|
103
|
+
selected: `${d.selected} font-normal `
|
|
104
|
+
+ '[&:not(.rangemiddle)>button]:!text-white '
|
|
105
|
+
+ '[&:not(.rangemiddle)>button]:!bg-primary-dark '
|
|
106
|
+
+ '[&:not(.rangemiddle)>button]:!border-primary-dark ',
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div>
|
|
112
|
+
{
|
|
113
|
+
mode === 'single' ? (
|
|
114
|
+
<DayPicker mode="single" selected={dates[0]} {...common} className={className} />
|
|
115
|
+
) : mode === 'range' ? (
|
|
116
|
+
<DayPicker mode="range" selected={{ from: dates[0], to: dates[1] }} {...common} className={className} />
|
|
117
|
+
) : (
|
|
118
|
+
<DayPicker mode="multiple" selected={dates.filter((d) => !!d)} {...common} className={className} />
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -104,7 +104,7 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
104
104
|
return cloneElement(el, { key, onMouseDown, onKeyDown }) // adds onClick
|
|
105
105
|
})
|
|
106
106
|
}
|
|
107
|
-
<ul
|
|
107
|
+
<ul
|
|
108
108
|
style={{ minWidth }}
|
|
109
109
|
class={`${menuStyle} absolute invisible opacity-0 select-none min-w-full z-[1]`}
|
|
110
110
|
>
|
|
@@ -132,7 +132,9 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
132
132
|
|
|
133
133
|
const style = css`
|
|
134
134
|
ul {
|
|
135
|
-
transition: transform 0.15s ease, opacity 0.15s ease, visibility 0s 0.15s ease;
|
|
135
|
+
transition: transform 0.15s ease, opacity 0.15s ease, visibility 0s 0.15s ease, max-width 0s 0.15s ease, max-height 0s 0.15s ease;
|
|
136
|
+
max-width: 0; // handy if the dropdown ul exceeds the viewport width
|
|
137
|
+
max-height: 0; // handy if the dropdown ul exceeds the viewport height
|
|
136
138
|
}
|
|
137
139
|
&.is-bottom-right,
|
|
138
140
|
&.is-top-right {
|
|
@@ -166,6 +168,8 @@ const style = css`
|
|
|
166
168
|
opacity: 1;
|
|
167
169
|
visibility: visible;
|
|
168
170
|
transition: transform 0.15s ease, opacity 0.15s ease;
|
|
171
|
+
max-width: 1000px;
|
|
172
|
+
max-height: 1000px;
|
|
169
173
|
}
|
|
170
174
|
&.is-bottom-left > ul,
|
|
171
175
|
&.is-bottom-right > ul {
|
|
@@ -1,111 +1,84 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// todo: finish tailwind conversion
|
|
3
|
-
import { css } from 'twin.macro'
|
|
4
1
|
import { IsFirstRender } from 'nitro-web'
|
|
5
2
|
import SvgX1 from 'nitro-web/client/imgs/icons/x1.svg'
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
type ModalProps = {
|
|
5
|
+
show: boolean
|
|
6
|
+
setShow: (show: boolean) => void
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
maxWidth?: string
|
|
9
|
+
minHeight?: string
|
|
10
|
+
dismissable?: boolean
|
|
11
|
+
[key: string]: unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Modal({ show, setShow, children, maxWidth, minHeight, dismissable = true, ...props }: ModalProps) {
|
|
15
|
+
const [state, setState] = useState(show ? 'open' : 'close')
|
|
16
|
+
const containerEl = useRef<HTMLDivElement>(null)
|
|
10
17
|
const isFirst = IsFirstRender()
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
const states = {
|
|
20
|
+
'close': {
|
|
21
|
+
root: 'left-[-100vw] transition-[left] duration-0 delay-200',
|
|
22
|
+
bg: 'opacity-0',
|
|
23
|
+
container: 'opacity-0 scale-[0.97]',
|
|
24
|
+
},
|
|
25
|
+
'close-now': {
|
|
26
|
+
root: '',
|
|
27
|
+
bg: '',
|
|
28
|
+
container: 'opacity-0 !transition-none',
|
|
29
|
+
},
|
|
30
|
+
'open': {
|
|
31
|
+
root: 'left-0 transition-none model-open',
|
|
32
|
+
bg: 'opacity-100 duration-200',
|
|
33
|
+
container: 'opacity-100 scale-[1] duration-200',
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
const _state = states[state as keyof typeof states]
|
|
37
|
+
|
|
18
38
|
|
|
19
39
|
useEffect(() => {
|
|
40
|
+
if (isFirst) return
|
|
20
41
|
if (show) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} else if (!isFirst) {
|
|
24
|
-
// Dont close if first render (forgot what use case this was needed for)
|
|
42
|
+
setState('open')
|
|
43
|
+
} else {
|
|
25
44
|
setTimeout(() => {
|
|
26
45
|
// If another modal is being opened, force close the container for a smoother transition
|
|
27
46
|
if (document.getElementsByClassName('modal-open').length > 1) {
|
|
28
|
-
setState('
|
|
47
|
+
setState('close-now')
|
|
29
48
|
} else {
|
|
30
|
-
setState('')
|
|
31
|
-
elementWithScrollbar().classList.remove('scrollbarPadding')
|
|
49
|
+
setState('close')
|
|
32
50
|
}
|
|
33
51
|
}, 10)
|
|
34
52
|
}
|
|
35
53
|
// There is a bug during hot-reloading where the modal does't open if we don't ensure
|
|
36
54
|
// the same truthy/falsey type is used.
|
|
37
55
|
}, [!!show])
|
|
38
|
-
|
|
39
|
-
function elementWithScrollbar() {
|
|
40
|
-
// this needs to be non-body element otherwise the Modal.jsx doesn't open/close smoothly
|
|
41
|
-
//document.getElementsByTagName('body')[0] // document.getElementsByClassName('page')[0]
|
|
42
|
-
return document.getElementById('app')
|
|
43
|
-
}
|
|
44
56
|
|
|
45
|
-
function onClick(e) {
|
|
46
|
-
|
|
47
|
-
if (!
|
|
57
|
+
function onClick(e: React.MouseEvent) {
|
|
58
|
+
const clickedOnModal = containerEl.current && containerEl.current.contains(e.target as Node)
|
|
59
|
+
if (!clickedOnModal && dismissable) {
|
|
48
60
|
setShow(false)
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
function createScrollbarClasses() {
|
|
53
|
-
/**
|
|
54
|
-
* Creates reusable margin and padding classes containing the scrollbar width and
|
|
55
|
-
* sets window.scrollbarWidth
|
|
56
|
-
* @return width
|
|
57
|
-
*/
|
|
58
|
-
if (typeof window.scrollbarWidth !== 'undefined') return
|
|
59
|
-
|
|
60
|
-
var outer = document.createElement('div')
|
|
61
|
-
outer.style.visibility = 'hidden'
|
|
62
|
-
outer.style.width = '100px'
|
|
63
|
-
outer.style.margin = '0px'
|
|
64
|
-
outer.style.padding = '0px'
|
|
65
|
-
outer.style.border = '0'
|
|
66
|
-
document.body.appendChild(outer)
|
|
67
|
-
|
|
68
|
-
var widthNoScroll = outer.offsetWidth
|
|
69
|
-
// force scrollbars
|
|
70
|
-
outer.style.overflow = 'scroll'
|
|
71
|
-
|
|
72
|
-
// add innerdiv
|
|
73
|
-
var inner = document.createElement('div')
|
|
74
|
-
inner.style.width = '100%'
|
|
75
|
-
outer.appendChild(inner)
|
|
76
|
-
|
|
77
|
-
var widthWithScroll = inner.offsetWidth
|
|
78
|
-
|
|
79
|
-
// Remove divs
|
|
80
|
-
outer.parentNode.removeChild(outer)
|
|
81
|
-
let width = (window.scrollbarWidth = widthNoScroll - widthWithScroll)
|
|
82
|
-
|
|
83
|
-
// Create new inline stylesheet and append to the head
|
|
84
|
-
let style = document.createElement('style')
|
|
85
|
-
let css = (
|
|
86
|
-
'.scrollbarPadding {padding-right:' + width + 'px !important; overflow:hidden !important;}' +
|
|
87
|
-
'.scrollbarMargin {margin-right:' + width + 'px !important; overflow:hidden !important;}'
|
|
88
|
-
)
|
|
89
|
-
style.type = 'text/css'
|
|
90
|
-
if (style.styleSheet) style.styleSheet.cssText = css //<=IE8
|
|
91
|
-
else style.appendChild(document.createTextNode(css))
|
|
92
|
-
document.getElementsByTagName('head')[0].appendChild(style)
|
|
93
|
-
|
|
94
|
-
return width
|
|
95
|
-
}
|
|
96
|
-
|
|
97
64
|
return (
|
|
98
|
-
<div
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
65
|
+
<div
|
|
66
|
+
onClick={(e) => e.stopPropagation()}
|
|
67
|
+
class={`fixed top-0 w-[100vw] h-[100vh] z-[700] ${_state.root}`}
|
|
68
|
+
>
|
|
69
|
+
<div class={`!absolute inset-0 box-content bg-gray-500/70 transition-opacity ${_state.bg}`}></div>
|
|
70
|
+
<div class={`relative h-[100vh] overflow-y-auto transition-[opacity,transform] ${_state.container}`}>
|
|
71
|
+
<div class="flex items-center justify-center min-h-full" onMouseDown={onClick}>
|
|
103
72
|
<div
|
|
104
73
|
ref={containerEl}
|
|
105
|
-
style={{ maxWidth: maxWidth || '
|
|
106
|
-
class={`
|
|
74
|
+
style={{ maxWidth: maxWidth || '550px', minHeight: minHeight }}
|
|
75
|
+
class={`relative w-full mx-6 mt-4 mb-8 bg-white rounded-lg shadow-lg ${props.className}`}
|
|
107
76
|
>
|
|
108
|
-
<div
|
|
77
|
+
<div
|
|
78
|
+
class="absolute top-0 right-0 p-3 m-1 cursor-pointer"
|
|
79
|
+
onClick={() => { if (dismissable) { setShow(false) }}}>
|
|
80
|
+
<SvgX1 />
|
|
81
|
+
</div>
|
|
109
82
|
{children}
|
|
110
83
|
</div>
|
|
111
84
|
</div>
|
|
@@ -113,118 +86,3 @@ export function Modal({ show, setShow, children, className, maxWidth, minHeight,
|
|
|
113
86
|
</div>
|
|
114
87
|
)
|
|
115
88
|
}
|
|
116
|
-
|
|
117
|
-
const style = css`
|
|
118
|
-
/* Modal structure */
|
|
119
|
-
& {
|
|
120
|
-
position: fixed;
|
|
121
|
-
top: 0;
|
|
122
|
-
width: 100%;
|
|
123
|
-
height: calc(100vh);
|
|
124
|
-
z-index: 699;
|
|
125
|
-
.modal-bg {
|
|
126
|
-
position: absolute !important;
|
|
127
|
-
display: flex;
|
|
128
|
-
top: 0;
|
|
129
|
-
left: 0;
|
|
130
|
-
right: 0;
|
|
131
|
-
bottom: 0;
|
|
132
|
-
box-sizing: content-box;
|
|
133
|
-
&:before {
|
|
134
|
-
content: '';
|
|
135
|
-
display: block;
|
|
136
|
-
flex: 1;
|
|
137
|
-
background: rgba(255, 255, 255, 0.82);
|
|
138
|
-
/* backdrop-filter: blur(1px);
|
|
139
|
-
-webkit-backdrop-filter: blur(1px); */
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
.modal-container {
|
|
143
|
-
position: relative;
|
|
144
|
-
height: calc(100vh);
|
|
145
|
-
// horisontal centering
|
|
146
|
-
> div {
|
|
147
|
-
display: flex;
|
|
148
|
-
align-items: center;
|
|
149
|
-
justify-content: center;
|
|
150
|
-
min-height: 100%;
|
|
151
|
-
// vertical centering
|
|
152
|
-
> div {
|
|
153
|
-
margin: 30px 20px 90px;
|
|
154
|
-
width: 100%;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
&.modal-close-immediately {
|
|
159
|
-
.modal-container > div > div {
|
|
160
|
-
transition: none !important;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/* Animation */
|
|
166
|
-
|
|
167
|
-
& {
|
|
168
|
-
left: -100%;
|
|
169
|
-
transition: left 0s 0.2s;
|
|
170
|
-
}
|
|
171
|
-
.modal-bg {
|
|
172
|
-
opacity: 0;
|
|
173
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
174
|
-
}
|
|
175
|
-
.modal-container {
|
|
176
|
-
/*overflow: hidden;*/
|
|
177
|
-
overflow-y: scroll;
|
|
178
|
-
overflow-x: auto;
|
|
179
|
-
}
|
|
180
|
-
.modal-container > div > div {
|
|
181
|
-
opacity: 0;
|
|
182
|
-
transform: scale(0.97);
|
|
183
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
184
|
-
}
|
|
185
|
-
&.modal-open {
|
|
186
|
-
left: 0;
|
|
187
|
-
transition: none;
|
|
188
|
-
.modal-bg {
|
|
189
|
-
opacity: 1;
|
|
190
|
-
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
191
|
-
}
|
|
192
|
-
.modal-container {
|
|
193
|
-
overflow-y: scroll;
|
|
194
|
-
overflow-x: auto;
|
|
195
|
-
}
|
|
196
|
-
.modal-container > div > div {
|
|
197
|
-
opacity: 1;
|
|
198
|
-
transform: scale(1);
|
|
199
|
-
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/* Modal customisations */
|
|
204
|
-
|
|
205
|
-
.modal1 {
|
|
206
|
-
background: white;
|
|
207
|
-
border: 2px solid #27242C;
|
|
208
|
-
box-shadow: 0px 1px 29px rgba(31, 29, 36, 0.07);
|
|
209
|
-
border-radius: 8px;
|
|
210
|
-
.subtitle {
|
|
211
|
-
margin-bottom: 34px; // same as form pages
|
|
212
|
-
}
|
|
213
|
-
.modal-close {
|
|
214
|
-
position: absolute;
|
|
215
|
-
margin: 10px;
|
|
216
|
-
padding: 15px 20px;
|
|
217
|
-
top: 0;
|
|
218
|
-
right: 0;
|
|
219
|
-
cursor: pointer;
|
|
220
|
-
line {
|
|
221
|
-
transition: all 0.1s;
|
|
222
|
-
}
|
|
223
|
-
&:hover {
|
|
224
|
-
line {
|
|
225
|
-
/* stroke: theme'colors.primary-dark'; */
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
`
|