bbannotate 1.0.0__py3-none-any.whl
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.
- bbannotate-1.0.0.dist-info/METADATA +182 -0
- bbannotate-1.0.0.dist-info/RECORD +21 -0
- bbannotate-1.0.0.dist-info/WHEEL +4 -0
- bbannotate-1.0.0.dist-info/entry_points.txt +2 -0
- bbannotate-1.0.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +4 -0
- src/api/__init__.py +5 -0
- src/api/routes.py +339 -0
- src/cli.py +263 -0
- src/frontend_dist/assets/index-DFfgJzXo.css +1 -0
- src/frontend_dist/assets/index-DezlWFbC.js +89 -0
- src/frontend_dist/index.html +20 -0
- src/frontend_dist/logo.png +0 -0
- src/main.py +90 -0
- src/models/__init__.py +21 -0
- src/models/annotations.py +62 -0
- src/py.typed +0 -0
- src/services/__init__.py +6 -0
- src/services/annotation_service.py +307 -0
- src/services/export_service.py +488 -0
- src/services/project_service.py +203 -0
src/cli.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Command-line interface for bbannotate."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import webbrowser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from src import __version__
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="bbannotate",
|
|
17
|
+
help="Bounding box annotation tool for image datasets.",
|
|
18
|
+
add_completion=False,
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
)
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def version_callback(value: bool) -> None:
|
|
25
|
+
"""Print version and exit."""
|
|
26
|
+
if value:
|
|
27
|
+
console.print(f"[bold blue]bbannotate[/bold blue] version {__version__}")
|
|
28
|
+
raise typer.Exit()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.callback()
|
|
32
|
+
def main_callback(
|
|
33
|
+
version: Annotated[
|
|
34
|
+
bool | None,
|
|
35
|
+
typer.Option(
|
|
36
|
+
"--version",
|
|
37
|
+
"-v",
|
|
38
|
+
help="Show version and exit.",
|
|
39
|
+
callback=version_callback,
|
|
40
|
+
is_eager=True,
|
|
41
|
+
),
|
|
42
|
+
] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Bbannotate - Bounding box annotation tool for image datasets."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command()
|
|
49
|
+
def start(
|
|
50
|
+
host: Annotated[
|
|
51
|
+
str,
|
|
52
|
+
typer.Option("--host", "-h", help="Host to bind the server to."),
|
|
53
|
+
] = "127.0.0.1",
|
|
54
|
+
port: Annotated[
|
|
55
|
+
int,
|
|
56
|
+
typer.Option("--port", "-p", help="Port to bind the server to."),
|
|
57
|
+
] = 8000,
|
|
58
|
+
no_browser: Annotated[
|
|
59
|
+
bool,
|
|
60
|
+
typer.Option("--no-browser", help="Don't open browser automatically."),
|
|
61
|
+
] = False,
|
|
62
|
+
reload: Annotated[
|
|
63
|
+
bool,
|
|
64
|
+
typer.Option("--reload", "-r", help="Enable auto-reload for development."),
|
|
65
|
+
] = False,
|
|
66
|
+
data_dir: Annotated[
|
|
67
|
+
Path | None,
|
|
68
|
+
typer.Option(
|
|
69
|
+
"--data-dir",
|
|
70
|
+
"-d",
|
|
71
|
+
help="Directory for storing data (defaults to ./data).",
|
|
72
|
+
),
|
|
73
|
+
] = None,
|
|
74
|
+
projects_dir: Annotated[
|
|
75
|
+
Path | None,
|
|
76
|
+
typer.Option(
|
|
77
|
+
"--projects-dir",
|
|
78
|
+
help="Directory for storing projects (defaults to ./projects).",
|
|
79
|
+
),
|
|
80
|
+
] = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Start the bbannotate annotation server.
|
|
83
|
+
|
|
84
|
+
Launches the FastAPI backend server and optionally opens a browser.
|
|
85
|
+
The frontend is served from the built assets if available.
|
|
86
|
+
"""
|
|
87
|
+
import os
|
|
88
|
+
|
|
89
|
+
# Set environment variables for configuration
|
|
90
|
+
if data_dir:
|
|
91
|
+
os.environ["BBANNOTATE_DATA_DIR"] = str(data_dir.resolve())
|
|
92
|
+
if projects_dir:
|
|
93
|
+
os.environ["BBANNOTATE_PROJECTS_DIR"] = str(projects_dir.resolve())
|
|
94
|
+
|
|
95
|
+
# Check if frontend is built
|
|
96
|
+
frontend_dist = _find_frontend_dist()
|
|
97
|
+
if frontend_dist is None:
|
|
98
|
+
console.print(
|
|
99
|
+
Panel(
|
|
100
|
+
"[yellow]Frontend not found.[/yellow]\n\n"
|
|
101
|
+
"The frontend assets are not built. Run:\n"
|
|
102
|
+
" [bold]bbannotate build-frontend[/bold]\n\n"
|
|
103
|
+
"Or start the frontend dev server separately:\n"
|
|
104
|
+
" [bold]cd frontend && npm run dev[/bold]",
|
|
105
|
+
title="⚠️ Frontend Missing",
|
|
106
|
+
border_style="yellow",
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
url = f"http://{host}:{port}"
|
|
111
|
+
|
|
112
|
+
console.print(
|
|
113
|
+
Panel(
|
|
114
|
+
f"[bold green]Starting bbannotate server[/bold green]\n\n"
|
|
115
|
+
f" URL: [link={url}]{url}[/link]\n"
|
|
116
|
+
f" Host: {host}\n"
|
|
117
|
+
f" Port: {port}\n"
|
|
118
|
+
f" Reload: {'enabled' if reload else 'disabled'}",
|
|
119
|
+
title="🚀 Bbannotate",
|
|
120
|
+
border_style="blue",
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Open browser if requested
|
|
125
|
+
if not no_browser:
|
|
126
|
+
webbrowser.open(url)
|
|
127
|
+
|
|
128
|
+
# Start uvicorn
|
|
129
|
+
import uvicorn
|
|
130
|
+
|
|
131
|
+
uvicorn.run(
|
|
132
|
+
"src.main:app",
|
|
133
|
+
host=host,
|
|
134
|
+
port=port,
|
|
135
|
+
reload=reload,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.command()
|
|
140
|
+
def build_frontend() -> None:
|
|
141
|
+
"""Build the frontend assets.
|
|
142
|
+
|
|
143
|
+
Runs npm install and npm run build in the frontend directory.
|
|
144
|
+
"""
|
|
145
|
+
frontend_dir = _find_frontend_src()
|
|
146
|
+
|
|
147
|
+
if frontend_dir is None:
|
|
148
|
+
console.print(
|
|
149
|
+
"[red]Error:[/red] Frontend source directory not found.\n"
|
|
150
|
+
"Make sure you're running from the project root or have "
|
|
151
|
+
"the frontend directory in your installation.",
|
|
152
|
+
style="bold red",
|
|
153
|
+
)
|
|
154
|
+
raise typer.Exit(1)
|
|
155
|
+
|
|
156
|
+
console.print(f"[blue]Building frontend in {frontend_dir}...[/blue]")
|
|
157
|
+
|
|
158
|
+
# Check for npm
|
|
159
|
+
try:
|
|
160
|
+
subprocess.run(
|
|
161
|
+
["npm", "--version"],
|
|
162
|
+
check=True,
|
|
163
|
+
capture_output=True,
|
|
164
|
+
)
|
|
165
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
166
|
+
console.print(
|
|
167
|
+
"[red]Error:[/red] npm not found. Please install Node.js and npm.",
|
|
168
|
+
style="bold red",
|
|
169
|
+
)
|
|
170
|
+
raise typer.Exit(1) from None
|
|
171
|
+
|
|
172
|
+
# Install dependencies
|
|
173
|
+
console.print("[blue]Installing npm dependencies...[/blue]")
|
|
174
|
+
result = subprocess.run(
|
|
175
|
+
["npm", "install"],
|
|
176
|
+
cwd=frontend_dir,
|
|
177
|
+
capture_output=False,
|
|
178
|
+
)
|
|
179
|
+
if result.returncode != 0:
|
|
180
|
+
console.print("[red]Error:[/red] npm install failed.", style="bold red")
|
|
181
|
+
raise typer.Exit(1)
|
|
182
|
+
|
|
183
|
+
# Build
|
|
184
|
+
console.print("[blue]Building frontend...[/blue]")
|
|
185
|
+
result = subprocess.run(
|
|
186
|
+
["npm", "run", "build"],
|
|
187
|
+
cwd=frontend_dir,
|
|
188
|
+
capture_output=False,
|
|
189
|
+
)
|
|
190
|
+
if result.returncode != 0:
|
|
191
|
+
console.print("[red]Error:[/red] npm build failed.", style="bold red")
|
|
192
|
+
raise typer.Exit(1)
|
|
193
|
+
|
|
194
|
+
console.print("[green]✓ Frontend built successfully![/green]")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@app.command()
|
|
198
|
+
def info() -> None:
|
|
199
|
+
"""Show information about the current installation."""
|
|
200
|
+
frontend_status = "Found" if _find_frontend_dist() else "Not built"
|
|
201
|
+
frontend_src = "Found" if _find_frontend_src() else "Not found"
|
|
202
|
+
console.print(
|
|
203
|
+
Panel(
|
|
204
|
+
f"[bold blue]bbannotate[/bold blue] v{__version__}\n\n"
|
|
205
|
+
f"[bold]Python:[/bold] {sys.version}\n"
|
|
206
|
+
f"[bold]Frontend:[/bold] {frontend_status}\n"
|
|
207
|
+
f"[bold]Frontend Source:[/bold] {frontend_src}",
|
|
208
|
+
title="ℹ️ Installation Info",
|
|
209
|
+
border_style="blue",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _find_frontend_dist() -> Path | None:
|
|
215
|
+
"""Find the frontend dist directory.
|
|
216
|
+
|
|
217
|
+
Checks in order:
|
|
218
|
+
1. Bundled with package (src/frontend_dist) - for pip install
|
|
219
|
+
2. Relative to package (frontend/dist) - for development
|
|
220
|
+
3. Current working directory (frontend/dist) - for development
|
|
221
|
+
"""
|
|
222
|
+
# Check bundled location (pip install includes frontend_dist in src/)
|
|
223
|
+
package_dir = Path(__file__).parent
|
|
224
|
+
bundled_path = package_dir / "frontend_dist"
|
|
225
|
+
if bundled_path.exists() and (bundled_path / "index.html").exists():
|
|
226
|
+
return bundled_path
|
|
227
|
+
|
|
228
|
+
# Check relative to package root (development mode with frontend/dist)
|
|
229
|
+
dev_path = package_dir.parent / "frontend" / "dist"
|
|
230
|
+
if dev_path.exists() and (dev_path / "index.html").exists():
|
|
231
|
+
return dev_path
|
|
232
|
+
|
|
233
|
+
# Check current working directory
|
|
234
|
+
cwd_dist = Path.cwd() / "frontend" / "dist"
|
|
235
|
+
if cwd_dist.exists() and (cwd_dist / "index.html").exists():
|
|
236
|
+
return cwd_dist
|
|
237
|
+
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _find_frontend_src() -> Path | None:
|
|
242
|
+
"""Find the frontend source directory."""
|
|
243
|
+
# Check relative to package
|
|
244
|
+
package_dir = Path(__file__).parent.parent
|
|
245
|
+
frontend_path = package_dir / "frontend"
|
|
246
|
+
if (frontend_path / "package.json").exists():
|
|
247
|
+
return frontend_path
|
|
248
|
+
|
|
249
|
+
# Check current working directory
|
|
250
|
+
cwd_frontend = Path.cwd() / "frontend"
|
|
251
|
+
if (cwd_frontend / "package.json").exists():
|
|
252
|
+
return cwd_frontend
|
|
253
|
+
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main() -> None:
|
|
258
|
+
"""Entry point for the CLI."""
|
|
259
|
+
app()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-3{left:.75rem}.right-2{right:.5rem}.top-2{top:.5rem}.top-3{top:.75rem}.z-10{z-index:10}.z-50{z-index:50}.float-right{float:right}.-mx-3{margin-left:-.75rem;margin-right:-.75rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.-mt-4{margin-top:-1rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:24rem}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-2\.5{width:.625rem}.w-32{width:8rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-96{width:24rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[48px\]{min-width:48px}.min-w-\[80px\]{min-width:80px}.max-w-2xl{max-width:42rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-primary-500{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-300{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-primary-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity, 1))}.bg-primary-50{--tw-bg-opacity: 1;background-color:rgb(240 249 255 / var(--tw-bg-opacity, 1))}.bg-primary-600{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-primary-600{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-primary-500{--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity, 1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}body{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb{border-radius:.25rem;--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.hover\:border-primary-500:hover{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-primary-50:hover{--tw-bg-opacity: 1;background-color:rgb(240 249 255 / var(--tw-bg-opacity, 1))}.hover\:bg-primary-700:hover{--tw-bg-opacity: 1;background-color:rgb(3 105 161 / var(--tw-bg-opacity, 1))}.hover\:bg-red-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:text-gray-600:hover{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.hover\:text-red-600:hover{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:border-primary-500:focus{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.focus\:ring-primary-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity, 1))}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:block{display:block}.dark\:divide-gray-700:is(.dark *)>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(55 65 81 / var(--tw-divide-opacity, 1))}.dark\:border-gray-600:is(.dark *){--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.dark\:border-gray-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.dark\:border-primary-400:is(.dark *){--tw-border-opacity: 1;border-color:rgb(56 189 248 / var(--tw-border-opacity, 1))}.dark\:bg-amber-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(120 53 15 / var(--tw-bg-opacity, 1))}.dark\:bg-blue-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-600:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-700:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-700\/50:is(.dark *){background-color:#37415180}.dark\:bg-gray-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.dark\:bg-green-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.dark\:bg-primary-900\/20:is(.dark *){background-color:#0c4a6e33}.dark\:bg-primary-900\/30:is(.dark *){background-color:#0c4a6e4d}.dark\:bg-red-900\/20:is(.dark *){background-color:#7f1d1d33}.dark\:bg-red-900\/30:is(.dark *){background-color:#7f1d1d4d}.dark\:text-amber-400:is(.dark *){--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.dark\:text-blue-400:is(.dark *){--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.dark\:text-gray-100:is(.dark *){--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.dark\:text-gray-200:is(.dark *){--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.dark\:text-gray-300:is(.dark *){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:text-gray-400:is(.dark *){--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.dark\:text-gray-500:is(.dark *){--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.dark\:text-green-400:is(.dark *){--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.dark\:text-red-400:is(.dark *){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.dark\:text-white:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity, 1))}.dark\:placeholder-gray-400:is(.dark *)::placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity, 1))}.dark\:hover\:border-primary-500:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.dark\:hover\:bg-gray-600:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-700:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-700\/50:hover:is(.dark *){background-color:#37415180}.dark\:hover\:bg-primary-900\/20:hover:is(.dark *){background-color:#0c4a6e33}.dark\:hover\:bg-red-900\/20:hover:is(.dark *){background-color:#7f1d1d33}.dark\:hover\:bg-red-900\/30:hover:is(.dark *){background-color:#7f1d1d4d}.dark\:hover\:text-gray-300:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:hover\:text-red-400:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}
|