nitro-web 0.0.87 → 0.0.88

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 (33) hide show
  1. package/components/auth/auth.api.js +411 -0
  2. package/components/auth/reset.tsx +86 -0
  3. package/components/auth/signin.tsx +76 -0
  4. package/components/auth/signup.tsx +62 -0
  5. package/components/billing/stripe.api.js +268 -0
  6. package/components/dashboard/dashboard.tsx +32 -0
  7. package/components/partials/element/accordion.tsx +102 -0
  8. package/components/partials/element/avatar.tsx +40 -0
  9. package/components/partials/element/button.tsx +98 -0
  10. package/components/partials/element/calendar.tsx +125 -0
  11. package/components/partials/element/dropdown.tsx +248 -0
  12. package/components/partials/element/filters.tsx +194 -0
  13. package/components/partials/element/github-link.tsx +16 -0
  14. package/components/partials/element/initials.tsx +66 -0
  15. package/components/partials/element/message.tsx +141 -0
  16. package/components/partials/element/modal.tsx +90 -0
  17. package/components/partials/element/sidebar.tsx +195 -0
  18. package/components/partials/element/tooltip.tsx +154 -0
  19. package/components/partials/element/topbar.tsx +15 -0
  20. package/components/partials/form/checkbox.tsx +150 -0
  21. package/components/partials/form/drop-handler.tsx +68 -0
  22. package/components/partials/form/drop.tsx +141 -0
  23. package/components/partials/form/field-color.tsx +86 -0
  24. package/components/partials/form/field-currency.tsx +158 -0
  25. package/components/partials/form/field-date.tsx +252 -0
  26. package/components/partials/form/field.tsx +231 -0
  27. package/components/partials/form/form-error.tsx +27 -0
  28. package/components/partials/form/location.tsx +225 -0
  29. package/components/partials/form/select.tsx +360 -0
  30. package/components/partials/is-first-render.ts +14 -0
  31. package/components/partials/not-found.tsx +7 -0
  32. package/components/partials/styleguide.tsx +407 -0
  33. package/package.json +2 -1
@@ -0,0 +1,90 @@
1
+ import { IsFirstRender, twMerge } from 'nitro-web'
2
+ import SvgX1 from 'nitro-web/client/imgs/icons/x1.svg'
3
+
4
+ type ModalProps = {
5
+ show: boolean
6
+ setShow: (show: boolean) => void
7
+ children: React.ReactNode
8
+ className?: string
9
+ rootClassName?: string
10
+ dismissable?: boolean
11
+ maxWidth?: string
12
+ minHeight?: string
13
+ [key: string]: unknown
14
+ }
15
+
16
+ export function Modal({ show, setShow, children, maxWidth, minHeight, dismissable = true, className, rootClassName }: ModalProps) {
17
+ const [state, setState] = useState(show ? 'open' : 'close')
18
+ const containerEl = useRef<HTMLDivElement>(null)
19
+ const isFirst = IsFirstRender()
20
+
21
+ const states = {
22
+ 'close': {
23
+ root: 'left-[-100vw] transition-[left] duration-0 delay-200',
24
+ bg: 'opacity-0',
25
+ container: 'opacity-0 scale-[0.97]',
26
+ },
27
+ 'close-now': {
28
+ root: '',
29
+ bg: '',
30
+ container: 'opacity-0 !transition-none',
31
+ },
32
+ 'open': {
33
+ root: 'left-0 transition-none model-open',
34
+ bg: 'opacity-100 duration-200',
35
+ container: 'opacity-100 scale-[1] duration-200',
36
+ },
37
+ }
38
+ const stateObj = states[state as keyof typeof states]
39
+
40
+ useEffect(() => {
41
+ if (isFirst) return
42
+ if (show) {
43
+ setState('open')
44
+ } else {
45
+ setTimeout(() => {
46
+ // If another modal is being opened, force close the container for a smoother transition
47
+ if (document.getElementsByClassName('modal-open').length > 1) {
48
+ setState('close-now')
49
+ } else {
50
+ setState('close')
51
+ }
52
+ }, 10)
53
+ }
54
+ // There is a bug during hot-reloading where the modal does't open if we don't ensure
55
+ // the same truthy/falsey type is used.
56
+ }, [!!show])
57
+
58
+ function onClick(e: React.MouseEvent) {
59
+ const clickedOnModal = containerEl.current && containerEl.current.contains(e.target as Node)
60
+ if (!clickedOnModal && dismissable) {
61
+ setShow(false)
62
+ }
63
+ }
64
+
65
+ return (
66
+ <div
67
+ onClick={(e) => e.stopPropagation()}
68
+ class={`${twMerge(`fixed top-0 w-[100vw] h-[100vh] z-[100] ${stateObj.root} ${rootClassName||''}`)} nitro-modal`}
69
+ >
70
+ <div class={`!absolute inset-0 box-content bg-gray-500/70 transition-opacity ${stateObj.bg}`}></div>
71
+ <div class={`relative h-[100vh] overflow-y-auto transition-[opacity,transform] ${stateObj.container}`}>
72
+ <div class="flex items-center justify-center min-h-full" onMouseDown={onClick}>
73
+ <div
74
+ ref={containerEl}
75
+ style={{ maxWidth: maxWidth || '550px', minHeight: minHeight }}
76
+ class={`relative w-full mx-6 mt-4 mb-8 bg-white rounded-lg shadow-lg p-9 ${className||''}`}
77
+ >
78
+ <div
79
+ class="absolute top-0 right-0 p-3 m-1 cursor-pointer"
80
+ onClick={() => { if (dismissable) { setShow(false) }}}
81
+ >
82
+ <SvgX1 />
83
+ </div>
84
+ {children}
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1,195 @@
1
+ // Component: https://tailwindui.com/components/application-ui/application-shells/sidebar#component-a69d85b6237ea2ad506c00ef1cd39a38
2
+ import { css } from 'twin.macro'
3
+ import avatarImg from 'nitro-web/client/imgs/avatar.jpg'
4
+ import { injectedConfig } from 'nitro-web'
5
+ import {
6
+ Bars3Icon,
7
+ HomeIcon,
8
+ UsersIcon,
9
+ ArrowLeftCircleIcon,
10
+ PaintBrushIcon,
11
+ } from '@heroicons/react/24/outline'
12
+ import { XIcon } from 'lucide-react'
13
+
14
+ const sidebarWidth = 'w-80'
15
+
16
+ export type SidebarProps = {
17
+ Logo?: React.FC<{ width?: string, height?: string }>;
18
+ menu?: { name: string; to: string; Icon: React.FC<{ className?: string }> }[]
19
+ links?: { name: string; to: string; initial: string }[]
20
+ }
21
+
22
+ function classNames(...classes: string[]) {
23
+ return classes.filter(Boolean).join(' ')
24
+ }
25
+
26
+ export function Sidebar({ Logo, menu, links }: SidebarProps) {
27
+ const [sidebarOpen, setSidebarOpen] = useState(false)
28
+ return (
29
+ <>
30
+ {/* desktop sidebar */}
31
+ <div css={style} className={
32
+ 'fixed inset-y-0 z-50 flex flex-col ease-in-out lg:left-0 lg:translate-x-0 lg:!delay-0 lg:!duration-0 ' +
33
+ (
34
+ sidebarOpen
35
+ ? 'left-0 translate-x-[0px] sidebar-transition '
36
+ : 'left-[-100%] translate-x-[-100%] sidebar-transition-delay '
37
+ ) +
38
+ sidebarWidth
39
+ }>
40
+ <div className={
41
+ 'absolute left-full top-0 flex w-16 justify-center pt-5 lg:hidden duration-300 ease ' +
42
+ (sidebarOpen ? 'opacity-100' : 'opacity-0')
43
+ }>
44
+ <button type="button" onClick={() => setSidebarOpen(false)} className="-m-2.5 p-2.5">
45
+ <XIcon aria-hidden="true" strokeWidth={1.5} size={24} className="text-white" />
46
+ </button>
47
+ </div>
48
+ <SidebarContents Logo={Logo} menu={menu} links={links} />
49
+ </div>
50
+
51
+ {/* mobile backdrop */}
52
+ <div
53
+ css={style}
54
+ onClick={() => setSidebarOpen(false)}
55
+ className={'fixed w-full z-[49] inset-0 bg-gray-900/70 ease-linear lg:hidden ' +
56
+ (
57
+ sidebarOpen
58
+ ? 'left-0 opacity-100 sidebar-transition '
59
+ : 'left-[-100%] opacity-0 sidebar-transition-delay '
60
+ )
61
+ }
62
+ />
63
+
64
+ {/* mobile sidebar topbar */}
65
+ <div className="sticky top-0 z-40 flex items-center gap-x-6 bg-white px-4 py-4 shadow-sm sm:px-6 lg:hidden">
66
+ <button type="button" onClick={() => setSidebarOpen(true)} className="-m-2.5 p-2.5 text-gray-700 lg:hidden">
67
+ <Bars3Icon aria-hidden="true" className="size-6" />
68
+ </button>
69
+ <div className="flex-1 text-sm/6 font-semibold text-gray-900">Dashboard</div>
70
+ <Link to="#">
71
+ <img alt="" src={avatarImg} className="size-8 rounded-full bg-gray-50" />
72
+ </Link>
73
+ </div>
74
+
75
+ <div class={`${sidebarWidth}`} />
76
+ </>
77
+ )
78
+ }
79
+
80
+ function SidebarContents ({ Logo, menu, links }: SidebarProps) {
81
+ const location = useLocation()
82
+ const [store] = useTracked()
83
+ const user = store.user
84
+
85
+ function isActive(path: string) {
86
+ if (path == '/' && location.pathname == path) return 'is-active'
87
+ else if (path != '/' && location.pathname.match(`^${path}`)) return 'is-active'
88
+ else return ''
89
+ }
90
+
91
+ const _menu = menu || [
92
+ { name: 'Dashboard', to: '/', Icon: HomeIcon },
93
+ { name: injectedConfig.isDemo ? 'Design System' : 'Style Guide', to: '/styleguide', Icon: PaintBrushIcon },
94
+ { name: 'Pricing', to: '/pricing', Icon: UsersIcon },
95
+ { name: 'Signout', to: '/signout', Icon: ArrowLeftCircleIcon },
96
+ ]
97
+
98
+ const _links = links || [
99
+ { name: 'Nitro on Github', to: 'https://github.com/boycce/nitro-web', initial: 'G' },
100
+ ]
101
+
102
+ // Sidebar component, swap this element with another sidebar if you like
103
+ return (
104
+ <div className="flex grow flex-col gap-y-8 overflow-y-auto bg-white py-5 px-10 lg:border-r lg:border-gray-200">
105
+ {Logo && (
106
+ <div className="flex h-16 shrink-0 items-center gap-2 justify-bedtween">
107
+ <Link to="/">
108
+ <Logo width="70" height={undefined} />
109
+ </Link>
110
+ <span className="text-[9px] text-gray-900 font-semibold mt-4">{injectedConfig.version}</span>
111
+ </div>
112
+ )}
113
+ <nav className="flex flex-1 flex-col">
114
+ <ul role="list" className="flex flex-1 flex-col gap-y-7">
115
+ <li>
116
+ <ul role="list" className="-mx-2 space-y-1">
117
+ {_menu.map((item) => (
118
+ <li key={item.name}>
119
+ <Link
120
+ to={item.to}
121
+ className={classNames(
122
+ isActive(item.to)
123
+ ? 'bg-gray-50 text-indigo-600'
124
+ : 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
125
+ 'group flex gap-x-3 items-center rounded-md p-2 text-md/6 font-semibold'
126
+ )}
127
+ >
128
+ { item.Icon &&
129
+ <item.Icon
130
+ className={classNames(
131
+ isActive(item.to) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-600',
132
+ 'size-5 shrink-0'
133
+ )}
134
+ />
135
+ }
136
+ {item.name}
137
+ </Link>
138
+ </li>
139
+ ))}
140
+ </ul>
141
+ </li>
142
+ <li>
143
+ <div className="text-xs/6 font-semibold text-gray-400">Other Links</div>
144
+ <ul role="list" className="-mx-2 mt-2 space-y-1">
145
+ {_links.map((team) => (
146
+ <li key={team.name}>
147
+ <Link
148
+ to={team.to}
149
+ className={classNames(
150
+ isActive(team.to)
151
+ ? 'bg-gray-50 text-indigo-600'
152
+ : 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
153
+ 'group flex gap-x-3 rounded-md p-2 text-md/6 font-semibold'
154
+ )}
155
+ >
156
+ <span
157
+ className={classNames(
158
+ isActive(team.to)
159
+ ? 'border-indigo-600 text-indigo-600'
160
+ : 'border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600',
161
+ 'flex size-6 shrink-0 items-center justify-center rounded-lg border bg-white text-[0.625rem] font-medium'
162
+ )}
163
+ >
164
+ {team.initial}
165
+ </span>
166
+ <span className="truncate">{team.name}</span>
167
+ </Link>
168
+ </li>
169
+ ))}
170
+ </ul>
171
+ </li>
172
+
173
+ <li className="-mx-6 mt-auto hidden lg:block">
174
+ <Link
175
+ to="#"
176
+ className="flex items-center gap-x-4 px-6 py-3 text-sm/6 font-semibold text-gray-900 hover:bg-gray-50"
177
+ >
178
+ <img alt="" src={avatarImg} className="size-8 rounded-full bg-gray-50" />
179
+ <span aria-hidden="true" class="truncate1 flex-1">{user?.name || 'Guest'}</span>
180
+ </Link>
181
+ </li>
182
+ </ul>
183
+ </nav>
184
+ </div>
185
+ )
186
+ }
187
+
188
+ const style = css`
189
+ &.sidebar-transition-delay {
190
+ transition: transform 300ms, opacity 300ms, left 0ms 300ms;
191
+ }
192
+ &.sidebar-transition {
193
+ transition: transform 300ms, opacity 300ms, left 0ms 0ms;
194
+ }
195
+ `
@@ -0,0 +1,154 @@
1
+ // todo: finish tailwind conversion
2
+ import { css } from 'twin.macro'
3
+
4
+ type TooltipProps = {
5
+ children: React.ReactNode
6
+ className?: string
7
+ classNamePopup?: string
8
+ isSmall?: boolean
9
+ text?: React.ReactNode
10
+ }
11
+
12
+ export function Tooltip({ text, children, className, classNamePopup, isSmall }: TooltipProps) {
13
+ return (
14
+ <div class={`${className} relative inline-block align-middle nitro-tooltip`} css={style}>
15
+ {
16
+ text
17
+ ? <>
18
+ <div class="tooltip-trigger ">{children}</div>
19
+ <div class={`tooltip-popup ${classNamePopup||''} ${isSmall ? 'is-small' : ''}`}>{text}</div>
20
+ </>
21
+ : children
22
+ }
23
+ </div>
24
+ )
25
+ }
26
+
27
+ const style = css`
28
+ .tooltip-popup {
29
+ position: absolute;
30
+ display: block;
31
+ margin-top: -10000px;
32
+ width: 200px;
33
+ padding: 14px;
34
+ font-weight: 400;
35
+ font-size: 11.5px;
36
+ line-height: 1.3;
37
+ letter-spacing: 0.5px;
38
+ text-align: center;
39
+ border-radius: 6px;
40
+ background: black;
41
+ color: white;
42
+ opacity: 0;
43
+ transition: opacity 0.15s ease, transform 0.15s ease, margin-top 0s 0.15s;
44
+ white-space: break-spaces;
45
+ overflow-wrap: break-word;
46
+ pointer-events: none;
47
+ z-index: 9999;
48
+ &:after {
49
+ content: '';
50
+ position: absolute;
51
+ border-width: 6px;
52
+ border-style: solid;
53
+ }
54
+ // Variation
55
+ &.is-small {
56
+ width: 160px;
57
+ padding: 10px;
58
+ font-size: 11px;
59
+ }
60
+ // Positions
61
+ &.is-top,
62
+ &.is-top-left,
63
+ &:not(.is-top-left):not(.is-left):not(.is-right):not(.is-bottom):not(.is-bottom-left) {
64
+ bottom: 100%;
65
+ left: 50%;
66
+ transform: translateX(-50%) translateY(-15px);
67
+ &:after {
68
+ top: 100%;
69
+ left: 50%;
70
+ margin-left: -6px;
71
+ border-color: black transparent transparent transparent;
72
+ }
73
+ &.is-top-left {
74
+ left: 0px;
75
+ transform: translateX(0%) translateY(-15px);
76
+ &:after {
77
+ left: 28px;
78
+ }
79
+ }
80
+ }
81
+ &.is-bottom,
82
+ &.is-bottom-left {
83
+ top: 100%;
84
+ left: 50%;
85
+ transform: translateX(-50%) translateY(15px) ;
86
+ &:after {
87
+ bottom: 100%;
88
+ left: 50%;
89
+ margin-left: -6px;
90
+ border-color: transparent transparent black transparent;
91
+ }
92
+ &.is-bottom-left {
93
+ left: 0px;
94
+ transform: translateX(0%) translateY(15px);
95
+ &:after {
96
+ left: 28px;
97
+ }
98
+ }
99
+ }
100
+ &.is-left {
101
+ top: 50%;
102
+ right: 100%;
103
+ transform: translateX(-15px) translateY(-50%);
104
+ &:after {
105
+ top: 50%;
106
+ right: -12px;
107
+ margin-top: -6px;
108
+ border-color: transparent transparent transparent black;
109
+ }
110
+ }
111
+ &.is-right {
112
+ top: 50%;
113
+ left: 100%;
114
+ transform: translateX(15px) translateY(-50%);
115
+ &:after {
116
+ top: 50%;
117
+ left: -12px;
118
+ margin-top: -6px;
119
+ border-color: transparent black transparent transparent;
120
+ }
121
+ }
122
+ }
123
+ .tooltip-trigger {
124
+ /* trigger can come before tooltip-popup or wrap it */
125
+ position: relative !important;
126
+ &:hover .tooltip-popup,
127
+ &:hover + .tooltip-popup,
128
+ .tooltip-popup.is-active,
129
+ & + .tooltip-popup.is-active {
130
+ opacity: 1;
131
+ margin-top: 0;
132
+ transition: opacity 0.15s ease, transform 0.15s ease, margin-top 0s 0s;
133
+ &.is-top,
134
+ &:not(.is-top-left):not(.is-left):not(.is-right):not(.is-bottom):not(.is-bottom-left) {
135
+ transform: translateX(-50%) translateY(-10px);
136
+ }
137
+ &.is-top-left {
138
+ transform: translateX(0%) translateY(-10px);
139
+ }
140
+ &.is-bottom {
141
+ transform: translateX(-50%) translateY(10px);
142
+ }
143
+ &.is-bottom-left {
144
+ transform: translateX(0%) translateY(10px);
145
+ }
146
+ &.is-left {
147
+ transform: translateX(-10px) translateY(-50%);
148
+ }
149
+ &.is-right {
150
+ transform: translateX(10px) translateY(-50%);
151
+ }
152
+ }
153
+ }
154
+ `
@@ -0,0 +1,15 @@
1
+ type TopbarProps = {
2
+ title: React.ReactNode
3
+ subtitle?: React.ReactNode
4
+ className?: string
5
+ }
6
+
7
+ export function Topbar({ title, subtitle, className }: TopbarProps) {
8
+ return (
9
+ <div class={`flex flex-col min-h-12 gap-0.5 mb-6 nitro-topbar ${className||''}`}>
10
+ <div class="text-2xl font-bold">{title}</div>
11
+ { subtitle && <div class="text-sm text-muted-foreground">{subtitle}</div>}
12
+ {/* { submenu && <div class="pt-2 text-large weight-500">{submenu}</div> } */}
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,150 @@
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'
4
+
5
+ type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement> & {
6
+ /** field name or path on state (used to match errors), e.g. 'date', 'company.email' */
7
+ name: string
8
+ /** name is applied if id is not provided. Used for radios */
9
+ id?: string
10
+ /** state object to get the value, and check errors against */
11
+ state?: { errors?: Errors, [key: string]: any }
12
+ size?: number
13
+ subtext?: string|React.ReactNode
14
+ text?: string|React.ReactNode
15
+ type?: 'checkbox' | 'radio' | 'toggle'
16
+ checkboxClassName?: string
17
+ svgClassName?: string
18
+ labelClassName?: string
19
+ /** title used to find related error messages */
20
+ errorTitle?: string|RegExp
21
+ }
22
+
23
+ export function Checkbox({
24
+ state, size, subtext, text, type='checkbox', className, checkboxClassName, svgClassName, labelClassName, errorTitle, ...props
25
+ }: CheckboxProps) {
26
+ // Checkbox/radio/toggle component
27
+ let value!: boolean
28
+ const error = getErrorFromState(state, errorTitle || props.name)
29
+ const id = props.id || props.name
30
+
31
+ if (!props.name) throw new Error('Checkbox requires a `name` prop')
32
+
33
+ // Value: Input is always controlled if state is passed in
34
+ if (typeof props.checked !== 'undefined') value = props.checked
35
+ else if (typeof state == 'object') {
36
+ const v = deepFind(state, props.name) as boolean | undefined
37
+ value = v ?? false
38
+ }
39
+
40
+ const BORDER = 2
41
+ const checkboxSize = size ?? 14
42
+ const toggleHeight = size ?? 18
43
+ const toggleWidth = toggleHeight * 2 - BORDER * 2
44
+ const toggleAfterSize = toggleHeight - BORDER * 2
45
+
46
+ return (
47
+ <div
48
+ className={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after text-sm nitro-checkbox ${className}`)}
49
+ >
50
+ <div className="flex gap-3 items-baseline">
51
+ <div className="shrink-0 flex items-center">
52
+ <div className="w-0">&nbsp;</div>
53
+ <div className="group relative">
54
+ {
55
+ type !== 'toggle'
56
+ ? <>
57
+ <input
58
+ {...props}
59
+ id={id}
60
+ type={type}
61
+ style={{ width: checkboxSize, height: checkboxSize }}
62
+ checked={value}
63
+ className={
64
+ twMerge(
65
+ `${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 ` +
66
+ // Variable-selected theme colors (was .*-blue-600)
67
+ 'checked:border-variable-selected checked:bg-variable-selected indeterminate:border-variable-selected indeterminate:bg-variable-selected focus-visible:outline-variable-selected ' +
68
+ // Dark mode not used yet... dark:focus-visible:outline-blue-800
69
+ checkboxClassName
70
+ )
71
+ }
72
+ />
73
+ <svg
74
+ fill="none"
75
+ viewBox="0 0 14 14"
76
+ style={{ width: checkboxSize, height: checkboxSize }}
77
+ className={twMerge('absolute top-0 left-0 pointer-events-none justify-self-center stroke-white group-has-[:disabled]:stroke-gray-950/25', svgClassName)}
78
+ >
79
+ {
80
+ type === 'radio'
81
+ ? <circle
82
+ // cx={(_size.checkbox.match(/\d+/)?.[0] as unknown as number) / 2}
83
+ // cy={(_size.checkbox.match(/\d+/)?.[0] as unknown as number) / 2}
84
+ // r={(_size.checkbox.match(/\d+/)?.[0] as unknown as number) / 6}
85
+ cx={7}
86
+ cy={7}
87
+ r={2.5}
88
+ className="fill-white opacity-0 group-has-[:checked]:opacity-100"
89
+ />
90
+ : <>
91
+ <path
92
+ d="M4 8L6 10L10 4.5"
93
+ strokeWidth={2}
94
+ strokeLinecap="round"
95
+ strokeLinejoin="round"
96
+ className="opacity-0 group-has-[:checked]:opacity-100"
97
+ />
98
+ <path
99
+ d="M4 7H10"
100
+ strokeWidth={2}
101
+ strokeLinecap="round"
102
+ strokeLinejoin="round"
103
+ className="opacity-0 group-has-[:indeterminate]:opacity-100"
104
+ />
105
+ </>
106
+ }
107
+ </svg>
108
+ </>
109
+ : <>
110
+ <input
111
+ {...props}
112
+ id={id}
113
+ type="checkbox"
114
+ className="sr-only peer"
115
+ checked={value}
116
+ />
117
+ <label
118
+ for={id}
119
+ style={{ width: toggleWidth, height: toggleHeight }}
120
+ className={
121
+ twMerge(
122
+ 'block bg-gray-200 rounded-full transition-colors peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 ' +
123
+ // Variable-selected theme colors (was .*-blue-600)
124
+ 'peer-checked:bg-variable-selected peer-focus-visible:outline-variable-selected ' +
125
+ labelClassName
126
+ )
127
+ }
128
+ >
129
+ <span
130
+ style={{ width: toggleAfterSize, height: toggleAfterSize }}
131
+ className={
132
+ '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 '
133
+ }
134
+ />
135
+ </label>
136
+ </>
137
+ }
138
+ </div>
139
+ </div>
140
+ {text &&
141
+ <label for={id} className="text-[length:inherit] leading-[inherit] select-none">
142
+ <span className="text-gray-900">{text}</span>
143
+ <span className="ml-2 text-gray-500">{subtext}</span>
144
+ </label>
145
+ }
146
+ </div>
147
+ {error && <div class="mt-1.5 text-xs text-danger-foreground nitro-error">{error.detail}</div>}
148
+ </div>
149
+ )
150
+ }
@@ -0,0 +1,68 @@
1
+ type DropHandlerProps = {
2
+ onDrop: (files: FileList) => void
3
+ children: React.ReactNode
4
+ className?: string
5
+ }
6
+
7
+ export const DropHandler = ({ onDrop, children, className }: DropHandlerProps) => {
8
+ const dropRef = useRef<HTMLDivElement>(null)
9
+ let dragCounter = useRef(0).current
10
+ const [dragging, setDragging] = useState(false)
11
+
12
+ useEffect(() => {
13
+ const div = dropRef.current
14
+ div?.addEventListener('dragenter', handleDragIn)
15
+ div?.addEventListener('dragleave', handleDragOut)
16
+ div?.addEventListener('dragover', handleDragOver)
17
+ div?.addEventListener('drop', handleDrop)
18
+ return () => {
19
+ div?.removeEventListener('dragenter', handleDragIn)
20
+ div?.removeEventListener('dragleave', handleDragOut)
21
+ div?.removeEventListener('dragover', handleDragOver)
22
+ div?.removeEventListener('drop', handleDrop)
23
+ }
24
+ }, [])
25
+
26
+ const handleDragIn = (e: DragEvent) => {
27
+ e.preventDefault()
28
+ e.stopPropagation()
29
+ dragCounter++
30
+ if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
31
+ setDragging(true)
32
+ }
33
+ }
34
+
35
+ const handleDragOut = (e: DragEvent) => {
36
+ e.preventDefault()
37
+ e.stopPropagation()
38
+ dragCounter--
39
+ if (dragCounter === 0) {
40
+ setDragging(false)
41
+ }
42
+ }
43
+
44
+ const handleDragOver = (e: DragEvent) => {
45
+ e.preventDefault()
46
+ e.stopPropagation()
47
+ }
48
+
49
+ const handleDrop = (e: DragEvent) => {
50
+ e.preventDefault()
51
+ e.stopPropagation()
52
+ setDragging(false)
53
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
54
+ onDrop(e.dataTransfer.files)
55
+ // e.dataTransfer.clearData() // causes an error in firefox
56
+ dragCounter = 0
57
+ }
58
+ }
59
+
60
+ return (
61
+ <div
62
+ ref={dropRef}
63
+ class={`${className} relative w-full p-[20px] border-2 border-dashed border-input-border rounded-md ${dragging ? 'border-primary before:content-[""] before:absolute before:inset-0 before:bg-primary before:opacity-5' : ''}`}
64
+ >
65
+ {children}
66
+ </div>
67
+ )
68
+ }