fastapi-admin-kit 0.1.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.
- fastapi_admin_kit/__init__.py +73 -0
- fastapi_admin_kit/actions/__init__.py +63 -0
- fastapi_admin_kit/actions/base.py +68 -0
- fastapi_admin_kit/actions/registry.py +43 -0
- fastapi_admin_kit/admin/__init__.py +17 -0
- fastapi_admin_kit/admin/admin_config.py +95 -0
- fastapi_admin_kit/admin/admin_database.py +138 -0
- fastapi_admin_kit/admin/admin_router.py +74 -0
- fastapi_admin_kit/admin/admin_template.py +203 -0
- fastapi_admin_kit/admin/builtin_models.py +284 -0
- fastapi_admin_kit/admin/core.py +1036 -0
- fastapi_admin_kit/admin/decorators.py +70 -0
- fastapi_admin_kit/admin/state.py +76 -0
- fastapi_admin_kit/admin.py +728 -0
- fastapi_admin_kit/api/__init__.py +44 -0
- fastapi_admin_kit/api/auth.py +342 -0
- fastapi_admin_kit/api/crud.py +128 -0
- fastapi_admin_kit/api/deps.py +79 -0
- fastapi_admin_kit/api/roles.py +128 -0
- fastapi_admin_kit/api/schema_generator.py +171 -0
- fastapi_admin_kit/api/schemas.py +81 -0
- fastapi_admin_kit/api/search.py +132 -0
- fastapi_admin_kit/audit/__init__.py +36 -0
- fastapi_admin_kit/audit/context.py +62 -0
- fastapi_admin_kit/audit/diff.py +77 -0
- fastapi_admin_kit/audit/event_bus.py +96 -0
- fastapi_admin_kit/audit/events.py +48 -0
- fastapi_admin_kit/audit/listener.py +159 -0
- fastapi_admin_kit/audit/logger.py +28 -0
- fastapi_admin_kit/audit/middleware.py +39 -0
- fastapi_admin_kit/audit/models.py +53 -0
- fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
- fastapi_admin_kit/auth/__init__.py +34 -0
- fastapi_admin_kit/auth/backend.py +95 -0
- fastapi_admin_kit/auth/csrf.py +240 -0
- fastapi_admin_kit/auth/dependencies.py +150 -0
- fastapi_admin_kit/auth/identity.py +181 -0
- fastapi_admin_kit/auth/models.py +246 -0
- fastapi_admin_kit/auth/password.py +35 -0
- fastapi_admin_kit/auth/permissions.py +205 -0
- fastapi_admin_kit/auth/protocol.py +22 -0
- fastapi_admin_kit/auth/ratelimit.py +88 -0
- fastapi_admin_kit/auth/router.py +10 -0
- fastapi_admin_kit/auth/session.py +79 -0
- fastapi_admin_kit/auth/totp.py +83 -0
- fastapi_admin_kit/auth/views.py +165 -0
- fastapi_admin_kit/cli.py +229 -0
- fastapi_admin_kit/config/__init__.py +19 -0
- fastapi_admin_kit/config/audit.py +18 -0
- fastapi_admin_kit/config/auth.py +54 -0
- fastapi_admin_kit/config/behavior.py +27 -0
- fastapi_admin_kit/config/nav.py +32 -0
- fastapi_admin_kit/config/storage.py +22 -0
- fastapi_admin_kit/config/theme.py +215 -0
- fastapi_admin_kit/config/ui.py +147 -0
- fastapi_admin_kit/dashboard/__init__.py +64 -0
- fastapi_admin_kit/db.py +133 -0
- fastapi_admin_kit/exceptions.py +5 -0
- fastapi_admin_kit/field_types.py +81 -0
- fastapi_admin_kit/filters/__init__.py +21 -0
- fastapi_admin_kit/filters/base.py +170 -0
- fastapi_admin_kit/filters/registry.py +68 -0
- fastapi_admin_kit/flash.py +45 -0
- fastapi_admin_kit/form/__init__.py +1 -0
- fastapi_admin_kit/form/pipeline.py +106 -0
- fastapi_admin_kit/inspection/__init__.py +117 -0
- fastapi_admin_kit/inspection/registry.py +253 -0
- fastapi_admin_kit/inspection.py +115 -0
- fastapi_admin_kit/modeladmin.py +375 -0
- fastapi_admin_kit/models/__init__.py +7 -0
- fastapi_admin_kit/models/base.py +7 -0
- fastapi_admin_kit/nav.py +208 -0
- fastapi_admin_kit/pagination/__init__.py +14 -0
- fastapi_admin_kit/pagination/base.py +40 -0
- fastapi_admin_kit/pagination/cursor.py +97 -0
- fastapi_admin_kit/pagination/dynamic.py +48 -0
- fastapi_admin_kit/pagination/offset.py +42 -0
- fastapi_admin_kit/plugins/__init__.py +1 -0
- fastapi_admin_kit/py.typed +0 -0
- fastapi_admin_kit/registry/__init__.py +5 -0
- fastapi_admin_kit/registry/core.py +287 -0
- fastapi_admin_kit/registry/validation.py +107 -0
- fastapi_admin_kit/registry.py +15 -0
- fastapi_admin_kit/router.py +335 -0
- fastapi_admin_kit/static/css/admin.css +4736 -0
- fastapi_admin_kit/static/css/presets.css +317 -0
- fastapi_admin_kit/static/css/tokens.css +217 -0
- fastapi_admin_kit/static/css/variables.css +74 -0
- fastapi_admin_kit/static/icons/heroicons.svg +160 -0
- fastapi_admin_kit/static/js/admin.js +692 -0
- fastapi_admin_kit/static/js/htmx-config.js +42 -0
- fastapi_admin_kit/storage/__init__.py +6 -0
- fastapi_admin_kit/storage/base.py +48 -0
- fastapi_admin_kit/storage/local.py +73 -0
- fastapi_admin_kit/templates/base.html +142 -0
- fastapi_admin_kit/templates/macros/form_fields.html +660 -0
- fastapi_admin_kit/templates/macros/icons.html +50 -0
- fastapi_admin_kit/templates/macros/table.html +108 -0
- fastapi_admin_kit/templates/macros/widgets.html +159 -0
- fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
- fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
- fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
- fastapi_admin_kit/templates/pages/audit_log.html +102 -0
- fastapi_admin_kit/templates/pages/dashboard.html +295 -0
- fastapi_admin_kit/templates/pages/detail.html +183 -0
- fastapi_admin_kit/templates/pages/form.html +119 -0
- fastapi_admin_kit/templates/pages/list.html +277 -0
- fastapi_admin_kit/templates/pages/login.html +85 -0
- fastapi_admin_kit/templates/pages/profile/password.html +78 -0
- fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
- fastapi_admin_kit/templates/pages/role_form.html +75 -0
- fastapi_admin_kit/templates/pages/roles/form.html +117 -0
- fastapi_admin_kit/templates/pages/roles/list.html +69 -0
- fastapi_admin_kit/templates/pages/roles.html +77 -0
- fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
- fastapi_admin_kit/templates/pages/users/form.html +229 -0
- fastapi_admin_kit/templates/pages/users/list.html +83 -0
- fastapi_admin_kit/templates/partials/command_palette.html +52 -0
- fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
- fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
- fastapi_admin_kit/templates/partials/head.html +21 -0
- fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
- fastapi_admin_kit/templates/partials/list_table.html +178 -0
- fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
- fastapi_admin_kit/templates/partials/pagination.html +82 -0
- fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
- fastapi_admin_kit/templates/partials/scripts.html +13 -0
- fastapi_admin_kit/templates/partials/sidebar.html +94 -0
- fastapi_admin_kit/templates/partials/topbar.html +95 -0
- fastapi_admin_kit/types.py +145 -0
- fastapi_admin_kit/validation.py +43 -0
- fastapi_admin_kit/views/__init__.py +78 -0
- fastapi_admin_kit/views/audit.py +134 -0
- fastapi_admin_kit/views/bulk.py +28 -0
- fastapi_admin_kit/views/class_views.py +1040 -0
- fastapi_admin_kit/views/context.py +588 -0
- fastapi_admin_kit/views/dashboard.py +162 -0
- fastapi_admin_kit/views/delete.py +31 -0
- fastapi_admin_kit/views/extra.py +65 -0
- fastapi_admin_kit/views/factory.py +667 -0
- fastapi_admin_kit/views/form.py +159 -0
- fastapi_admin_kit/views/list.py +28 -0
- fastapi_admin_kit/views/profile.py +219 -0
- fastapi_admin_kit/views/protocols.py +54 -0
- fastapi_admin_kit/views/renderers.py +634 -0
- fastapi_admin_kit/views/roles.py +230 -0
- fastapi_admin_kit/views/search.py +31 -0
- fastapi_admin_kit/views/settings.py +31 -0
- fastapi_admin_kit/views/sidebar.py +101 -0
- fastapi_admin_kit/views/totp.py +249 -0
- fastapi_admin_kit/views/users.py +347 -0
- fastapi_admin_kit/views.py +117 -0
- fastapi_admin_kit/widgets/__init__.py +44 -0
- fastapi_admin_kit/widgets/base.py +44 -0
- fastapi_admin_kit/widgets/inputs.py +363 -0
- fastapi_admin_kit/widgets/registry.py +110 -0
- fastapi_admin_kit/widgets/relation.py +70 -0
- fastapi_admin_kit/widgets/resolver.py +102 -0
- fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
- fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
- fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
- fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
- fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
FastAPI Admin Kit — HTMX Configuration
|
|
3
|
+
═══════════════════════════════════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
6
|
+
|
|
7
|
+
/* ── Default swap style ──────────────────────────────────────────────── */
|
|
8
|
+
|
|
9
|
+
if (typeof htmx !== 'undefined') {
|
|
10
|
+
htmx.config.defaultSwapStyle = 'outerHTML';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ── CSRF / HTMX header injection ────────────────────────────────────── */
|
|
14
|
+
|
|
15
|
+
document.body.addEventListener('htmx:configRequest', (event) => {
|
|
16
|
+
/* Read CSRF token from meta tag if present */
|
|
17
|
+
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
18
|
+
if (meta) {
|
|
19
|
+
event.detail.headers['X-CSRF-Token'] = meta.getAttribute('content');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Always send the HTMX marker so the server can distinguish HTMX
|
|
23
|
+
requests from full-page navigations. */
|
|
24
|
+
event.detail.headers['HX-Request'] = 'true';
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/* ── Show/hide loading bar on HTMX events ────────────────────────────── */
|
|
28
|
+
|
|
29
|
+
const loadingBar = document.getElementById('loading-bar');
|
|
30
|
+
if (loadingBar) {
|
|
31
|
+
document.body.addEventListener('htmx:beforeRequest', () => {
|
|
32
|
+
loadingBar.style.display = 'block';
|
|
33
|
+
});
|
|
34
|
+
document.body.addEventListener('htmx:afterRequest', () => {
|
|
35
|
+
loadingBar.style.display = 'none';
|
|
36
|
+
});
|
|
37
|
+
document.body.addEventListener('htmx:responseError', () => {
|
|
38
|
+
loadingBar.style.display = 'none';
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Storage backend ABC — defines the interface for file storage implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from pathlib import PurePosixPath
|
|
8
|
+
|
|
9
|
+
from starlette.datastructures import UploadFile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StorageBackend(ABC):
|
|
13
|
+
"""Abstract base class for file storage backends.
|
|
14
|
+
|
|
15
|
+
Implementations must define ``save``, ``delete``, and ``url``.
|
|
16
|
+
``sanitize_filename`` is shared across all backends.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def save(self, file: UploadFile, directory: str = "") -> str:
|
|
21
|
+
"""Save an uploaded file and return its relative path within storage."""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def delete(self, path: str) -> None:
|
|
25
|
+
"""Delete a file at the given relative path."""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def url(self, path: str) -> str:
|
|
29
|
+
"""Return the public URL for a stored file (relative path)."""
|
|
30
|
+
|
|
31
|
+
def sanitize_filename(self, filename: str) -> str:
|
|
32
|
+
"""Sanitize an uploaded filename.
|
|
33
|
+
|
|
34
|
+
Prepends a UUID prefix to prevent collisions and strips path
|
|
35
|
+
separators to avoid directory traversal.
|
|
36
|
+
"""
|
|
37
|
+
# Strip any directory components
|
|
38
|
+
name = PurePosixPath(filename).name
|
|
39
|
+
if not name:
|
|
40
|
+
name = "unnamed"
|
|
41
|
+
|
|
42
|
+
# Remove null bytes and path separators
|
|
43
|
+
name = name.replace("\x00", "")
|
|
44
|
+
name = name.replace("/", "_").replace("\\", "_")
|
|
45
|
+
|
|
46
|
+
# Prepend UUID for uniqueness
|
|
47
|
+
prefix = uuid.uuid4().hex[:12]
|
|
48
|
+
return f"{prefix}_{name}"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Local filesystem storage backend — saves files to a local directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from starlette.datastructures import UploadFile
|
|
9
|
+
|
|
10
|
+
from fastapi_admin_kit.storage.base import StorageBackend
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocalStorageBackend(StorageBackend):
|
|
14
|
+
"""Saves uploaded files to a local directory and serves via StaticFiles.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
upload_dir:
|
|
19
|
+
Absolute or relative path where files are stored.
|
|
20
|
+
base_url:
|
|
21
|
+
The URL prefix that maps to ``upload_dir`` (e.g. ``"/uploads"``).
|
|
22
|
+
max_size_mb:
|
|
23
|
+
Maximum allowed file size in megabytes. ``None`` means no limit.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
upload_dir: str | Path = "uploads",
|
|
29
|
+
base_url: str = "/uploads",
|
|
30
|
+
max_size_mb: float | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.upload_dir = Path(upload_dir)
|
|
33
|
+
self.base_url = base_url.rstrip("/")
|
|
34
|
+
self.max_size_mb = max_size_mb
|
|
35
|
+
|
|
36
|
+
async def save(self, file: UploadFile, directory: str = "") -> str:
|
|
37
|
+
"""Save an uploaded file. Returns the relative path within storage."""
|
|
38
|
+
filename = self.sanitize_filename(file.filename or "unnamed")
|
|
39
|
+
target_dir = self.upload_dir / directory
|
|
40
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
target_path = target_dir / filename
|
|
43
|
+
content = await file.read()
|
|
44
|
+
|
|
45
|
+
if self.max_size_mb is not None:
|
|
46
|
+
max_bytes = int(self.max_size_mb * 1024 * 1024)
|
|
47
|
+
if len(content) > max_bytes:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"File size ({len(content)} bytes) exceeds maximum "
|
|
50
|
+
f"allowed size ({self.max_size_mb} MB)."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
with open(target_path, "wb") as f:
|
|
54
|
+
f.write(content)
|
|
55
|
+
|
|
56
|
+
# Return path relative to upload_dir, using forward slashes
|
|
57
|
+
if directory:
|
|
58
|
+
return f"{directory}/{filename}"
|
|
59
|
+
return filename
|
|
60
|
+
|
|
61
|
+
async def delete(self, path: str) -> None:
|
|
62
|
+
"""Delete a file at the given relative path."""
|
|
63
|
+
target = self.upload_dir / path
|
|
64
|
+
if target.is_file():
|
|
65
|
+
os.remove(target)
|
|
66
|
+
|
|
67
|
+
def url(self, path: str) -> str:
|
|
68
|
+
"""Return the public URL for a stored file."""
|
|
69
|
+
return f"{self.base_url}/{path}"
|
|
70
|
+
|
|
71
|
+
def ensure_dir(self) -> None:
|
|
72
|
+
"""Create the upload directory if it doesn't exist."""
|
|
73
|
+
self.upload_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="{{ 'dark' if admin_config.dark_mode_default else 'light' }}"
|
|
3
|
+
data-preset="{{ theme_preset | default('editorial') }}">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>{% block title %}{{ title | default("Admin") }}{% endblock %}</title>
|
|
8
|
+
<meta name="csrf-token" content="{{ get_csrf_token(request) }}">
|
|
9
|
+
|
|
10
|
+
{% if admin_config.favicon_url %}
|
|
11
|
+
<link rel="icon" href="{{ admin_config.favicon_url }}">
|
|
12
|
+
{% endif %}
|
|
13
|
+
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
+
{% if theme_font_import_url | default('') %}
|
|
17
|
+
<link href="{{ theme_font_import_url }}" rel="stylesheet">
|
|
18
|
+
{% else %}
|
|
19
|
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
20
|
+
{% endif %}
|
|
21
|
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&display=block" rel="stylesheet">
|
|
22
|
+
|
|
23
|
+
<link rel="stylesheet" href="/static/css/tokens.css?v={{ static_version }}">
|
|
24
|
+
<link rel="stylesheet" href="/static/css/presets.css?v={{ static_version }}">
|
|
25
|
+
<link rel="stylesheet" href="/static/css/admin.css?v={{ static_version }}">
|
|
26
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
|
27
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css">
|
|
28
|
+
|
|
29
|
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
30
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
|
31
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
32
|
+
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
|
33
|
+
|
|
34
|
+
{% if theme_css | default('') %}
|
|
35
|
+
<style>{{ theme_css }}</style>
|
|
36
|
+
{% endif %}
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
:root {
|
|
40
|
+
--primary-500: {{ admin_config.primary_color | default('#10B981') }};
|
|
41
|
+
--primary-600: {{ admin_config.primary_color_dark | default('#059669') }};
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
|
|
45
|
+
{% set _ui = ui_config | default({}) %}
|
|
46
|
+
{% if _ui.custom_css_url | default('') %}
|
|
47
|
+
<link rel="stylesheet" href="{{ _ui.custom_css_url }}">
|
|
48
|
+
{% endif %}
|
|
49
|
+
{% if _ui.custom_css | default('') %}
|
|
50
|
+
<style>{{ _ui.custom_css }}</style>
|
|
51
|
+
{% endif %}
|
|
52
|
+
|
|
53
|
+
{% block head_extra %}{% endblock %}
|
|
54
|
+
|
|
55
|
+
{% for css_url in extra_css | default([]) %}
|
|
56
|
+
<link rel="stylesheet" href="{{ css_url }}">
|
|
57
|
+
{% endfor %}
|
|
58
|
+
{% for js_url in extra_js | default([]) %}
|
|
59
|
+
<script src="{{ js_url }}"></script>
|
|
60
|
+
{% endfor %}
|
|
61
|
+
{% if extra_css_inline | default('') %}
|
|
62
|
+
<style>{{ extra_css_inline }}</style>
|
|
63
|
+
{% endif %}
|
|
64
|
+
{% if extra_js_inline | default('') %}
|
|
65
|
+
<script>{{ extra_js_inline }}</script>
|
|
66
|
+
{% endif %}
|
|
67
|
+
</head>
|
|
68
|
+
<body x-data="{
|
|
69
|
+
sidebarOpen: false,
|
|
70
|
+
sidebarCollapsed: localStorage.getItem('sidebarCollapsed') === 'true',
|
|
71
|
+
init() {
|
|
72
|
+
if (window.innerWidth < 1024) { this.sidebarCollapsed = true; }
|
|
73
|
+
window.addEventListener('resize', () => {
|
|
74
|
+
if (window.innerWidth < 1024) { this.sidebarCollapsed = true; this.sidebarOpen = false; }
|
|
75
|
+
});
|
|
76
|
+
this.$watch('sidebarCollapsed', val => { localStorage.setItem('sidebarCollapsed', val); });
|
|
77
|
+
},
|
|
78
|
+
toggleMobileSidebar() { this.sidebarOpen = !this.sidebarOpen; }
|
|
79
|
+
}"
|
|
80
|
+
class="font-sans antialiased"
|
|
81
|
+
style="color: var(--text-primary); background-color: var(--surface-base);">
|
|
82
|
+
|
|
83
|
+
<div id="loading-bar" class="htmx-indicator-loading"></div>
|
|
84
|
+
|
|
85
|
+
<div class="admin-shell">
|
|
86
|
+
{% include "partials/topbar.html" %}
|
|
87
|
+
|
|
88
|
+
<div class="admin-body">
|
|
89
|
+
{% include "partials/sidebar.html" %}
|
|
90
|
+
|
|
91
|
+
<div class="admin-content {% set _cw = (ui_config | default({})).content_width | default('default') %}{% if _cw != 'default' %}content--{{ _cw }}{% endif %}"
|
|
92
|
+
id="admin-content">
|
|
93
|
+
{% include "partials/flash_messages.html" %}
|
|
94
|
+
|
|
95
|
+
<div class="admin-content__inner">
|
|
96
|
+
{% block content %}{% endblock %}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{# Confirm dialog modal #}
|
|
103
|
+
<template x-if="true">
|
|
104
|
+
<div id="confirm-dialog"
|
|
105
|
+
x-data="{ open: false, title: '', message: '', onConfirm: null }"
|
|
106
|
+
x-show="open" x-cloak>
|
|
107
|
+
<div class="modal-backdrop" @click="open = false"></div>
|
|
108
|
+
<div class="modal modal--sm" @click.stop>
|
|
109
|
+
<div class="modal__header">
|
|
110
|
+
<h3 class="modal__title" x-text="title"></h3>
|
|
111
|
+
<button type="button" class="modal__close" @click="open = false">
|
|
112
|
+
{{ icon("x-mark") }}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="modal__body">
|
|
116
|
+
<div class="flex items-start gap-3">
|
|
117
|
+
{{ icon("exclamation-triangle", class="flex-shrink-0 mt-0.5", size="24px") }}
|
|
118
|
+
<p x-text="message" style="color: var(--text-secondary);"></p>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="modal__footer">
|
|
122
|
+
<button @click="open = false" class="btn btn-secondary btn-sm">Cancel</button>
|
|
123
|
+
<button @click="onConfirm(); open = false" class="btn btn-danger btn-sm">Delete</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
{% include "partials/command_palette.html" %}
|
|
130
|
+
|
|
131
|
+
{% set _ui2 = ui_config | default({}) %}
|
|
132
|
+
{% if _ui2.custom_js_url | default('') %}
|
|
133
|
+
<script src="{{ _ui2.custom_js_url }}"></script>
|
|
134
|
+
{% endif %}
|
|
135
|
+
{% if _ui2.custom_js | default('') %}
|
|
136
|
+
<script>{{ _ui2.custom_js }}</script>
|
|
137
|
+
{% endif %}
|
|
138
|
+
|
|
139
|
+
<script src="/static/js/htmx-config.js"></script>
|
|
140
|
+
<script src="/static/js/admin.js?v={{ static_version }}"></script>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|