flask-admin-tabler 0.1.1__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 (43) hide show
  1. flask_admin_tabler/__init__.py +18 -0
  2. flask_admin_tabler/json_widget.py +363 -0
  3. flask_admin_tabler/static/admin/css/tabler/admin.css +90 -0
  4. flask_admin_tabler/static/admin/css/tabler/rediscli.css +48 -0
  5. flask_admin_tabler/static/tabler/css/tabler-themes.min.css +8 -0
  6. flask_admin_tabler/static/tabler/css/tabler.min.css +9 -0
  7. flask_admin_tabler/static/tabler/icons/fonts/tabler-icons.ttf +0 -0
  8. flask_admin_tabler/static/tabler/icons/fonts/tabler-icons.woff +0 -0
  9. flask_admin_tabler/static/tabler/icons/fonts/tabler-icons.woff2 +0 -0
  10. flask_admin_tabler/static/tabler/icons/tabler-icons.min.css +4 -0
  11. flask_admin_tabler/static/tabler/js/tabler.min.js +13 -0
  12. flask_admin_tabler/static/vendor/highlightjs/highlight.min.js +1244 -0
  13. flask_admin_tabler/static/vendor/highlightjs/styles/googlecode.min.css +1 -0
  14. flask_admin_tabler/static/vendor/highlightjs/styles/monokai.min.css +1 -0
  15. flask_admin_tabler/templates/tabler/admin/actions.html +38 -0
  16. flask_admin_tabler/templates/tabler/admin/base.html +219 -0
  17. flask_admin_tabler/templates/tabler/admin/file/form.html +29 -0
  18. flask_admin_tabler/templates/tabler/admin/file/list.html +210 -0
  19. flask_admin_tabler/templates/tabler/admin/file/modals/form.html +19 -0
  20. flask_admin_tabler/templates/tabler/admin/index.html +4 -0
  21. flask_admin_tabler/templates/tabler/admin/layout.html +109 -0
  22. flask_admin_tabler/templates/tabler/admin/lib.html +290 -0
  23. flask_admin_tabler/templates/tabler/admin/master.html +1 -0
  24. flask_admin_tabler/templates/tabler/admin/model/create.html +30 -0
  25. flask_admin_tabler/templates/tabler/admin/model/details.html +52 -0
  26. flask_admin_tabler/templates/tabler/admin/model/edit.html +40 -0
  27. flask_admin_tabler/templates/tabler/admin/model/inline_field_list.html +15 -0
  28. flask_admin_tabler/templates/tabler/admin/model/inline_form.html +4 -0
  29. flask_admin_tabler/templates/tabler/admin/model/inline_list_base.html +45 -0
  30. flask_admin_tabler/templates/tabler/admin/model/layout.html +107 -0
  31. flask_admin_tabler/templates/tabler/admin/model/list.html +198 -0
  32. flask_admin_tabler/templates/tabler/admin/model/modals/create.html +26 -0
  33. flask_admin_tabler/templates/tabler/admin/model/modals/details.html +40 -0
  34. flask_admin_tabler/templates/tabler/admin/model/modals/edit.html +27 -0
  35. flask_admin_tabler/templates/tabler/admin/model/row_actions.html +38 -0
  36. flask_admin_tabler/templates/tabler/admin/rediscli/console.html +27 -0
  37. flask_admin_tabler/templates/tabler/admin/rediscli/response.html +32 -0
  38. flask_admin_tabler/templates/tabler/admin/static.html +3 -0
  39. flask_admin_tabler/theme.py +78 -0
  40. flask_admin_tabler-0.1.1.dist-info/METADATA +86 -0
  41. flask_admin_tabler-0.1.1.dist-info/RECORD +43 -0
  42. flask_admin_tabler-0.1.1.dist-info/WHEEL +4 -0
  43. flask_admin_tabler-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,18 @@
1
+ from flask_admin_tabler.json_widget import (
2
+ JsonColumnsMixin,
3
+ JsonTextAreaWidget,
4
+ json_detail_formatter,
5
+ json_list_formatter,
6
+ )
7
+ from flask_admin_tabler.theme import TablerTheme
8
+
9
+ tabler_bool_formatter = TablerTheme.bool_formatter
10
+
11
+ __all__ = [
12
+ "TablerTheme",
13
+ "tabler_bool_formatter",
14
+ "JsonColumnsMixin",
15
+ "JsonTextAreaWidget",
16
+ "json_detail_formatter",
17
+ "json_list_formatter",
18
+ ]
@@ -0,0 +1,363 @@
1
+ """flask_admin_tabler.json_widget
2
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3
+ Reusable JSON column support for Flask-Admin ModelViews.
4
+
5
+ Usage::
6
+
7
+ from flask_admin_tabler import JsonColumnsMixin
8
+
9
+ class MyView(JsonColumnsMixin, SecureModelView):
10
+ json_columns = ["config", "metadata"]
11
+
12
+ The mixin automatically:
13
+ - Shows a compact badge in the list view for JSON objects/arrays.
14
+ - Shows pretty-printed, syntax-highlighted JSON in the details view.
15
+ - Adds a "Preview JSON" button in the edit / create forms.
16
+ - Minifies JSON values before saving (strips unnecessary whitespace).
17
+ """
18
+
19
+ import json
20
+ import typing as t
21
+
22
+ from markupsafe import Markup, escape
23
+ from wtforms.widgets import TextArea
24
+
25
+ _HLJS = "/static/flask_admin_tabler/vendor/highlightjs"
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # One-time per-page initialisation block.
29
+ # window.__jsonToolsInit guards against double-execution when multiple JSON
30
+ # fields or formatters inject this snippet on the same page.
31
+ # All DOM construction uses createElement/appendChild — no innerHTML.
32
+ # ---------------------------------------------------------------------------
33
+ _ONCE_SCRIPT = Markup(
34
+ """<script>
35
+ (function () {
36
+ if (window.__jsonToolsInit) return;
37
+ window.__jsonToolsInit = true;
38
+
39
+ /* ---- helpers --------------------------------------------------------- */
40
+ function mkIcon(cls) {
41
+ var el = document.createElement('i');
42
+ el.className = cls;
43
+ return el;
44
+ }
45
+ function loadLink(id, href) {
46
+ var existing = document.getElementById(id);
47
+ if (existing) return existing;
48
+ var link = document.createElement('link');
49
+ link.id = id; link.rel = 'stylesheet'; link.href = href;
50
+ document.head.appendChild(link);
51
+ return link;
52
+ }
53
+ function copyText(text, btn) {
54
+ navigator.clipboard.writeText(text).then(function () {
55
+ btn.textContent = '';
56
+ btn.appendChild(mkIcon('ti ti-check text-success'));
57
+ btn.appendChild(document.createTextNode(' Copied!'));
58
+ setTimeout(function () {
59
+ btn.textContent = '';
60
+ btn.appendChild(mkIcon('ti ti-copy'));
61
+ }, 1500);
62
+ });
63
+ }
64
+
65
+ /* ---- inject styles --------------------------------------------------- */
66
+ var st = document.createElement('style');
67
+ st.textContent = [
68
+ '.json-viewer{border-radius:4px;overflow:auto;font-size:.82em;margin:0;padding:1rem}',
69
+ '.json-viewer-wrapper{position:relative}',
70
+ '.json-copy-btn{position:absolute;top:.4rem;right:.4rem;opacity:.55;transition:opacity .15s;z-index:1}',
71
+ '.json-copy-btn:hover{opacity:1}',
72
+ '#json-preview-modal .json-viewer{max-height:70vh}',
73
+ ].join('');
74
+ document.head.appendChild(st);
75
+
76
+ /* ---- inject modal (DOM only) ----------------------------------------- */
77
+ if (!document.getElementById('json-preview-modal')) {
78
+ /* hidden trigger for Bootstrap's data-bs-toggle delegation */
79
+ var trigger = document.createElement('button');
80
+ trigger.type = 'button';
81
+ trigger.id = 'json-preview-trigger';
82
+ trigger.setAttribute('data-bs-toggle', 'modal');
83
+ trigger.setAttribute('data-bs-target', '#json-preview-modal');
84
+ trigger.style.display = 'none';
85
+ trigger.setAttribute('aria-hidden', 'true');
86
+ document.body.appendChild(trigger);
87
+
88
+ var modal = document.createElement('div');
89
+ modal.className = 'modal fade';
90
+ modal.id = 'json-preview-modal';
91
+ modal.tabIndex = -1;
92
+ modal.setAttribute('aria-hidden', 'true');
93
+
94
+ var dialog = document.createElement('div');
95
+ dialog.className = 'modal-dialog modal-xl modal-dialog-scrollable';
96
+
97
+ var content = document.createElement('div');
98
+ content.className = 'modal-content';
99
+
100
+ /* header */
101
+ var hdr = document.createElement('div');
102
+ hdr.className = 'modal-header';
103
+ var ttl = document.createElement('h5');
104
+ ttl.className = 'modal-title';
105
+ ttl.appendChild(mkIcon('ti ti-code me-2'));
106
+ ttl.appendChild(document.createTextNode('JSON Preview'));
107
+ var xBtn = document.createElement('button');
108
+ xBtn.type = 'button';
109
+ xBtn.className = 'btn-close';
110
+ xBtn.setAttribute('data-bs-dismiss', 'modal');
111
+ hdr.appendChild(ttl);
112
+ hdr.appendChild(xBtn);
113
+
114
+ /* body */
115
+ var bdy = document.createElement('div');
116
+ bdy.className = 'modal-body p-0';
117
+ var pre = document.createElement('pre');
118
+ pre.className = 'json-viewer mb-0 rounded-0';
119
+ var code = document.createElement('code');
120
+ code.className = 'language-json';
121
+ code.id = 'json-preview-code';
122
+ pre.appendChild(code);
123
+ bdy.appendChild(pre);
124
+
125
+ /* footer */
126
+ var ftr = document.createElement('div');
127
+ ftr.className = 'modal-footer';
128
+ var cpBtn = document.createElement('button');
129
+ cpBtn.type = 'button';
130
+ cpBtn.id = 'json-preview-copy-btn';
131
+ cpBtn.className = 'btn btn-sm btn-secondary';
132
+ cpBtn.appendChild(mkIcon('ti ti-copy me-1'));
133
+ cpBtn.appendChild(document.createTextNode('Copy'));
134
+ cpBtn.addEventListener('click', function () {
135
+ copyText(document.getElementById('json-preview-code').innerText, cpBtn);
136
+ });
137
+ var clBtn = document.createElement('button');
138
+ clBtn.type = 'button';
139
+ clBtn.className = 'btn btn-sm btn-secondary';
140
+ clBtn.setAttribute('data-bs-dismiss', 'modal');
141
+ clBtn.textContent = 'Close';
142
+ ftr.appendChild(cpBtn);
143
+ ftr.appendChild(clBtn);
144
+
145
+ content.appendChild(hdr);
146
+ content.appendChild(bdy);
147
+ content.appendChild(ftr);
148
+ dialog.appendChild(content);
149
+ modal.appendChild(dialog);
150
+ document.body.appendChild(modal);
151
+ }
152
+
153
+ /* ---- CSS themes ------------------------------------------------------ */
154
+ var lightCss = loadLink('hljs-light', '%%HLJS%%/styles/googlecode.min.css');
155
+ var darkCss = loadLink('hljs-dark', '%%HLJS%%/styles/monokai.min.css');
156
+ function applyTheme() {
157
+ var dark = document.documentElement.getAttribute('data-bs-theme') === 'dark';
158
+ lightCss.disabled = dark;
159
+ darkCss.disabled = !dark;
160
+ }
161
+ applyTheme();
162
+ new MutationObserver(applyTheme).observe(document.documentElement, {
163
+ attributes: true, attributeFilter: ['data-bs-theme']
164
+ });
165
+
166
+ /* ---- load highlight.js then wire everything up ----------------------- */
167
+ var hljsEl = document.createElement('script');
168
+ hljsEl.src = '%%HLJS%%/highlight.min.js';
169
+ hljsEl.onload = function () {
170
+ /* highlight existing .json-viewer blocks and add copy buttons */
171
+ document.querySelectorAll('.json-viewer code').forEach(function (c) {
172
+ hljs.highlightElement(c);
173
+ var p = c.parentElement;
174
+ var w = document.createElement('div');
175
+ w.className = 'json-viewer-wrapper';
176
+ p.parentNode.insertBefore(w, p);
177
+ w.appendChild(p);
178
+ var b = document.createElement('button');
179
+ b.type = 'button';
180
+ b.className = 'json-copy-btn btn btn-sm btn-ghost-secondary';
181
+ b.title = 'Copy JSON';
182
+ b.appendChild(mkIcon('ti ti-copy'));
183
+ b.addEventListener('click', function () { copyText(c.innerText, b); });
184
+ w.appendChild(b);
185
+ });
186
+
187
+ /* event delegation for .json-preview-btn (edit / create forms) */
188
+ document.addEventListener('click', function (e) {
189
+ var btn = e.target.closest('.json-preview-btn');
190
+ if (!btn) return;
191
+ var field = document.getElementById(btn.dataset.jsonTarget);
192
+ if (!field) return;
193
+ var raw = field.value.trim();
194
+ if (!raw) { alert('The value field is empty.'); return; }
195
+ var parsed;
196
+ try { parsed = JSON.parse(raw); }
197
+ catch (err) { alert('Invalid JSON:\\n' + err.message); return; }
198
+ if (typeof parsed !== 'object' || parsed === null) {
199
+ alert('Value is not a JSON object or array.'); return;
200
+ }
201
+ var codeEl = document.getElementById('json-preview-code');
202
+ codeEl.textContent = JSON.stringify(parsed, null, 2);
203
+ delete codeEl.dataset.highlighted;
204
+ hljs.highlightElement(codeEl);
205
+ document.getElementById('json-preview-trigger').click();
206
+ });
207
+ };
208
+ document.head.appendChild(hljsEl);
209
+ })();
210
+ </script>""".replace("%%HLJS%%", _HLJS)
211
+ )
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # WTForms widget
216
+ # ---------------------------------------------------------------------------
217
+
218
+
219
+ class JsonTextAreaWidget:
220
+ """WTForms widget that renders a textarea with a JSON preview button.
221
+
222
+ Injects the one-time initialisation script on first use per page.
223
+ """
224
+
225
+ def __call__(self, field: t.Any, **kwargs: t.Any) -> Markup:
226
+ textarea = TextArea()(field, **kwargs)
227
+ btn = Markup(
228
+ f'<button type="button"'
229
+ f' class="btn btn-sm btn-secondary mt-1 json-preview-btn"'
230
+ f' data-json-target="{escape(field.id)}">'
231
+ f'<i class="ti ti-eye me-1"></i>Preview JSON</button>'
232
+ )
233
+ return textarea + btn + _ONCE_SCRIPT
234
+
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # Internal helper
238
+ # ---------------------------------------------------------------------------
239
+
240
+
241
+ def _parse_json_value(value: t.Any) -> dict | list | None:
242
+ """Return a parsed JSON object/array from *value*, or None.
243
+
244
+ Handles both:
245
+ - ``str`` — Text columns that store JSON as a string (uses ``json.loads``).
246
+ - ``dict`` / ``list`` — SQLAlchemy JSON columns that return Python objects directly.
247
+ """
248
+ if isinstance(value, (dict, list)):
249
+ return value
250
+ if isinstance(value, str) and value:
251
+ try:
252
+ parsed = json.loads(value)
253
+ if isinstance(parsed, (dict, list)):
254
+ return parsed
255
+ except (ValueError, TypeError):
256
+ pass
257
+ return None
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Column formatters
262
+ # ---------------------------------------------------------------------------
263
+
264
+
265
+ def json_list_formatter(view: t.Any, context: t.Any, model: t.Any, name: str) -> Markup:
266
+ """List-view formatter: compact badge for JSON objects/arrays."""
267
+ value = getattr(model, name, None)
268
+ if value is None:
269
+ return Markup("")
270
+ parsed = _parse_json_value(value)
271
+ if parsed is not None:
272
+ tip = escape(json.dumps(parsed, separators=(",", ":"), ensure_ascii=False))
273
+ if isinstance(parsed, dict):
274
+ label = f"{{ {len(parsed)} keys }}"
275
+ return Markup(f'<span class="badge bg-green-lt" title="{tip}">{escape(label)}</span>')
276
+ label = f"[ {len(parsed)} items ]"
277
+ return Markup(f'<span class="badge bg-blue-lt" title="{tip}">{escape(label)}</span>')
278
+ # Plain string fallback
279
+ text = str(value)
280
+ if len(text) > 80:
281
+ return Markup(f'<span title="{escape(text)}">{escape(text[:77])}…</span>')
282
+ return Markup(escape(text))
283
+
284
+
285
+ def json_detail_formatter(view: t.Any, context: t.Any, model: t.Any, name: str) -> Markup:
286
+ """Details-view formatter: pretty-printed, syntax-highlighted JSON."""
287
+ value = getattr(model, name, None)
288
+ if value is None:
289
+ return Markup("")
290
+ parsed = _parse_json_value(value)
291
+ if parsed is not None:
292
+ formatted = json.dumps(parsed, indent=2, ensure_ascii=False)
293
+ return (
294
+ Markup(f'<pre class="json-viewer"><code class="language-json">{escape(formatted)}</code></pre>')
295
+ + _ONCE_SCRIPT
296
+ )
297
+ return Markup(escape(str(value)))
298
+
299
+
300
+ # ---------------------------------------------------------------------------
301
+ # ModelView mixin
302
+ # ---------------------------------------------------------------------------
303
+
304
+
305
+ class JsonColumnsMixin:
306
+ """Mixin for Flask-Admin ModelView that adds JSON column support.
307
+
308
+ Set ``json_columns`` to the names of columns that hold JSON values::
309
+
310
+ class MyView(JsonColumnsMixin, ModelView):
311
+ json_columns = ["config", "metadata"]
312
+
313
+ The mixin merges formatters and widget args at instantiation time so it
314
+ never mutates shared class-level dicts. Any formatters or widget args
315
+ already defined on the subclass take precedence (``setdefault`` semantics).
316
+ """
317
+
318
+ json_columns: t.ClassVar[list[str]] = []
319
+
320
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
321
+ if self.json_columns:
322
+ # Copy existing dicts — never mutate shared class-level objects.
323
+ cf = dict(getattr(self, "column_formatters", {}) or {})
324
+ cfd = dict(getattr(self, "column_formatters_detail", {}) or {})
325
+ fwa = dict(getattr(self, "form_widget_args", {}) or {})
326
+
327
+ for col in self.json_columns:
328
+ cf.setdefault(col, json_list_formatter)
329
+ cfd.setdefault(col, json_detail_formatter)
330
+ fwa.setdefault(col, {"widget": JsonTextAreaWidget()})
331
+
332
+ # Set as instance attributes so Flask-Admin's __init__ picks them up.
333
+ self.column_formatters = cf
334
+ self.column_formatters_detail = cfd
335
+ self.form_widget_args = fwa
336
+
337
+ super().__init__(*args, **kwargs)
338
+
339
+ def on_model_change(self, form: t.Any, model: t.Any, is_created: bool) -> None:
340
+ """Minify JSON string values before persisting to the database.
341
+
342
+ Only applies to Text columns that store JSON as a string. Native
343
+ dict/list values (SQLAlchemy JSON columns) are left untouched —
344
+ SQLAlchemy serialises those itself.
345
+ """
346
+ for col in self.json_columns:
347
+ value = getattr(model, col, None)
348
+ if isinstance(value, str) and value:
349
+ try:
350
+ parsed = json.loads(value)
351
+ if isinstance(parsed, (dict, list)):
352
+ setattr(
353
+ model,
354
+ col,
355
+ json.dumps(
356
+ parsed,
357
+ separators=(",", ":"),
358
+ ensure_ascii=False,
359
+ ),
360
+ )
361
+ except (ValueError, TypeError):
362
+ pass
363
+ super().on_model_change(form, model, is_created)
@@ -0,0 +1,90 @@
1
+ /* Flask-Admin Tabler Theme — admin.css
2
+ Minimal overrides to make Flask-Admin widgets blend with Tabler 1.4.0.
3
+ */
4
+
5
+ /* --- General tweaks --- */
6
+ .admin-form fieldset {
7
+ border: none;
8
+ padding: 0;
9
+ margin: 0;
10
+ }
11
+
12
+ .submit-row {
13
+ display: flex;
14
+ gap: 0.5rem;
15
+ flex-wrap: wrap;
16
+ }
17
+
18
+ /* --- List view --- */
19
+ .model-list th.list-checkbox-column,
20
+ .model-list td.list-checkbox-column {
21
+ width: 2rem;
22
+ text-align: center;
23
+ }
24
+
25
+ .list-buttons-column {
26
+ white-space: nowrap;
27
+ }
28
+
29
+ .list-buttons-column form.icon {
30
+ display: inline;
31
+ }
32
+
33
+ .list-buttons-column form.icon button {
34
+ background: none;
35
+ border: none;
36
+ padding: 0 0.25rem;
37
+ cursor: pointer;
38
+ color: var(--tblr-secondary);
39
+ }
40
+
41
+ .list-buttons-column form.icon button:hover {
42
+ color: var(--tblr-danger);
43
+ }
44
+
45
+ .list-buttons-column a.icon {
46
+ padding: 0 0.25rem;
47
+ color: var(--tblr-secondary);
48
+ }
49
+
50
+ .list-buttons-column a.icon:hover {
51
+ color: var(--tblr-primary);
52
+ }
53
+
54
+ /* --- Inline fields --- */
55
+ .inline-field .inline-field-list .inline-field {
56
+ margin-bottom: 1rem;
57
+ }
58
+
59
+ .inline-remove-field {
60
+ color: var(--tblr-danger);
61
+ }
62
+
63
+ /* --- Filters --- */
64
+ .filters td {
65
+ padding: 0.25rem;
66
+ }
67
+
68
+ .field-filters .filter {
69
+ cursor: pointer;
70
+ }
71
+
72
+ /* --- Select2 integration --- */
73
+ .select2-container {
74
+ min-width: 100%;
75
+ }
76
+
77
+ /* --- Nav tabs used in model views --- */
78
+ .nav-tabs {
79
+ margin-bottom: 1rem;
80
+ }
81
+
82
+ /* --- Pagination alignment --- */
83
+ .pagination {
84
+ margin-top: 1rem;
85
+ }
86
+
87
+ /* --- Flash messages --- */
88
+ .alert-dismissible .btn-close {
89
+ padding: 0.75rem 1rem;
90
+ }
@@ -0,0 +1,48 @@
1
+ .console {
2
+ position: relative;
3
+ width: 100%;
4
+ min-height: 400px;
5
+ }
6
+
7
+ .console-container {
8
+ border-radius: 4px;
9
+ position: absolute;
10
+ border: 1px solid #d4d4d4;
11
+ padding: 2px;
12
+ overflow: scroll;
13
+ top: 2px;
14
+ left: 2px;
15
+ right: 2px;
16
+ bottom: 5em;
17
+ }
18
+
19
+ .console-line {
20
+ position: absolute;
21
+ left: 2px;
22
+ right: 2px;
23
+ bottom: 2px;
24
+ }
25
+
26
+ .console-line input {
27
+ width: 100%;
28
+ box-sizing: border-box;
29
+ -moz-box-sizing: border-box;
30
+ -webkit-box-sizing: border-box;
31
+ height: 2em;
32
+ }
33
+
34
+ .console .cmd {
35
+ background-color: #f5f5f5;
36
+ padding: 2px;
37
+ margin: 1px;
38
+ }
39
+
40
+ .console .response {
41
+ background-color: #f0f0f0;
42
+ padding: 2px;
43
+ margin: 1px;
44
+ }
45
+
46
+ .console .error {
47
+ color: red;
48
+ }
@@ -0,0 +1,8 @@
1
+ /*!
2
+ * Tabler v1.4.0 (https://tabler.io)
3
+ * Copyright 2018-2025 The Tabler Authors
4
+ * Copyright 2018-2025 codecalm.net Paweł Kuna
5
+ * Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)
6
+ */
7
+ [data-bs-theme-base=slate]{--tblr-gray-50:#f8fafc;--tblr-gray-100:#f1f5f9;--tblr-gray-200:#e2e8f0;--tblr-gray-300:#cbd5e1;--tblr-gray-400:#94a3b8;--tblr-gray-500:#64748b;--tblr-gray-600:#475569;--tblr-gray-700:#334155;--tblr-gray-800:#1e293b;--tblr-gray-900:#0f172a;--tblr-gray-950:#020617}[data-bs-theme-base=gray]{--tblr-gray-50:#f9fafb;--tblr-gray-100:#f3f4f6;--tblr-gray-200:#e5e7eb;--tblr-gray-300:#d1d5db;--tblr-gray-400:#9ca3af;--tblr-gray-500:#6b7280;--tblr-gray-600:#4b5563;--tblr-gray-700:#374151;--tblr-gray-800:#1f2937;--tblr-gray-900:#111827;--tblr-gray-950:#030712}[data-bs-theme-base=zinc]{--tblr-gray-50:#fafafa;--tblr-gray-100:#f4f4f5;--tblr-gray-200:#e4e4e7;--tblr-gray-300:#d4d4d8;--tblr-gray-400:#a1a1aa;--tblr-gray-500:#71717a;--tblr-gray-600:#52525b;--tblr-gray-700:#3f3f46;--tblr-gray-800:#27272a;--tblr-gray-900:#18181b;--tblr-gray-950:#09090b}[data-bs-theme-base=neutral]{--tblr-gray-50:#fafafa;--tblr-gray-100:#f5f5f5;--tblr-gray-200:#e5e5e5;--tblr-gray-300:#d4d4d4;--tblr-gray-400:#a3a3a3;--tblr-gray-500:#737373;--tblr-gray-600:#525252;--tblr-gray-700:#404040;--tblr-gray-800:#262626;--tblr-gray-900:#171717;--tblr-gray-950:#0a0a0a}[data-bs-theme-base=stone]{--tblr-gray-50:#fafaf9;--tblr-gray-100:#f5f5f4;--tblr-gray-200:#e7e5e4;--tblr-gray-300:#d6d3d1;--tblr-gray-400:#a8a29e;--tblr-gray-500:#78716c;--tblr-gray-600:#57534e;--tblr-gray-700:#44403c;--tblr-gray-800:#292524;--tblr-gray-900:#1c1917;--tblr-gray-950:#0c0a09}[data-bs-theme-base=pink]{--tblr-gray-50:#fdf2f8;--tblr-gray-100:#fce7f3;--tblr-gray-200:#fbcfe8;--tblr-gray-300:#f9a8d4;--tblr-gray-400:#f472b6;--tblr-gray-500:#ec4899;--tblr-gray-600:#db2777;--tblr-gray-700:#be185d;--tblr-gray-800:#9d174d;--tblr-gray-900:#831843;--tblr-gray-950:#500724}[data-bs-theme-primary=blue]{--tblr-primary:#066fd1;--tblr-primary-rgb:6,111,209}[data-bs-theme-primary=azure]{--tblr-primary:#4299e1;--tblr-primary-rgb:66,153,225}[data-bs-theme-primary=indigo]{--tblr-primary:#4263eb;--tblr-primary-rgb:66,99,235}[data-bs-theme-primary=purple]{--tblr-primary:#ae3ec9;--tblr-primary-rgb:174,62,201}[data-bs-theme-primary=pink]{--tblr-primary:#d6336c;--tblr-primary-rgb:214,51,108}[data-bs-theme-primary=red]{--tblr-primary:#d63939;--tblr-primary-rgb:214,57,57}[data-bs-theme-primary=orange]{--tblr-primary:#f76707;--tblr-primary-rgb:247,103,7}[data-bs-theme-primary=yellow]{--tblr-primary:#f59f00;--tblr-primary-rgb:245,159,0}[data-bs-theme-primary=lime]{--tblr-primary:#74b816;--tblr-primary-rgb:116,184,22}[data-bs-theme-primary=green]{--tblr-primary:#2fb344;--tblr-primary-rgb:47,179,68}[data-bs-theme-primary=teal]{--tblr-primary:#0ca678;--tblr-primary-rgb:12,166,120}[data-bs-theme-primary=cyan]{--tblr-primary:#17a2b8;--tblr-primary-rgb:23,162,184}[data-bs-theme-radius="0"]{--tblr-border-radius-scale:0}[data-bs-theme-radius="0.5"]{--tblr-border-radius-scale:0.5}[data-bs-theme-radius="1"]{--tblr-border-radius-scale:1}[data-bs-theme-radius="1.5"]{--tblr-border-radius-scale:1.5}[data-bs-theme-radius="2"]{--tblr-border-radius-scale:2}[data-bs-theme-primary=inverted]{--tblr-primary:var(--tblr-gray-800);--tblr-primary-fg:var(--tblr-light);--tblr-primary-rgb:31,41,55}[data-bs-theme-primary=inverted] [data-bs-theme=dark],[data-bs-theme-primary=inverted][data-bs-theme=dark]{--tblr-primary:#f9fafb;--tblr-primary-fg:var(--tblr-dark);--tblr-primary-rgb:249,250,251}[data-bs-theme-font=monospace]{--tblr-body-font-family:var(--tblr-font-monospace);--tblr-body-font-size:80%}[data-bs-theme-font=sans-serif]{--tblr-body-font-family:var(--tblr-font-sans-serif)}[data-bs-theme-font=serif]{--tblr-body-font-family:var(--tblr-font-serif)}[data-bs-theme-font=comic]{--tblr-body-font-family:var(--tblr-font-comic)}
8
+ /*# sourceMappingURL=tabler-themes.min.css.map */