nitro-web 0.0.87 → 0.0.89
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/index.ts +0 -3
- package/components/auth/auth.api.js +411 -0
- package/components/auth/reset.tsx +86 -0
- package/components/auth/signin.tsx +76 -0
- package/components/auth/signup.tsx +62 -0
- package/components/billing/stripe.api.js +268 -0
- package/components/dashboard/dashboard.tsx +32 -0
- package/components/partials/element/accordion.tsx +102 -0
- package/components/partials/element/avatar.tsx +40 -0
- package/components/partials/element/button.tsx +98 -0
- package/components/partials/element/calendar.tsx +125 -0
- package/components/partials/element/dropdown.tsx +248 -0
- package/components/partials/element/filters.tsx +194 -0
- package/components/partials/element/github-link.tsx +16 -0
- package/components/partials/element/initials.tsx +66 -0
- package/components/partials/element/message.tsx +141 -0
- package/components/partials/element/modal.tsx +90 -0
- package/components/partials/element/sidebar.tsx +195 -0
- package/components/partials/element/tooltip.tsx +154 -0
- package/components/partials/element/topbar.tsx +15 -0
- package/components/partials/form/checkbox.tsx +150 -0
- package/components/partials/form/drop-handler.tsx +68 -0
- package/components/partials/form/drop.tsx +141 -0
- package/components/partials/form/field-color.tsx +86 -0
- package/components/partials/form/field-currency.tsx +158 -0
- package/components/partials/form/field-date.tsx +252 -0
- package/components/partials/form/field.tsx +231 -0
- package/components/partials/form/form-error.tsx +27 -0
- package/components/partials/form/location.tsx +225 -0
- package/components/partials/form/select.tsx +360 -0
- package/components/partials/is-first-render.ts +14 -0
- package/components/partials/not-found.tsx +7 -0
- package/components/partials/styleguide.tsx +407 -0
- package/package.json +2 -1
- package/types/globals.d.ts +0 -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"> </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
|
+
}
|