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.
Files changed (163) hide show
  1. fastapi_admin_kit/__init__.py +73 -0
  2. fastapi_admin_kit/actions/__init__.py +63 -0
  3. fastapi_admin_kit/actions/base.py +68 -0
  4. fastapi_admin_kit/actions/registry.py +43 -0
  5. fastapi_admin_kit/admin/__init__.py +17 -0
  6. fastapi_admin_kit/admin/admin_config.py +95 -0
  7. fastapi_admin_kit/admin/admin_database.py +138 -0
  8. fastapi_admin_kit/admin/admin_router.py +74 -0
  9. fastapi_admin_kit/admin/admin_template.py +203 -0
  10. fastapi_admin_kit/admin/builtin_models.py +284 -0
  11. fastapi_admin_kit/admin/core.py +1036 -0
  12. fastapi_admin_kit/admin/decorators.py +70 -0
  13. fastapi_admin_kit/admin/state.py +76 -0
  14. fastapi_admin_kit/admin.py +728 -0
  15. fastapi_admin_kit/api/__init__.py +44 -0
  16. fastapi_admin_kit/api/auth.py +342 -0
  17. fastapi_admin_kit/api/crud.py +128 -0
  18. fastapi_admin_kit/api/deps.py +79 -0
  19. fastapi_admin_kit/api/roles.py +128 -0
  20. fastapi_admin_kit/api/schema_generator.py +171 -0
  21. fastapi_admin_kit/api/schemas.py +81 -0
  22. fastapi_admin_kit/api/search.py +132 -0
  23. fastapi_admin_kit/audit/__init__.py +36 -0
  24. fastapi_admin_kit/audit/context.py +62 -0
  25. fastapi_admin_kit/audit/diff.py +77 -0
  26. fastapi_admin_kit/audit/event_bus.py +96 -0
  27. fastapi_admin_kit/audit/events.py +48 -0
  28. fastapi_admin_kit/audit/listener.py +159 -0
  29. fastapi_admin_kit/audit/logger.py +28 -0
  30. fastapi_admin_kit/audit/middleware.py +39 -0
  31. fastapi_admin_kit/audit/models.py +53 -0
  32. fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
  33. fastapi_admin_kit/auth/__init__.py +34 -0
  34. fastapi_admin_kit/auth/backend.py +95 -0
  35. fastapi_admin_kit/auth/csrf.py +240 -0
  36. fastapi_admin_kit/auth/dependencies.py +150 -0
  37. fastapi_admin_kit/auth/identity.py +181 -0
  38. fastapi_admin_kit/auth/models.py +246 -0
  39. fastapi_admin_kit/auth/password.py +35 -0
  40. fastapi_admin_kit/auth/permissions.py +205 -0
  41. fastapi_admin_kit/auth/protocol.py +22 -0
  42. fastapi_admin_kit/auth/ratelimit.py +88 -0
  43. fastapi_admin_kit/auth/router.py +10 -0
  44. fastapi_admin_kit/auth/session.py +79 -0
  45. fastapi_admin_kit/auth/totp.py +83 -0
  46. fastapi_admin_kit/auth/views.py +165 -0
  47. fastapi_admin_kit/cli.py +229 -0
  48. fastapi_admin_kit/config/__init__.py +19 -0
  49. fastapi_admin_kit/config/audit.py +18 -0
  50. fastapi_admin_kit/config/auth.py +54 -0
  51. fastapi_admin_kit/config/behavior.py +27 -0
  52. fastapi_admin_kit/config/nav.py +32 -0
  53. fastapi_admin_kit/config/storage.py +22 -0
  54. fastapi_admin_kit/config/theme.py +215 -0
  55. fastapi_admin_kit/config/ui.py +147 -0
  56. fastapi_admin_kit/dashboard/__init__.py +64 -0
  57. fastapi_admin_kit/db.py +133 -0
  58. fastapi_admin_kit/exceptions.py +5 -0
  59. fastapi_admin_kit/field_types.py +81 -0
  60. fastapi_admin_kit/filters/__init__.py +21 -0
  61. fastapi_admin_kit/filters/base.py +170 -0
  62. fastapi_admin_kit/filters/registry.py +68 -0
  63. fastapi_admin_kit/flash.py +45 -0
  64. fastapi_admin_kit/form/__init__.py +1 -0
  65. fastapi_admin_kit/form/pipeline.py +106 -0
  66. fastapi_admin_kit/inspection/__init__.py +117 -0
  67. fastapi_admin_kit/inspection/registry.py +253 -0
  68. fastapi_admin_kit/inspection.py +115 -0
  69. fastapi_admin_kit/modeladmin.py +375 -0
  70. fastapi_admin_kit/models/__init__.py +7 -0
  71. fastapi_admin_kit/models/base.py +7 -0
  72. fastapi_admin_kit/nav.py +208 -0
  73. fastapi_admin_kit/pagination/__init__.py +14 -0
  74. fastapi_admin_kit/pagination/base.py +40 -0
  75. fastapi_admin_kit/pagination/cursor.py +97 -0
  76. fastapi_admin_kit/pagination/dynamic.py +48 -0
  77. fastapi_admin_kit/pagination/offset.py +42 -0
  78. fastapi_admin_kit/plugins/__init__.py +1 -0
  79. fastapi_admin_kit/py.typed +0 -0
  80. fastapi_admin_kit/registry/__init__.py +5 -0
  81. fastapi_admin_kit/registry/core.py +287 -0
  82. fastapi_admin_kit/registry/validation.py +107 -0
  83. fastapi_admin_kit/registry.py +15 -0
  84. fastapi_admin_kit/router.py +335 -0
  85. fastapi_admin_kit/static/css/admin.css +4736 -0
  86. fastapi_admin_kit/static/css/presets.css +317 -0
  87. fastapi_admin_kit/static/css/tokens.css +217 -0
  88. fastapi_admin_kit/static/css/variables.css +74 -0
  89. fastapi_admin_kit/static/icons/heroicons.svg +160 -0
  90. fastapi_admin_kit/static/js/admin.js +692 -0
  91. fastapi_admin_kit/static/js/htmx-config.js +42 -0
  92. fastapi_admin_kit/storage/__init__.py +6 -0
  93. fastapi_admin_kit/storage/base.py +48 -0
  94. fastapi_admin_kit/storage/local.py +73 -0
  95. fastapi_admin_kit/templates/base.html +142 -0
  96. fastapi_admin_kit/templates/macros/form_fields.html +660 -0
  97. fastapi_admin_kit/templates/macros/icons.html +50 -0
  98. fastapi_admin_kit/templates/macros/table.html +108 -0
  99. fastapi_admin_kit/templates/macros/widgets.html +159 -0
  100. fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
  101. fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
  102. fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
  103. fastapi_admin_kit/templates/pages/audit_log.html +102 -0
  104. fastapi_admin_kit/templates/pages/dashboard.html +295 -0
  105. fastapi_admin_kit/templates/pages/detail.html +183 -0
  106. fastapi_admin_kit/templates/pages/form.html +119 -0
  107. fastapi_admin_kit/templates/pages/list.html +277 -0
  108. fastapi_admin_kit/templates/pages/login.html +85 -0
  109. fastapi_admin_kit/templates/pages/profile/password.html +78 -0
  110. fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
  111. fastapi_admin_kit/templates/pages/role_form.html +75 -0
  112. fastapi_admin_kit/templates/pages/roles/form.html +117 -0
  113. fastapi_admin_kit/templates/pages/roles/list.html +69 -0
  114. fastapi_admin_kit/templates/pages/roles.html +77 -0
  115. fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
  116. fastapi_admin_kit/templates/pages/users/form.html +229 -0
  117. fastapi_admin_kit/templates/pages/users/list.html +83 -0
  118. fastapi_admin_kit/templates/partials/command_palette.html +52 -0
  119. fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
  120. fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
  121. fastapi_admin_kit/templates/partials/head.html +21 -0
  122. fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
  123. fastapi_admin_kit/templates/partials/list_table.html +178 -0
  124. fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
  125. fastapi_admin_kit/templates/partials/pagination.html +82 -0
  126. fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
  127. fastapi_admin_kit/templates/partials/scripts.html +13 -0
  128. fastapi_admin_kit/templates/partials/sidebar.html +94 -0
  129. fastapi_admin_kit/templates/partials/topbar.html +95 -0
  130. fastapi_admin_kit/types.py +145 -0
  131. fastapi_admin_kit/validation.py +43 -0
  132. fastapi_admin_kit/views/__init__.py +78 -0
  133. fastapi_admin_kit/views/audit.py +134 -0
  134. fastapi_admin_kit/views/bulk.py +28 -0
  135. fastapi_admin_kit/views/class_views.py +1040 -0
  136. fastapi_admin_kit/views/context.py +588 -0
  137. fastapi_admin_kit/views/dashboard.py +162 -0
  138. fastapi_admin_kit/views/delete.py +31 -0
  139. fastapi_admin_kit/views/extra.py +65 -0
  140. fastapi_admin_kit/views/factory.py +667 -0
  141. fastapi_admin_kit/views/form.py +159 -0
  142. fastapi_admin_kit/views/list.py +28 -0
  143. fastapi_admin_kit/views/profile.py +219 -0
  144. fastapi_admin_kit/views/protocols.py +54 -0
  145. fastapi_admin_kit/views/renderers.py +634 -0
  146. fastapi_admin_kit/views/roles.py +230 -0
  147. fastapi_admin_kit/views/search.py +31 -0
  148. fastapi_admin_kit/views/settings.py +31 -0
  149. fastapi_admin_kit/views/sidebar.py +101 -0
  150. fastapi_admin_kit/views/totp.py +249 -0
  151. fastapi_admin_kit/views/users.py +347 -0
  152. fastapi_admin_kit/views.py +117 -0
  153. fastapi_admin_kit/widgets/__init__.py +44 -0
  154. fastapi_admin_kit/widgets/base.py +44 -0
  155. fastapi_admin_kit/widgets/inputs.py +363 -0
  156. fastapi_admin_kit/widgets/registry.py +110 -0
  157. fastapi_admin_kit/widgets/relation.py +70 -0
  158. fastapi_admin_kit/widgets/resolver.py +102 -0
  159. fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
  160. fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
  161. fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
  162. fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
  163. fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,75 @@
1
+ {# pages/role_form.html — role edit with permission matrix #}
2
+ {% extends "base.html" %}
3
+
4
+ {% block title %}{% if role %}Edit Role{% else %}Create Role{% endif %} — {{ title | default("Admin") }}{% endblock %}
5
+
6
+ {% block breadcrumb %}
7
+ {{ super() }}
8
+ <span class="text-[var(--color-gray-400)]">/</span>
9
+ <a href="{{ admin_path | default('/admin') }}/roles/" class="hover:text-[var(--color-primary-600)] transition-colors">Roles</a>
10
+ <span class="text-[var(--color-gray-400)]">/</span>
11
+ <span class="text-[var(--color-gray-900)] font-medium">{% if role %}{{ role.name }}{% else %}Create{% endif %}</span>
12
+ {% endblock %}
13
+
14
+ {% block content %}
15
+ <div class="space-y-6">
16
+ <div class="page-header">
17
+ <div class="page-header-left">
18
+ <a href="{{ admin_path | default('/admin') }}/roles/" class="page-header-back">
19
+ {{ icon("chevron-left") }}
20
+ Roles
21
+ </a>
22
+ <h1 class="page-header-title">
23
+ {% if role %}Edit Role{% else %}New Role{% endif %}
24
+ </h1>
25
+ {% if role %}
26
+ <p class="page-header-subtitle">
27
+ <span class="mono">#{{ role.id }}</span> — {{ role.name }}
28
+ </p>
29
+ {% endif %}
30
+ </div>
31
+ </div>
32
+
33
+ <form method="post" action="{{ admin_path | default('/admin') }}/roles{% if role %}/{{ role.id }}{% endif %}"
34
+ class="space-y-6">
35
+ <input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
36
+ <div class="bg-white rounded-xl border border-[var(--color-gray-200)] p-6">
37
+ <h2 class="text-lg font-semibold text-[var(--color-gray-900)] mb-4">Role Details</h2>
38
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
39
+ <div>
40
+ <label for="name" class="block text-sm font-medium text-[var(--color-gray-700)] mb-1">Name</label>
41
+ <input type="text" id="name" name="name" value="{{ role.name | default('') }}" required
42
+ class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)] transition-colors">
43
+ </div>
44
+ <div>
45
+ <label for="description" class="block text-sm font-medium text-[var(--color-gray-700)] mb-1">Description</label>
46
+ <input type="text" id="description" name="description" value="{{ role.description | default('') }}"
47
+ class="form-input w-full px-4 py-2.5 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)] transition-colors">
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="bg-white rounded-xl border border-[var(--color-gray-200)]">
53
+ <div class="px-6 py-4 border-b border-[var(--color-gray-200)]">
54
+ <h2 class="text-lg font-semibold text-[var(--color-gray-900)]">Permissions</h2>
55
+ <p class="mt-1 text-sm text-[var(--color-gray-500)]">Select tables and configure actions for this role.</p>
56
+ </div>
57
+
58
+ <div class="p-6">
59
+ {% include "partials/permission_widget.html" with context %}
60
+ </div>
61
+ </div>
62
+
63
+ <div class="sticky bottom-0 bg-white border border-[var(--color-gray-200)] rounded-xl p-4 flex items-center justify-end gap-3">
64
+ <a href="{{ admin_path | default('/admin') }}/roles/"
65
+ class="px-4 py-2 rounded-lg border border-[var(--color-gray-300)] text-sm text-[var(--color-gray-700)] hover:bg-[var(--color-gray-50)] transition-colors">
66
+ Cancel
67
+ </a>
68
+ <button type="submit"
69
+ class="btn-primary px-4 py-2 rounded-lg bg-[var(--color-primary-600)] text-white text-sm font-medium hover:bg-[var(--color-primary-500)] transition-colors">
70
+ {% if role %}Save Changes{% else %}Create Role{% endif %}
71
+ </button>
72
+ </div>
73
+ </form>
74
+ </div>
75
+ {% endblock %}
@@ -0,0 +1,117 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}{% if role %}Edit Role{% else %}Create Role{% endif %} — Admin{% endblock %}
4
+
5
+ {% block content %}
6
+ <a href="{{ request.app.state.admin_config.admin_path }}/roles" class="back-link">
7
+ {{ icon("chevron-left", size="20px") }}
8
+ Back to Roles
9
+ </a>
10
+
11
+ <div class="page-header">
12
+ <div class="page-header__left">
13
+ <h1 class="page-header__title">{% if role %}Edit Role{% else %}Create Role{% endif %}</h1>
14
+ {% if role %}
15
+ <p class="page-header__subtitle">Editing: <strong style="color: var(--text-primary);">{{ role.name }}</strong></p>
16
+ {% endif %}
17
+ </div>
18
+ </div>
19
+
20
+ <form method="post">
21
+ <input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
22
+ <!-- Role details -->
23
+ <div class="fieldset" style="margin-bottom: var(--space-5); max-width: var(--form-max-width);">
24
+ <div class="fieldset__header">
25
+ <h3 class="fieldset__title">Role Details</h3>
26
+ </div>
27
+ <div class="fieldset__fields">
28
+ <div class="field-wrapper">
29
+ <label for="role_name" class="field-label">Role Name</label>
30
+ <input type="text" id="role_name" name="role_name" required
31
+ value="{% if role %}{{ role.name }}{% endif %}"
32
+ class="form-input">
33
+ </div>
34
+ <div class="field-wrapper field-wrapper--full">
35
+ <label for="description" class="field-label">Description</label>
36
+ <textarea id="description" name="description" rows="2"
37
+ class="form-textarea">{% if role %}{{ role.description or '' }}{% endif %}</textarea>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- Permissions matrix -->
43
+ <div class="fieldset" style="margin-bottom: var(--space-5); max-width: var(--form-max-width);">
44
+ <div class="fieldset__header">
45
+ <h3 class="fieldset__title">Model Permissions</h3>
46
+ </div>
47
+ <div class="permissions-matrix" style="border: none;">
48
+ <table>
49
+ <thead>
50
+ <tr>
51
+ <th style="text-align: left;">Model</th>
52
+ <th>View</th>
53
+ <th>Create</th>
54
+ <th>Edit</th>
55
+ <th>Delete</th>
56
+ <th style="text-align: left;">Fields</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ {% for model in models %}
61
+ {% set table_name = model.table_name %}
62
+ {% set perm = permissions.get(table_name) %}
63
+ {% set f_perms = field_permissions.get(table_name, {}) %}
64
+ <tr>
65
+ <td style="font-weight: var(--font-medium);">{{ model.verbose_name_plural }}</td>
66
+ <td><input type="checkbox" name="perm[{{ table_name }}][view]" class="matrix-checkbox" {% if perm and perm.can_view %}checked{% endif %}></td>
67
+ <td><input type="checkbox" name="perm[{{ table_name }}][create]" class="matrix-checkbox" {% if perm and perm.can_create %}checked{% endif %}></td>
68
+ <td><input type="checkbox" name="perm[{{ table_name }}][edit]" class="matrix-checkbox" {% if perm and perm.can_edit %}checked{% endif %}></td>
69
+ <td><input type="checkbox" name="perm[{{ table_name }}][delete]" class="matrix-checkbox" {% if perm and perm.can_delete %}checked{% endif %}></td>
70
+ <td>
71
+ <details>
72
+ <summary style="cursor: pointer; color: var(--primary-500); font-size: var(--text-xs);">Configure</summary>
73
+ <div style="margin-top: 8px; display: flex; flex-direction: column; gap: 4px;">
74
+ {% for field in model.form_fields %}
75
+ {% set field_key = field.name %}
76
+ {% set fp = f_perms.get(field_key) %}
77
+ <div style="display: flex; align-items: center; gap: 8px;">
78
+ <span style="font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--text-secondary); min-width: 100px;">{{ field.label or field.name }}</span>
79
+ <label style="display: inline-flex; align-items: center; gap: 4px; font-size: var(--text-2xs); color: var(--text-secondary);">
80
+ <input type="checkbox" name="field_perm[{{ table_name }}][{{ field_key }}][view]" class="matrix-checkbox" style="width: 14px; height: 14px;" {% if fp and fp.can_view %}checked{% endif %}>
81
+ View
82
+ </label>
83
+ <label style="display: inline-flex; align-items: center; gap: 4px; font-size: var(--text-2xs); color: var(--text-secondary);">
84
+ <input type="checkbox" name="field_perm[{{ table_name }}][{{ field_key }}][edit]" class="matrix-checkbox" style="width: 14px; height: 14px;" {% if fp and fp.can_edit %}checked{% endif %}>
85
+ Edit
86
+ </label>
87
+ </div>
88
+ {% endfor %}
89
+ </div>
90
+ </details>
91
+ </td>
92
+ </tr>
93
+ {% endfor %}
94
+ </tbody>
95
+ </table>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Save bar -->
100
+ <div class="form-save-bar" style="margin: var(--space-8) calc(-1 * var(--content-padding)) 0; width: calc(100% + var(--content-padding) * 2);">
101
+ <div class="form-save-bar__left">
102
+ {% if role %}
103
+ <button type="button" class="btn btn-danger btn-sm"
104
+ onclick="confirmAction('Delete Role', 'Are you sure you want to delete this role?', () => { window.location='{{ request.app.state.admin_config.admin_path }}/roles/{{ role.id }}/delete'; })">
105
+ Delete Role
106
+ </button>
107
+ {% endif %}
108
+ </div>
109
+ <div class="form-save-bar__right">
110
+ <a href="{{ request.app.state.admin_config.admin_path }}/roles" class="btn btn-ghost">Cancel</a>
111
+ <button type="submit" class="btn btn-primary">
112
+ Save Role
113
+ </button>
114
+ </div>
115
+ </div>
116
+ </form>
117
+ {% endblock %}
@@ -0,0 +1,69 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Roles — Admin{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="page-header">
7
+ <div class="page-header__left">
8
+ <h1 class="page-header__title">Roles</h1>
9
+ <p class="page-header__subtitle">Manage user roles and permissions.</p>
10
+ </div>
11
+ <div class="page-header__actions">
12
+ <a href="{{ request.app.state.admin_config.admin_path }}/roles/create" class="btn btn-primary">
13
+ {{ icon("plus", size="15px") }}
14
+ New Role
15
+ </a>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="table-container">
20
+ <table class="table-wrapper">
21
+ <thead>
22
+ <tr>
23
+ <th class="table__th">Name</th>
24
+ <th class="table__th">Description</th>
25
+ <th class="table__th">Users</th>
26
+ <th class="table__th table__th--actions"></th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ {% for data in roles %}
31
+ <tr>
32
+ <td class="table__cell" style="font-weight: var(--font-medium);">{{ data.role.name }}</td>
33
+ <td class="table__cell" style="color: var(--text-secondary);">{{ data.role.description or '—' }}</td>
34
+ <td class="table__cell table__cell--mono">{{ data.user_count }}</td>
35
+ <td class="table__cell table__cell--actions">
36
+ <div class="table-actions">
37
+ <a href="{{ request.app.state.admin_config.admin_path }}/roles/{{ data.role.id }}"
38
+ class="table-action">Edit</a>
39
+ <span class="table-actions__separator"></span>
40
+ <form method="post" action="{{ request.app.state.admin_config.admin_path }}/roles/{{ data.role.id }}/delete"
41
+ class="inline">
42
+ <input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
43
+ <button type="button"
44
+ class="table-action table-action--danger"
45
+ {% if data.user_count > 0 %}disabled{% endif %}
46
+ onclick="confirmAction('Delete Role', 'Are you sure you want to delete this role?', () => this.closest('form').submit())">
47
+ Delete
48
+ </button>
49
+ </form>
50
+ </div>
51
+ </td>
52
+ </tr>
53
+ {% endfor %}
54
+
55
+ {% if not roles %}
56
+ <tr>
57
+ <td colspan="4">
58
+ <div class="empty-state">
59
+ {{ icon("shield-check", css_class="empty-state__icon", size="48px") }}
60
+ <h3 class="empty-state__title">No roles found</h3>
61
+ <p class="empty-state__text">Create your first role to get started.</p>
62
+ </div>
63
+ </td>
64
+ </tr>
65
+ {% endif %}
66
+ </tbody>
67
+ </table>
68
+ </div>
69
+ {% endblock %}
@@ -0,0 +1,77 @@
1
+ {# pages/roles.html — role list page #}
2
+ {% extends "base.html" %}
3
+
4
+ {% block title %}Roles — {{ title | default("Admin") }}{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="space-y-6">
8
+ <div class="page-header">
9
+ <div class="page-header-left">
10
+ <h1 class="page-header-title">Roles</h1>
11
+ <p class="page-header-subtitle">Manage roles and permissions</p>
12
+ </div>
13
+ <div class="page-header-right">
14
+ <a href="{{ admin_path | default('/admin') }}/roles/create" class="btn btn-primary">
15
+ + New Role
16
+ </a>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="bg-white rounded-xl border border-[var(--color-gray-200)]">
21
+ <div class="overflow-x-auto">
22
+ <table class="table-wrapper w-full">
23
+ <thead>
24
+ <tr class="border-b border-[var(--color-gray-200)]">
25
+ <th class="px-6 py-3 text-left text-xs font-semibold text-[var(--color-gray-500)] uppercase tracking-wider">Name</th>
26
+ <th class="px-6 py-3 text-left text-xs font-semibold text-[var(--color-gray-500)] uppercase tracking-wider">Description</th>
27
+ <th class="px-6 py-3 text-left text-xs font-semibold text-[var(--color-gray-500)] uppercase tracking-wider">Users</th>
28
+ <th class="px-6 py-3 text-right text-xs font-semibold text-[var(--color-gray-500)] uppercase tracking-wider">Actions</th>
29
+ </tr>
30
+ </thead>
31
+ <tbody class="divide-y divide-[var(--color-gray-100)]">
32
+ {% if roles %}
33
+ {% for role in roles %}
34
+ <tr class="hover:bg-[var(--color-gray-50)] transition-colors">
35
+ <td class="px-6 py-4">
36
+ <a href="{{ admin_path | default('/admin') }}/roles/{{ role.id }}" class="text-sm font-medium text-[var(--color-primary-600)] hover:underline">
37
+ {{ role.name }}
38
+ </a>
39
+ </td>
40
+ <td class="px-6 py-4 text-sm text-[var(--color-gray-600)]">{{ role.description | default("-") }}</td>
41
+ <td class="px-6 py-4 text-sm text-[var(--color-gray-600)]">{{ role.user_count }} user{{ "s" if role.user_count != 1 else "" }}</td>
42
+ <td class="px-6 py-4 text-right text-sm space-x-2">
43
+ <a href="{{ admin_path | default('/admin') }}/roles/{{ role.id }}"
44
+ class="text-[var(--color-primary-600)] hover:text-[var(--color-primary-500)] font-medium transition-colors">
45
+ Edit
46
+ </a>
47
+ {% if role.user_count == 0 %}
48
+ <form method="post" action="{{ admin_path | default('/admin') }}/roles/{{ role.id }}/delete" class="inline">
49
+ <input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
50
+ <button type="button"
51
+ class="text-[var(--color-danger)] hover:text-red-700 font-medium transition-colors"
52
+ onclick="confirmAction('Delete Role', 'Are you sure you want to delete this role?', () => this.closest('form').submit())">
53
+ Delete
54
+ </button>
55
+ </form>
56
+ {% endif %}
57
+ </td>
58
+ </tr>
59
+ {% endfor %}
60
+ {% else %}
61
+ <tr>
62
+ <td colspan="4" class="px-6 py-12 text-center">
63
+ {{ icon("users", size="48px") }}
64
+ <p class="mt-4 text-sm text-[var(--color-gray-500)]">No roles defined</p>
65
+ <a href="{{ admin_path | default('/admin') }}/roles/create"
66
+ class="mt-4 inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[var(--color-primary-600)] text-white text-sm font-medium hover:bg-[var(--color-primary-500)] transition-colors">
67
+ + Create your first role
68
+ </a>
69
+ </td>
70
+ </tr>
71
+ {% endif %}
72
+ </tbody>
73
+ </table>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ {% endblock %}
@@ -0,0 +1,255 @@
1
+ {# pages/settings/theme.html — visual theme builder #}
2
+ {% extends "base.html" %}
3
+
4
+ {% block title %}Theme Settings — {{ title | default("Admin") }}{% endblock %}
5
+
6
+ {% block content %}
7
+ <div x-data="themeBuilder()" class="space-y-6">
8
+ <div class="page-header">
9
+ <div class="page-header-left">
10
+ <h1 class="page-header-title">Theme Settings</h1>
11
+ <p class="page-header-subtitle">Customize the look and feel of your admin panel</p>
12
+ </div>
13
+ </div>
14
+
15
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
16
+ {# Controls Panel #}
17
+ <div class="lg:col-span-2 space-y-6">
18
+ {# Preset Selector #}
19
+ <div class="fieldset">
20
+ <div class="fieldset__header">
21
+ <h2 class="fieldset__title">Theme Preset</h2>
22
+ </div>
23
+ <div class="grid grid-cols-3 gap-3">
24
+ {% for p in ["editorial", "modern", "midnight", "paper", "forest", "minimal"] %}
25
+ <button @click="applyPreset('{{ p }}')"
26
+ :class="preset === '{{ p }}' ? 'ring-2 ring-[var(--primary-500)]' : ''"
27
+ class="p-4 rounded-lg border border-[var(--surface-border)] text-left transition-all hover:border-[var(--primary-400)]">
28
+ <div class="flex items-center gap-2 mb-2">
29
+ <span class="w-4 h-4 rounded-full border border-[var(--surface-border)]"
30
+ :style="'background: ' + presets['{{ p }}'].primary_color"></span>
31
+ <span class="text-sm font-medium">{{ p | title }}</span>
32
+ </div>
33
+ <div class="flex gap-1">
34
+ <span class="w-4 h-3 rounded-sm" :style="'background: ' + presets['{{ p }}'].surface_base"></span>
35
+ <span class="w-4 h-3 rounded-sm" :style="'background: ' + presets['{{ p }}'].surface_raised"></span>
36
+ <span class="w-4 h-3 rounded-sm" :style="'background: ' + presets['{{ p }}'].text_primary"></span>
37
+ </div>
38
+ </button>
39
+ {% endfor %}
40
+ </div>
41
+ </div>
42
+
43
+ {# Color Customization #}
44
+ <div class="fieldset">
45
+ <div class="fieldset__header">
46
+ <h2 class="fieldset__title">Colors</h2>
47
+ </div>
48
+ <div class="grid grid-cols-2 gap-4">
49
+ <div>
50
+ <label class="field-label">Primary Color</label>
51
+ <div class="flex items-center gap-2">
52
+ <input type="color" x-model="customizations.primary_color" class="h-10 w-14 rounded border border-[var(--surface-border)] cursor-pointer">
53
+ <input type="text" x-model="customizations.primary_color" class="form-input flex-1">
54
+ </div>
55
+ </div>
56
+ <div>
57
+ <label class="field-label">Surface Background</label>
58
+ <div class="flex items-center gap-2">
59
+ <input type="color" x-model="customizations.surface_base" class="h-10 w-14 rounded border border-[var(--surface-border)] cursor-pointer">
60
+ <input type="text" x-model="customizations.surface_base" class="form-input flex-1">
61
+ </div>
62
+ </div>
63
+ <div>
64
+ <label class="field-label">Card Background</label>
65
+ <div class="flex items-center gap-2">
66
+ <input type="color" x-model="customizations.surface_raised" class="h-10 w-14 rounded border border-[var(--surface-border)] cursor-pointer">
67
+ <input type="text" x-model="customizations.surface_raised" class="form-input flex-1">
68
+ </div>
69
+ </div>
70
+ <div>
71
+ <label class="field-label">Text Color</label>
72
+ <div class="flex items-center gap-2">
73
+ <input type="color" x-model="customizations.text_primary" class="h-10 w-14 rounded border border-[var(--surface-border)] cursor-pointer">
74
+ <input type="text" x-model="customizations.text_primary" class="form-input flex-1">
75
+ </div>
76
+ </div>
77
+ <div>
78
+ <label class="field-label">Border Color</label>
79
+ <div class="flex items-center gap-2">
80
+ <input type="color" x-model="customizations.border_color" class="h-10 w-14 rounded border border-[var(--surface-border)] cursor-pointer">
81
+ <input type="text" x-model="customizations.border_color" class="form-input flex-1">
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ {# Border Radius #}
88
+ <div class="fieldset">
89
+ <div class="fieldset__header">
90
+ <h2 class="fieldset__title">Border Radius</h2>
91
+ </div>
92
+ <div class="grid grid-cols-3 gap-4">
93
+ <div>
94
+ <label class="field-label">Small</label>
95
+ <input type="text" x-model="customizations.radius_sm" class="form-input">
96
+ </div>
97
+ <div>
98
+ <label class="field-label">Medium</label>
99
+ <input type="text" x-model="customizations.radius_md" class="form-input">
100
+ </div>
101
+ <div>
102
+ <label class="field-label">Large</label>
103
+ <input type="text" x-model="customizations.radius_lg" class="form-input">
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ {# Feature Toggles #}
109
+ <div class="fieldset">
110
+ <div class="fieldset__header">
111
+ <h2 class="fieldset__title">Features</h2>
112
+ </div>
113
+ <div class="space-y-3">
114
+ <label class="flex items-center gap-3 cursor-pointer">
115
+ <input type="checkbox" x-model="customizations.show_grain" class="table-checkbox">
116
+ <span class="text-sm">Show grain texture overlay</span>
117
+ </label>
118
+ <label class="flex items-center gap-3 cursor-pointer">
119
+ <input type="checkbox" x-model="customizations.show_accent_line" class="table-checkbox">
120
+ <span class="text-sm">Show topbar accent line</span>
121
+ </label>
122
+ </div>
123
+ </div>
124
+
125
+ {# Save Button #}
126
+ <div class="flex items-center gap-3">
127
+ <button @click="save()" class="btn btn-primary">
128
+ Save Theme
129
+ </button>
130
+ <button @click="reset()" class="btn btn-secondary">
131
+ Reset to Default
132
+ </button>
133
+ <span x-show="saved" x-transition class="text-sm text-[var(--success-500)]">Saved!</span>
134
+ </div>
135
+ </div>
136
+
137
+ {# Live Preview Panel #}
138
+ <div class="space-y-4">
139
+ <div class="fieldset">
140
+ <div class="fieldset__header">
141
+ <h2 class="fieldset__title">Preview</h2>
142
+ </div>
143
+ <div class="space-y-3 p-4 rounded-lg border border-[var(--surface-border)]"
144
+ :style="'background: ' + customizations.surface_base + '; color: ' + customizations.text_primary">
145
+ <div class="flex items-center gap-2">
146
+ <div class="w-8 h-8 rounded-full" :style="'background: ' + customizations.primary_color"></div>
147
+ <span class="font-medium">Sample Card</span>
148
+ </div>
149
+ <div class="p-3 rounded-lg" :style="'background: ' + customizations.surface_raised + '; border: 1px solid ' + customizations.border_color">
150
+ <div class="text-xs opacity-60 mb-1">Card Title</div>
151
+ <div class="text-2xl font-bold" :style="'color: ' + customizations.primary_color">1,234</div>
152
+ </div>
153
+ <div class="flex gap-2">
154
+ <button class="px-3 py-1.5 rounded text-xs text-white" :style="'background: ' + customizations.primary_color">Primary</button>
155
+ <button class="px-3 py-1.5 rounded text-xs border" :style="'border-color: ' + customizations.border_color + '; color: ' + customizations.text_primary">Secondary</button>
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ {# Export #}
161
+ <div class="fieldset">
162
+ <div class="fieldset__header">
163
+ <h2 class="fieldset__title">Python Config</h2>
164
+ </div>
165
+ <pre class="text-xs font-mono p-3 rounded-lg bg-[var(--surface-inset)] overflow-auto max-h-64"><code x-text="exportConfig()"></code></pre>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <script>
172
+ function themeBuilder() {
173
+ return {
174
+ preset: '{{ theme_preset | default("editorial") }}',
175
+ saved: false,
176
+ presets: {
177
+ editorial: { primary_color: '#059669', surface_base: '#FAF8F5', surface_raised: '#FFFFFF', text_primary: '#1C1917', border_color: '#E8E4DE', radius_sm: '3px', radius_md: '5px', radius_lg: '8px' },
178
+ modern: { primary_color: '#6366F1', surface_base: '#F8FAFC', surface_raised: '#FFFFFF', text_primary: '#0F172A', border_color: '#E2E8F0', radius_sm: '6px', radius_md: '8px', radius_lg: '12px' },
179
+ midnight: { primary_color: '#818CF8', surface_base: '#0B0F19', surface_raised: '#151B2B', text_primary: '#E2E8F0', border_color: '#2A3248', radius_sm: '4px', radius_md: '6px', radius_lg: '10px' },
180
+ paper: { primary_color: '#059669', surface_base: '#FAF8F5', surface_raised: '#FFFFFF', text_primary: '#1C1917', border_color: '#E8E4DE', radius_sm: '3px', radius_md: '5px', radius_lg: '8px' },
181
+ forest: { primary_color: '#22C55E', surface_base: '#F0FDF4', surface_raised: '#FFFFFF', text_primary: '#14532D', border_color: '#BBF7D0', radius_sm: '4px', radius_md: '6px', radius_lg: '10px' },
182
+ minimal: { primary_color: '#404040', surface_base: '#FFFFFF', surface_raised: '#FFFFFF', text_primary: '#171717', border_color: '#E5E5E5', radius_sm: '0px', radius_md: '0px', radius_lg: '0px' },
183
+ },
184
+ customizations: {
185
+ primary_color: '#059669',
186
+ surface_base: '#FAF8F5',
187
+ surface_raised: '#FFFFFF',
188
+ text_primary: '#1C1917',
189
+ border_color: '#E8E4DE',
190
+ radius_sm: '3px',
191
+ radius_md: '5px',
192
+ radius_lg: '8px',
193
+ show_grain: true,
194
+ show_accent_line: true,
195
+ },
196
+
197
+ init() {
198
+ const saved = JSON.parse(localStorage.getItem('admin_theme_custom') || 'null');
199
+ if (saved) {
200
+ Object.assign(this.customizations, saved);
201
+ this.preset = saved.preset || this.preset;
202
+ }
203
+ },
204
+
205
+ applyPreset(name) {
206
+ this.preset = name;
207
+ const p = this.presets[name];
208
+ if (p) {
209
+ this.customizations.primary_color = p.primary_color;
210
+ this.customizations.surface_base = p.surface_base;
211
+ this.customizations.surface_raised = p.surface_raised;
212
+ this.customizations.text_primary = p.text_primary;
213
+ this.customizations.border_color = p.border_color;
214
+ this.customizations.radius_sm = p.radius_sm;
215
+ this.customizations.radius_md = p.radius_md;
216
+ this.customizations.radius_lg = p.radius_lg;
217
+ }
218
+ this.applyLive();
219
+ },
220
+
221
+ applyLive() {
222
+ const root = document.documentElement;
223
+ root.style.setProperty('--primary-500', this.customizations.primary_color);
224
+ root.style.setProperty('--surface-base', this.customizations.surface_base);
225
+ root.style.setProperty('--surface-raised', this.customizations.surface_raised);
226
+ root.style.setProperty('--text-primary', this.customizations.text_primary);
227
+ root.style.setProperty('--surface-border', this.customizations.border_color);
228
+ root.style.setProperty('--radius-sm', this.customizations.radius_sm);
229
+ root.style.setProperty('--radius-md', this.customizations.radius_md);
230
+ root.style.setProperty('--radius-lg', this.customizations.radius_lg);
231
+ },
232
+
233
+ save() {
234
+ localStorage.setItem('admin_theme_preset', this.preset);
235
+ localStorage.setItem('admin_theme_custom', JSON.stringify({ ...this.customizations, preset: this.preset }));
236
+ document.documentElement.setAttribute('data-preset', this.preset);
237
+ this.applyLive();
238
+ this.saved = true;
239
+ setTimeout(() => this.saved = false, 2000);
240
+ },
241
+
242
+ reset() {
243
+ localStorage.removeItem('admin_theme_preset');
244
+ localStorage.removeItem('admin_theme_custom');
245
+ document.documentElement.setAttribute('data-preset', 'editorial');
246
+ location.reload();
247
+ },
248
+
249
+ exportConfig() {
250
+ return `from fastapi_admin_kit import Admin\nfrom fastapi_admin_kit.config import ThemeConfig\n\nadmin = Admin(\n theme=ThemeConfig(\n preset="${this.preset}",\n primary_color="${this.customizations.primary_color}",\n surface_base="${this.customizations.surface_base}",\n surface_raised="${this.customizations.surface_raised}",\n text_primary="${this.customizations.text_primary}",\n border_color="${this.customizations.border_color}",\n radius_sm="${this.customizations.radius_sm}",\n radius_md="${this.customizations.radius_md}",\n radius_lg="${this.customizations.radius_lg}",\n show_grain_texture=${this.customizations.show_grain},\n show_accent_line=${this.customizations.show_accent_line},\n ),\n)`;
251
+ },
252
+ };
253
+ }
254
+ </script>
255
+ {% endblock %}