169 lines
6.3 KiB
HTML
169 lines
6.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="{{ current_lang }}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
<title>{% block title %}oMLX Admin{% endblock %}</title>
|
|
<link rel="icon" type="image/svg+xml" href="/admin/static/favicon.svg">
|
|
|
|
<!-- Inter Font (self-hosted, Latin) -->
|
|
<link rel="stylesheet" href="/admin/static/fonts/inter/inter.css">
|
|
|
|
<!-- CJK Font (conditional, self-hosted) -->
|
|
{% if current_lang == 'zh' %}
|
|
<link rel="stylesheet" href="/admin/static/fonts/noto-sans-sc/noto-sans-sc.css">
|
|
{% elif current_lang == 'zh-TW' %}
|
|
<link rel="stylesheet" href="/admin/static/fonts/noto-sans-tc/noto-sans-tc.css">
|
|
{% elif current_lang == 'ko' %}
|
|
<link rel="stylesheet" href="/admin/static/fonts/noto-sans-kr/noto-sans-kr.css">
|
|
{% elif current_lang == 'ja' %}
|
|
<link rel="stylesheet" href="/admin/static/fonts/noto-sans-jp/noto-sans-jp.css">
|
|
{% endif %}
|
|
|
|
<!-- Tailwind CSS (pre-compiled) -->
|
|
<link rel="stylesheet" href="{{ static('css/tailwind.css') }}">
|
|
|
|
<!-- Alpine.js -->
|
|
<script defer src="/admin/static/js/alpine.min.js"></script>
|
|
|
|
<!-- Lucide Icons -->
|
|
<script src="/admin/static/js/lucide.min.js"></script>
|
|
|
|
<script>
|
|
// Apply theme immediately to prevent flash of wrong theme
|
|
(function() {
|
|
var stored = localStorage.getItem('omlx-chat-theme');
|
|
var theme = stored || 'auto';
|
|
if (theme === 'auto') {
|
|
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
})();
|
|
</script>
|
|
|
|
<script>
|
|
// i18n locale strings
|
|
window._t = {{ locale_json | safe }};
|
|
window.t = function(key) { return window._t[key] !== undefined ? window._t[key] : key; };
|
|
</script>
|
|
|
|
<style>
|
|
/* Lucide icon fallback: ensure unprocessed <i data-lucide> elements
|
|
are inline-block so Tailwind w-*/h-* classes apply even if
|
|
lucide.min.js fails to load or createIcons() hasn't run yet. */
|
|
[data-lucide] {
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Language-specific font stack */
|
|
:root {
|
|
{% if current_lang == 'ko' %}
|
|
--font-sans: 'Noto Sans KR', 'Inter', system-ui, sans-serif;
|
|
{% elif current_lang == 'zh' %}
|
|
--font-sans: 'Noto Sans SC', 'Inter', system-ui, sans-serif;
|
|
{% elif current_lang == 'zh-TW' %}
|
|
--font-sans: 'Noto Sans TC', 'Inter', system-ui, sans-serif;
|
|
{% elif current_lang == 'ja' %}
|
|
--font-sans: 'Noto Sans JP', 'Inter', system-ui, sans-serif;
|
|
{% else %}
|
|
--font-sans: 'Inter', system-ui, sans-serif;
|
|
{% endif %}
|
|
}
|
|
|
|
body {
|
|
font-family: var(--font-sans) !important;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
[x-cloak] { display: none !important; }
|
|
|
|
/* Subtle grain texture for background */
|
|
.bg-grain {
|
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.03'/%3E%3C/svg%3E");
|
|
}
|
|
|
|
/* Card hover effect */
|
|
.card-hover {
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
}
|
|
.card-hover:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 20px 40px -10px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
/* Input focus ring */
|
|
input:focus, select:focus, textarea:focus {
|
|
outline: none;
|
|
box-shadow: 0 0 0 2px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
/* Benchmark tooltip (fixed position, outside overflow containers) */
|
|
.bench-tip-fixed {
|
|
position: fixed;
|
|
z-index: 9999;
|
|
max-width: 320px;
|
|
padding: 10px 12px;
|
|
background: #171717;
|
|
color: #e5e5e5;
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
line-height: 1.5;
|
|
border-radius: 8px;
|
|
white-space: normal;
|
|
text-align: left;
|
|
pointer-events: none;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
}
|
|
</style>
|
|
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
<body class="font-sans bg-white text-neutral-900 antialiased selection:bg-neutral-900 selection:text-white">
|
|
{% block content %}{% endblock %}
|
|
|
|
<script>
|
|
// Lucide icon processor — bypasses createIcons() which has Safari issues.
|
|
// Directly creates SVGs from lucide.icons definitions.
|
|
(function() {
|
|
if (typeof lucide === 'undefined' || !lucide.icons) return;
|
|
|
|
function toPascal(s) {
|
|
return s.replace(/(^|[-_ ])(\w)/g, function(m, sep, c) { return c.toUpperCase(); });
|
|
}
|
|
|
|
function replaceIcon(el) {
|
|
var name = el.getAttribute('data-lucide');
|
|
if (!name) return;
|
|
var def = lucide.icons[toPascal(name)];
|
|
if (!def) return;
|
|
var svg = lucide.createElement(def);
|
|
Array.from(el.attributes).forEach(function(a) {
|
|
if (a.name !== 'data-lucide') svg.setAttribute(a.name, a.value);
|
|
});
|
|
svg.classList.add('lucide', 'lucide-' + name);
|
|
if (el.parentNode) el.parentNode.replaceChild(svg, el);
|
|
}
|
|
|
|
function processAll() {
|
|
document.querySelectorAll('i[data-lucide]').forEach(replaceIcon);
|
|
}
|
|
|
|
// Initial pass
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', processAll);
|
|
} else {
|
|
processAll();
|
|
}
|
|
|
|
// Poll for dynamically added icons (Alpine x-for, x-if, modals).
|
|
setInterval(function() {
|
|
if (document.querySelector('i[data-lucide]')) processAll();
|
|
}, 300);
|
|
})();
|
|
</script>
|
|
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|