nitro-web 0.0.73 → 0.0.75
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/client/app.tsx +6 -12
- package/components/auth/auth.api.js +6 -2
- package/components/partials/element/dropdown.tsx +61 -8
- package/components/partials/element/filters.tsx +2 -1
- package/components/partials/element/github-link.tsx +1 -1
- package/components/partials/form/checkbox.tsx +128 -103
- package/components/partials/form/field.tsx +13 -19
- package/components/partials/styleguide.tsx +8 -5
- package/package.json +1 -1
package/client/app.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createBrowserRouter, createHashRouter, redirect,
|
|
1
|
+
import { createBrowserRouter, createHashRouter, redirect, RouterProvider } from 'react-router-dom'
|
|
2
2
|
import { Fragment, ReactNode } from 'react'
|
|
3
3
|
import ReactDOM from 'react-dom/client'
|
|
4
4
|
import { axios, camelCase, pick, toArray, setTimeoutPromise } from 'nitro-web/util'
|
|
@@ -25,7 +25,7 @@ type Settings = {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
type Route = {
|
|
28
|
-
component: React.FC<{ route?: Route;
|
|
28
|
+
component: React.FC<{ route?: Route; config?: Config }>
|
|
29
29
|
middleware: string[]
|
|
30
30
|
name: string
|
|
31
31
|
path: string
|
|
@@ -70,9 +70,7 @@ export function updateJwt(token?: string | null) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function App({ settings, config, storeContainer }: { settings: Settings, config: Config, storeContainer: StoreContainer }): ReactNode {
|
|
73
|
-
|
|
74
|
-
const router = getRouter({ settings, config })
|
|
75
|
-
// const theme = pick(themeNormalised, []) // e.g. 'topPanelHeight'
|
|
73
|
+
const router = useMemo(() => getRouter({ settings, config }), [])
|
|
76
74
|
|
|
77
75
|
useEffect(() => {
|
|
78
76
|
/**
|
|
@@ -97,10 +95,8 @@ function App({ settings, config, storeContainer }: { settings: Settings, config:
|
|
|
97
95
|
|
|
98
96
|
return (
|
|
99
97
|
<storeContainer.Provider>
|
|
100
|
-
{
|
|
101
|
-
{ router && <RouterProvider router={router} /> }
|
|
98
|
+
{ router && <RouterProvider router={router}/> }
|
|
102
99
|
<AfterApp settings={settings} />
|
|
103
|
-
{/* </ThemeProvider> */}
|
|
104
100
|
</storeContainer.Provider>
|
|
105
101
|
)
|
|
106
102
|
}
|
|
@@ -206,7 +202,7 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
|
|
|
206
202
|
),
|
|
207
203
|
path: route.path,
|
|
208
204
|
loader: async () => { // request
|
|
209
|
-
// wait for container/exposedStoreData to be setup
|
|
205
|
+
// wait for container/exposedStoreData to be setup (note that this causes ReactRouter to re-render, but not the page)
|
|
210
206
|
if (!nonce) {
|
|
211
207
|
nonce = true
|
|
212
208
|
await setTimeoutPromise(() => {}, 0)
|
|
@@ -255,11 +251,9 @@ function RestoreScroll() {
|
|
|
255
251
|
|
|
256
252
|
function RouteComponent({ route, config }: { route: Route, config: Config }) {
|
|
257
253
|
const Component = route.component
|
|
258
|
-
const params = useParams()
|
|
259
|
-
const location = useLocation()
|
|
260
254
|
document.title = route.meta?.title || ''
|
|
261
255
|
return (
|
|
262
|
-
<Component route={route}
|
|
256
|
+
<Component route={route} config={config} />
|
|
263
257
|
)
|
|
264
258
|
}
|
|
265
259
|
|
|
@@ -106,7 +106,7 @@ async function store(req, res) {
|
|
|
106
106
|
async function signup(req, res) {
|
|
107
107
|
try {
|
|
108
108
|
const desktop = req.query.desktop
|
|
109
|
-
let user = await this.userCreate(req.body
|
|
109
|
+
let user = await this.userCreate(req.body)
|
|
110
110
|
sendEmail({
|
|
111
111
|
config: authConfig,
|
|
112
112
|
template: 'welcome',
|
|
@@ -315,8 +315,12 @@ export async function signinAndGetStore(user, isDesktop, getStore) {
|
|
|
315
315
|
return { ...store, jwt }
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
export async function userCreate({ name, business, email, password
|
|
318
|
+
export async function userCreate({ name, business, email, password }) {
|
|
319
319
|
try {
|
|
320
|
+
if (!this.findUserFromProvider) {
|
|
321
|
+
throw new Error('this.findUserFromProvider doesn\'t exist, make sure the context is available when calling this function')
|
|
322
|
+
}
|
|
323
|
+
|
|
320
324
|
const options = { blacklist: ['-_id'] }
|
|
321
325
|
const isMultiTenant = !authConfig.isNotMultiTenant
|
|
322
326
|
const userId = db.id()
|
|
@@ -14,14 +14,14 @@ type DropdownProps = {
|
|
|
14
14
|
options?: { label: string|React.ReactNode, onClick?: Function, isSelected?: boolean, icon?: React.ReactNode, className?: string }[]
|
|
15
15
|
/** Whether the dropdown is hoverable **/
|
|
16
16
|
isHoverable?: boolean
|
|
17
|
-
/** The minimum width of the menu **/
|
|
18
|
-
minWidth?: number | string
|
|
19
17
|
/** The content to render inside the top of the dropdown **/
|
|
20
18
|
menuContent?: React.ReactNode
|
|
21
19
|
menuClassName?: string
|
|
22
20
|
menuOptionClassName?: string
|
|
23
21
|
menuIsOpen?: boolean
|
|
24
22
|
menuToggles?: boolean
|
|
23
|
+
/** The minimum width of the menu **/
|
|
24
|
+
minWidth?: number | string
|
|
25
25
|
toggleCallback?: (isActive: boolean) => void
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -30,15 +30,15 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
30
30
|
animate=true,
|
|
31
31
|
children,
|
|
32
32
|
className,
|
|
33
|
-
dir,
|
|
33
|
+
dir='bottom-left',
|
|
34
34
|
options,
|
|
35
35
|
isHoverable,
|
|
36
|
-
minWidth, // remove in favour of menuClassName
|
|
37
36
|
menuClassName,
|
|
38
37
|
menuOptionClassName,
|
|
39
38
|
menuContent,
|
|
40
39
|
menuIsOpen,
|
|
41
40
|
menuToggles=true,
|
|
41
|
+
minWidth,
|
|
42
42
|
toggleCallback,
|
|
43
43
|
}: DropdownProps, ref) {
|
|
44
44
|
// https://letsbuildui.dev/articles/building-a-dropdown-menu-component-with-react-hooks
|
|
@@ -46,6 +46,8 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
46
46
|
const dropdownRef = useRef<HTMLDivElement|null>(null)
|
|
47
47
|
const [isActive, setIsActive] = useState(!!menuIsOpen)
|
|
48
48
|
const menuStyle = getSelectStyle({ name: 'menu' })
|
|
49
|
+
const [direction, setDirection] = useState<null | 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'>(null)
|
|
50
|
+
const [ready, setReady] = useState(false)
|
|
49
51
|
|
|
50
52
|
// Expose the setIsActive function to the parent component
|
|
51
53
|
useImperativeHandle(ref, () => ({ setIsActive }))
|
|
@@ -76,7 +78,56 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
76
78
|
useEffect(() => {
|
|
77
79
|
if (toggleCallback) toggleCallback(isActive)
|
|
78
80
|
}, [isActive])
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
setReady(false)
|
|
84
|
+
console.log(1111, isActive, dropdownRef.current)
|
|
85
|
+
if (!isActive || !dropdownRef.current) return
|
|
86
|
+
|
|
87
|
+
// const ul = dropdownRef.current.querySelector('&>ul') as HTMLElement
|
|
88
|
+
// if (!ul) return
|
|
89
|
+
// console.log(1111)
|
|
90
|
+
|
|
91
|
+
// Temporarily show the ul for measurement
|
|
92
|
+
// const originalMaxHeight = ul.style.maxHeight
|
|
93
|
+
// const originalVisibility = ul.style.visibility
|
|
94
|
+
// const originalOpacity = ul.style.opacity
|
|
95
|
+
// const originalPointerEvents = ul.style.pointerEvents
|
|
96
|
+
|
|
97
|
+
// ul.style.maxHeight = 'none'
|
|
98
|
+
// ul.style.visibility = 'hidden'
|
|
99
|
+
// ul.style.opacity = '0'
|
|
100
|
+
// ul.style.pointerEvents = 'none'
|
|
101
|
+
|
|
102
|
+
// const dropdownHeight = ul.getBoundingClientRect().height
|
|
79
103
|
|
|
104
|
+
// Revert styles
|
|
105
|
+
// ul.style.maxHeight = originalMaxHeight
|
|
106
|
+
// ul.style.visibility = originalVisibility
|
|
107
|
+
// ul.style.opacity = originalOpacity
|
|
108
|
+
// ul.style.pointerEvents = originalPointerEvents
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
// const rect = dropdownRef.current.getBoundingClientRect()
|
|
112
|
+
// const spaceBelow = window.innerHeight - rect.bottom
|
|
113
|
+
// const spaceAbove = rect.top
|
|
114
|
+
|
|
115
|
+
// const side = dir.endsWith('right') ? 'right' : 'left'
|
|
116
|
+
|
|
117
|
+
// const newDirection = dir.startsWith('bottom')
|
|
118
|
+
// ? `${1 < 1 && 1 > 1 ? 'top' : 'bottom'}-${side}`
|
|
119
|
+
// : `${1 < 1 && 1 > 1 ? 'bottom' : 'top'}-${side}`
|
|
120
|
+
|
|
121
|
+
// setDirection(dir as 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right')
|
|
122
|
+
|
|
123
|
+
// requestAnimationFrame(() => {
|
|
124
|
+
// // console.log('ul', originalOpacity)
|
|
125
|
+
// setReady(true)
|
|
126
|
+
// })
|
|
127
|
+
setDirection(dir)
|
|
128
|
+
setReady(true)
|
|
129
|
+
}, [isActive, dir])
|
|
130
|
+
|
|
80
131
|
function onMouseDown(e: { key: string, preventDefault: Function }) {
|
|
81
132
|
if (e.key && e.key != 'Enter') return
|
|
82
133
|
if (e.key) e.preventDefault() // for button, stops buttons firing twice
|
|
@@ -91,8 +142,8 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
91
142
|
return (
|
|
92
143
|
<div
|
|
93
144
|
class={
|
|
94
|
-
|
|
95
|
-
(
|
|
145
|
+
`relative is-${direction || dir}` + // until hovered, show the original direction to prevent scrollbars
|
|
146
|
+
(ready ? ' is-ready' : '') +
|
|
96
147
|
(isHoverable ? ' is-hoverable' : '') +
|
|
97
148
|
(isActive ? ' is-active' : '') +
|
|
98
149
|
(!animate ? ' no-animation' : '') +
|
|
@@ -113,7 +164,8 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
113
164
|
}
|
|
114
165
|
<ul
|
|
115
166
|
style={{ minWidth }}
|
|
116
|
-
class={
|
|
167
|
+
class={
|
|
168
|
+
twMerge(`${menuStyle} absolute invisible opacity-0 select-none min-w-full z-[1] ${menuClassName||''}`)}
|
|
117
169
|
>
|
|
118
170
|
{menuContent}
|
|
119
171
|
{
|
|
@@ -146,7 +198,7 @@ const style = css`
|
|
|
146
198
|
}
|
|
147
199
|
&.is-bottom-right,
|
|
148
200
|
&.is-top-right {
|
|
149
|
-
ul {
|
|
201
|
+
&>ul {
|
|
150
202
|
left: auto;
|
|
151
203
|
right: 0;
|
|
152
204
|
}
|
|
@@ -166,6 +218,7 @@ const style = css`
|
|
|
166
218
|
}
|
|
167
219
|
}
|
|
168
220
|
// active submenu
|
|
221
|
+
&./////////////
|
|
169
222
|
&.is-hoverable:hover,
|
|
170
223
|
&:focus,
|
|
171
224
|
&.is-active,
|
|
@@ -119,9 +119,10 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
|
|
|
119
119
|
|
|
120
120
|
return (
|
|
121
121
|
<Elements.Dropdown
|
|
122
|
+
// menuIsOpen={true}
|
|
123
|
+
className='ddddddddd'
|
|
122
124
|
dir="bottom-right"
|
|
123
125
|
allowOverflow={true}
|
|
124
|
-
// menuIsOpen={true}
|
|
125
126
|
{...dropdownProps}
|
|
126
127
|
menuClassName={twMerge(`min-w-[330px] ${dropdownProps?.menuClassName || ''}`)}
|
|
127
128
|
menuContent={
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import GithubIcon from 'nitro-web/client/imgs/github.svg'
|
|
2
2
|
|
|
3
3
|
export function GithubLink({ filename }: { filename: string }) {
|
|
4
|
-
const base = 'https://github.com/boycce/nitro-web/blob/master/'
|
|
4
|
+
const base = 'https://github.com/boycce/nitro-web/blob/master/packages/'
|
|
5
5
|
// Filenames are relative to the webpack start directory
|
|
6
6
|
// 1. Remove ../ from filename (i.e. for _example build)
|
|
7
7
|
// 2. Remove node_modules/nitro-web/ from filename (i.e. for packages using nitro-web)
|
|
@@ -1,121 +1,146 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { twMerge, deepFind, getErrorFromState } from 'nitro-web/util'
|
|
3
|
+
import { Errors, type Error } from 'nitro-web/types'
|
|
2
4
|
|
|
3
|
-
type CheckboxProps = {
|
|
5
|
+
type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
6
|
+
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' */
|
|
4
7
|
name: string
|
|
5
|
-
/** name is applied if not provided. Used for radios */
|
|
8
|
+
/** name is applied if id is not provided. Used for radios */
|
|
6
9
|
id?: string
|
|
7
|
-
|
|
10
|
+
/** state object to get the value, and check errors against */
|
|
11
|
+
state?: { errors?: Errors, [key: string]: any }
|
|
12
|
+
size?: number
|
|
8
13
|
subtext?: string|React.ReactNode
|
|
9
14
|
text?: string|React.ReactNode
|
|
10
15
|
type?: 'checkbox' | 'radio' | 'toggle'
|
|
11
|
-
|
|
16
|
+
checkboxClassName?: string
|
|
17
|
+
svgClassName?: string
|
|
18
|
+
labelClassName?: string
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
export function Checkbox({
|
|
21
|
+
export function Checkbox({
|
|
22
|
+
state, size, subtext, text, type='checkbox', className, checkboxClassName, svgClassName, labelClassName, ...props
|
|
23
|
+
}: CheckboxProps) {
|
|
15
24
|
// Checkbox/radio/toggle component
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
id = id || name
|
|
25
|
+
let value!: boolean
|
|
26
|
+
const error = getErrorFromState(state, props.name)
|
|
27
|
+
const id = props.id || props.name
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
md: {
|
|
28
|
-
checkbox: 'size-[16px]',
|
|
29
|
-
toggleWidth: 'w-[40px]', // 4px border + (toggleAfterSize * 2)
|
|
30
|
-
toggleHeight: 'h-[22px]',
|
|
31
|
-
toggleAfterSize: 'after:size-[18px]', // account for 2px border
|
|
32
|
-
},
|
|
29
|
+
if (!props.name) throw new Error('Checkbox requires a `name` prop')
|
|
30
|
+
|
|
31
|
+
// Value: Input is always controlled if state is passed in
|
|
32
|
+
if (typeof props.checked !== 'undefined') value = props.checked
|
|
33
|
+
else if (typeof state == 'object') {
|
|
34
|
+
const v = deepFind(state, props.name) as boolean | undefined
|
|
35
|
+
value = v ?? false
|
|
33
36
|
}
|
|
34
|
-
|
|
37
|
+
|
|
38
|
+
const BORDER = 2
|
|
39
|
+
const checkboxSize = size ?? 14
|
|
40
|
+
const toggleHeight = size ?? 18
|
|
41
|
+
const toggleWidth = toggleHeight * 2 - BORDER * 2
|
|
42
|
+
const toggleAfterSize = toggleHeight - BORDER * 2
|
|
35
43
|
|
|
36
44
|
return (
|
|
37
|
-
<div
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
45
|
+
<div
|
|
46
|
+
className={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after text-sm nitro-checkbox ${className}`)}
|
|
47
|
+
>
|
|
48
|
+
<div className="flex gap-3 items-baseline">
|
|
49
|
+
<div className="shrink-0 flex items-center">
|
|
50
|
+
<div className="w-0"> </div>
|
|
51
|
+
<div className="group relative">
|
|
52
|
+
{
|
|
53
|
+
type !== 'toggle'
|
|
54
|
+
? <>
|
|
55
|
+
<input
|
|
56
|
+
{...props}
|
|
57
|
+
type={type}
|
|
58
|
+
style={{ width: checkboxSize, height: checkboxSize }}
|
|
59
|
+
checked={value}
|
|
60
|
+
className={
|
|
61
|
+
twMerge(
|
|
62
|
+
`${type === 'radio' ? 'rounded-full' : 'rounded'} appearance-none border border-gray-300 bg-white forced-colors:appearance-auto disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ` +
|
|
63
|
+
// Variable-selected theme colors (was .*-blue-600)
|
|
64
|
+
'checked:border-variable-selected checked:bg-variable-selected indeterminate:border-variable-selected indeterminate:bg-variable-selected focus-visible:outline-variable-selected ' +
|
|
65
|
+
// Dark mode not used yet... dark:focus-visible:outline-blue-800
|
|
66
|
+
checkboxClassName
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
/>
|
|
70
|
+
<svg
|
|
71
|
+
fill="none"
|
|
72
|
+
viewBox="0 0 14 14"
|
|
73
|
+
style={{ width: checkboxSize, height: checkboxSize }}
|
|
74
|
+
className={twMerge('absolute top-0 left-0 pointer-events-none justify-self-center stroke-white group-has-[:disabled]:stroke-gray-950/25', svgClassName)}
|
|
75
|
+
>
|
|
76
|
+
{
|
|
77
|
+
type === 'radio'
|
|
78
|
+
? <circle
|
|
79
|
+
// cx={(_size.checkbox.match(/\d+/)?.[0] as unknown as number) / 2}
|
|
80
|
+
// cy={(_size.checkbox.match(/\d+/)?.[0] as unknown as number) / 2}
|
|
81
|
+
// r={(_size.checkbox.match(/\d+/)?.[0] as unknown as number) / 6}
|
|
82
|
+
cx={7}
|
|
83
|
+
cy={7}
|
|
84
|
+
r={2.5}
|
|
85
|
+
className="fill-white opacity-0 group-has-[:checked]:opacity-100"
|
|
86
|
+
/>
|
|
87
|
+
: <>
|
|
88
|
+
<path
|
|
89
|
+
d="M4 8L6 10L10 4.5"
|
|
90
|
+
strokeWidth={2}
|
|
91
|
+
strokeLinecap="round"
|
|
92
|
+
strokeLinejoin="round"
|
|
93
|
+
className="opacity-0 group-has-[:checked]:opacity-100"
|
|
94
|
+
/>
|
|
95
|
+
<path
|
|
96
|
+
d="M4 7H10"
|
|
97
|
+
strokeWidth={2}
|
|
98
|
+
strokeLinecap="round"
|
|
99
|
+
strokeLinejoin="round"
|
|
100
|
+
className="opacity-0 group-has-[:indeterminate]:opacity-100"
|
|
101
|
+
/>
|
|
102
|
+
</>
|
|
103
|
+
}
|
|
104
|
+
</svg>
|
|
105
|
+
</>
|
|
106
|
+
: <>
|
|
107
|
+
<input
|
|
108
|
+
{...props}
|
|
109
|
+
type="checkbox"
|
|
110
|
+
className="sr-only peer"
|
|
111
|
+
checked={value}
|
|
112
|
+
/>
|
|
113
|
+
<label
|
|
114
|
+
for={id}
|
|
115
|
+
style={{ width: toggleWidth, height: toggleHeight }}
|
|
116
|
+
className={
|
|
117
|
+
twMerge(
|
|
118
|
+
'block bg-gray-200 rounded-full transition-colors peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 ' +
|
|
119
|
+
// Variable-selected theme colors (was .*-blue-600)
|
|
120
|
+
'peer-checked:bg-variable-selected peer-focus-visible:outline-variable-selected ' +
|
|
121
|
+
labelClassName
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
<span
|
|
126
|
+
style={{ width: toggleAfterSize, height: toggleAfterSize }}
|
|
127
|
+
className={
|
|
128
|
+
'absolute top-[2px] start-[2px] bg-white border-gray-300 border rounded-full transition-all group-has-[:checked]:border-white group-has-[:checked]:translate-x-full '
|
|
129
|
+
}
|
|
130
|
+
/>
|
|
131
|
+
</label>
|
|
132
|
+
</>
|
|
133
|
+
}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{text &&
|
|
137
|
+
<label for={id} className="text-[length:inherit] leading-[inherit] select-none">
|
|
138
|
+
<span className="text-gray-900">{text}</span>
|
|
139
|
+
<span className="ml-2 text-gray-500">{subtext}</span>
|
|
140
|
+
</label>
|
|
111
141
|
}
|
|
112
142
|
</div>
|
|
113
|
-
{
|
|
114
|
-
<label for={id} className="self-center text-sm select-none">
|
|
115
|
-
<span className="text-gray-900">{text}</span>
|
|
116
|
-
<span className="ml-2 text-gray-500">{subtext}</span>
|
|
117
|
-
</label>
|
|
118
|
-
}
|
|
143
|
+
{error && <div class="mt-1.5 text-xs text-danger-foreground nitro-error">{error.detail}</div>}
|
|
119
144
|
</div>
|
|
120
145
|
)
|
|
121
146
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
//
|
|
2
|
+
// fill-current tw class for lucide icons (https://github.com/lucide-icons/lucide/discussions/458)
|
|
3
3
|
import { css } from 'twin.macro'
|
|
4
4
|
import { FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps } from 'nitro-web'
|
|
5
5
|
import { twMerge, getErrorFromState, deepFind } from 'nitro-web/util'
|
|
@@ -22,7 +22,7 @@ type FieldExtraProps = {
|
|
|
22
22
|
/** icon to show in the input */
|
|
23
23
|
icon?: React.ReactNode
|
|
24
24
|
iconPos?: 'left' | 'right'
|
|
25
|
-
/**
|
|
25
|
+
/** Dependencies to break the implicit memoization of onChange/onInputChange */
|
|
26
26
|
deps?: unknown[]
|
|
27
27
|
placeholder?: string
|
|
28
28
|
}
|
|
@@ -181,25 +181,19 @@ function ColorSvg({ hex }: { hex?: string }) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
export function isFieldCached(prev: IsFieldCachedProps, next: IsFieldCachedProps) {
|
|
184
|
+
// Check if the field is cached, onChange/onInputChange doesn't affect the cache
|
|
184
185
|
const path = prev.name
|
|
185
186
|
const state = prev.state || {}
|
|
186
|
-
// If state
|
|
187
|
-
if (deepFind(state, path) !== deepFind(next.state || {}, path))
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if ((next.deps?.length !== prev.deps?.length) || next.deps?.some((v, i) => v !== prev.deps?.[i])) {
|
|
197
|
-
// console.log(3, 'deps changed', path)
|
|
198
|
-
return false
|
|
199
|
-
}
|
|
200
|
-
// If any other props have changed, except onChange/onInputChange, re-render!
|
|
201
|
-
// In most cases, onChange/onInputChange remain the same and reference the same function...
|
|
202
|
-
for (const k in prev) {
|
|
187
|
+
// If the state value has changed, re-render!
|
|
188
|
+
if (deepFind(state, path) !== deepFind(next.state || {}, path)) return false
|
|
189
|
+
// If the state error has changed, re-render!
|
|
190
|
+
if (getErrorFromState(state, path) !== getErrorFromState(next.state || {}, path)) return false
|
|
191
|
+
// If `deps` have changed, handy for onChange/onInputChange, re-render!
|
|
192
|
+
if ((next.deps?.length !== prev.deps?.length) || next.deps?.some((v, i) => v !== prev.deps?.[i])) return false
|
|
193
|
+
|
|
194
|
+
// Check if any prop has changed, except `onChange`/`onInputChange`
|
|
195
|
+
const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)])
|
|
196
|
+
for (const k of allKeys) {
|
|
203
197
|
if (k === 'state' || k === 'onChange' || k === 'onInputChange') continue
|
|
204
198
|
if (prev[k as keyof typeof prev] !== next[k as keyof typeof next]) {
|
|
205
199
|
// console.log(4, 'changed', path, k)
|
|
@@ -204,18 +204,21 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
|
|
|
204
204
|
<div class="grid grid-cols-3 gap-x-6 mb-4">
|
|
205
205
|
<div>
|
|
206
206
|
<label for="input2">Label</label>
|
|
207
|
-
<Checkbox name="input2" type="toggle" text="Toggle sm" subtext="some additional text here." class="!mb-0"
|
|
208
|
-
|
|
207
|
+
<Checkbox name="input2" type="toggle" text="Toggle sm" subtext="some additional text here." class="!mb-0"
|
|
208
|
+
state={state} onChange={(e) => onChange(setState, e)} />
|
|
209
|
+
<Checkbox name="input3" type="toggle" text="Toggle 22px" subtext="some additional text here." size={22} />
|
|
209
210
|
</div>
|
|
210
211
|
<div>
|
|
211
212
|
<label for="input1">Label</label>
|
|
212
|
-
<Checkbox name="input1" type="radio" text="Radio
|
|
213
|
+
<Checkbox name="input1" type="radio" text="Radio" subtext="some additional text here 1." id="input1-1" class="!mb-0"
|
|
213
214
|
defaultChecked />
|
|
214
|
-
<Checkbox name="input1" type="radio" text="Radio
|
|
215
|
+
<Checkbox name="input1" type="radio" text="Radio 16px" subtext="some additional text here 2." id="input1-2" size={16} />
|
|
215
216
|
</div>
|
|
216
217
|
<div>
|
|
217
218
|
<label for="input0">Label</label>
|
|
218
|
-
<Checkbox name="input0" type="checkbox" text="Checkbox" subtext="some additional text here." defaultChecked />
|
|
219
|
+
<Checkbox name="input0" type="checkbox" text="Checkbox" subtext="some additional text here." class="!mb-0" defaultChecked />
|
|
220
|
+
<Checkbox name="input0.1" type="checkbox" text="Checkbox 16px" size={16}
|
|
221
|
+
subtext="some additional text here which is a bit longer that will be line-wrap to the next line." />
|
|
219
222
|
</div>
|
|
220
223
|
</div>
|
|
221
224
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.75",
|
|
4
4
|
"repository": "github:boycce/nitro-web",
|
|
5
5
|
"homepage": "https://boycce.github.io/nitro-web/",
|
|
6
6
|
"description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",
|