1340 lines
45 KiB
HTML
1340 lines
45 KiB
HTML
<!doctype html>
|
|
<html lang="fa" dir="rtl">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>xFramework Theme Generator</title>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
body {
|
|
font-family: "Segoe UI", Tahoma, sans-serif;
|
|
background: linear-gradient(135deg, #1e1e2f 0%, #2a2a3e 100%);
|
|
color: #eee;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
header {
|
|
text-align: center;
|
|
padding: 30px 20px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 16px;
|
|
margin-bottom: 30px;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
header h1 {
|
|
font-size: 2.2em;
|
|
margin-bottom: 10px;
|
|
color: #7ec8ff;
|
|
}
|
|
header p {
|
|
opacity: 0.8;
|
|
}
|
|
header .meta {
|
|
margin-top: 12px;
|
|
font-size: 0.85em;
|
|
opacity: 0.6;
|
|
}
|
|
.container {
|
|
max-width: 1500px;
|
|
margin: 0 auto;
|
|
}
|
|
.section {
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
margin-bottom: 24px;
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
.section h2 {
|
|
margin-bottom: 20px;
|
|
color: #7ec8ff;
|
|
border-bottom: 2px solid rgba(126, 200, 255, 0.3);
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
/* Source Selection */
|
|
.source-tabs {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 20px;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
padding: 6px;
|
|
border-radius: 10px;
|
|
}
|
|
.source-tab {
|
|
flex: 1;
|
|
padding: 12px 16px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
border: 1px solid transparent;
|
|
font-size: 14px;
|
|
}
|
|
.source-tab:hover {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
}
|
|
.source-tab.active {
|
|
background: rgba(126, 200, 255, 0.15);
|
|
border-color: rgba(126, 200, 255, 0.4);
|
|
color: #7ec8ff;
|
|
}
|
|
.source-content {
|
|
display: none;
|
|
}
|
|
.source-content.active {
|
|
display: block;
|
|
}
|
|
|
|
/* Preset Themes Scrollable */
|
|
.preset-container {
|
|
max-height: 340px;
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
background: rgba(0, 0, 0, 0.15);
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
.preset-container::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
.preset-container::-webkit-scrollbar-track {
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 4px;
|
|
}
|
|
.preset-container::-webkit-scrollbar-thumb {
|
|
background: rgba(126, 200, 255, 0.4);
|
|
border-radius: 4px;
|
|
}
|
|
.preset-container::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(126, 200, 255, 0.6);
|
|
}
|
|
.preset-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
gap: 12px;
|
|
padding: 4px;
|
|
}
|
|
.theme-card {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-radius: 10px;
|
|
padding: 12px;
|
|
border: 2px solid transparent;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
.theme-card:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
|
border-color: rgba(126, 200, 255, 0.3);
|
|
}
|
|
.theme-card.selected {
|
|
border-color: #7ec8ff;
|
|
box-shadow: 0 0 0 2px rgba(126, 200, 255, 0.3);
|
|
}
|
|
.theme-card h3 {
|
|
margin-bottom: 8px;
|
|
font-size: 0.95em;
|
|
color: #cde4ff;
|
|
}
|
|
.color-row {
|
|
display: flex;
|
|
gap: 4px;
|
|
margin-bottom: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.color-swatch {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 4px;
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
/* Upload */
|
|
.upload-section {
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border: 2px dashed rgba(126, 200, 255, 0.3);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
text-align: center;
|
|
transition: all 0.3s;
|
|
}
|
|
.upload-section:hover {
|
|
border-color: rgba(126, 200, 255, 0.6);
|
|
background: rgba(0, 0, 0, 0.3);
|
|
}
|
|
.upload-btn {
|
|
display: inline-block;
|
|
padding: 10px 24px;
|
|
background: #7ec8ff;
|
|
color: #1e1e2f;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
}
|
|
.upload-btn:hover {
|
|
background: #5ab0f0;
|
|
transform: translateY(-2px);
|
|
}
|
|
.upload-status {
|
|
margin-top: 12px;
|
|
font-size: 13px;
|
|
min-height: 18px;
|
|
}
|
|
.upload-status.success {
|
|
color: #4caf50;
|
|
}
|
|
.upload-status.error {
|
|
color: #f44336;
|
|
}
|
|
|
|
/* Edit Layout */
|
|
.edit-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
}
|
|
@media (max-width: 1100px) {
|
|
.edit-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* Preview Mode Toggle */
|
|
.preview-mode-toggle {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-bottom: 12px;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
padding: 4px;
|
|
border-radius: 8px;
|
|
}
|
|
.mode-btn {
|
|
flex: 1;
|
|
padding: 8px 12px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
font-size: 13px;
|
|
border: 1px solid transparent;
|
|
}
|
|
.mode-btn:hover {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
}
|
|
.mode-btn.active {
|
|
background: rgba(126, 200, 255, 0.2);
|
|
border-color: rgba(126, 200, 255, 0.4);
|
|
color: #7ec8ff;
|
|
}
|
|
|
|
/* Color Inputs */
|
|
.color-group {
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 10px;
|
|
padding: 14px;
|
|
margin-bottom: 12px;
|
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
.color-group-title {
|
|
color: #7ec8ff;
|
|
margin-bottom: 10px;
|
|
font-size: 14px;
|
|
padding-bottom: 6px;
|
|
border-bottom: 1px solid rgba(126, 200, 255, 0.2);
|
|
font-weight: 600;
|
|
}
|
|
.color-input-row {
|
|
display: grid;
|
|
grid-template-columns: 110px 40px 1fr;
|
|
gap: 8px;
|
|
align-items: center;
|
|
margin-bottom: 6px;
|
|
padding: 4px 6px;
|
|
border-radius: 4px;
|
|
transition: background 0.2s;
|
|
}
|
|
.color-input-row:hover {
|
|
background: rgba(255, 255, 255, 0.03);
|
|
}
|
|
.color-input-row label {
|
|
font-size: 12px;
|
|
color: #aaa;
|
|
font-weight: 500;
|
|
}
|
|
.color-input-row input[type="color"] {
|
|
width: 40px;
|
|
height: 30px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
padding: 0;
|
|
}
|
|
.color-input-row input[type="text"] {
|
|
width: 100%;
|
|
padding: 5px 8px;
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
border-radius: 4px;
|
|
color: #fff;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Preview Box */
|
|
.preview-box {
|
|
background: rgba(0, 0, 0, 0.4);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
min-height: 280px;
|
|
position: sticky;
|
|
top: 20px;
|
|
}
|
|
.preview-header {
|
|
padding: 14px;
|
|
border-radius: 8px;
|
|
margin-bottom: 12px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
transition: all 0.3s;
|
|
}
|
|
.preview-buttons {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 12px;
|
|
}
|
|
.preview-btn {
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
border: none;
|
|
color: #fff;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
transition: all 0.3s;
|
|
}
|
|
.preview-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 10px;
|
|
}
|
|
.preview-card {
|
|
padding: 14px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
transition: all 0.3s;
|
|
}
|
|
.preview-info {
|
|
margin-top: 12px;
|
|
padding: 10px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
}
|
|
.preview-info strong {
|
|
color: #7ec8ff;
|
|
}
|
|
|
|
/* JSON Output */
|
|
.json-output {
|
|
background: #0d1117;
|
|
border-radius: 8px;
|
|
padding: 14px;
|
|
max-height: 380px;
|
|
overflow: auto;
|
|
font-family: "Consolas", monospace;
|
|
font-size: 11px;
|
|
color: #c9d1d9;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
direction: ltr;
|
|
text-align: left;
|
|
white-space: pre;
|
|
}
|
|
.json-output::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
.json-output::-webkit-scrollbar-track {
|
|
background: rgba(0, 0, 0, 0.2);
|
|
}
|
|
.json-output::-webkit-scrollbar-thumb {
|
|
background: rgba(126, 200, 255, 0.4);
|
|
border-radius: 4px;
|
|
}
|
|
.tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.tab {
|
|
padding: 8px 16px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 6px 6px 0 0;
|
|
cursor: pointer;
|
|
border: 1px solid transparent;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
.tab.active {
|
|
background: rgba(126, 200, 255, 0.15);
|
|
border-color: rgba(126, 200, 255, 0.3);
|
|
color: #7ec8ff;
|
|
}
|
|
|
|
/* Action Buttons */
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
margin-top: 16px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
.btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-primary {
|
|
background: #7ec8ff;
|
|
color: #1e1e2f;
|
|
}
|
|
.btn-primary:hover {
|
|
background: #5ab0f0;
|
|
}
|
|
.btn-success {
|
|
background: #4caf50;
|
|
color: #fff;
|
|
}
|
|
.btn-success:hover {
|
|
background: #3d9140;
|
|
}
|
|
.btn-warning {
|
|
background: #ff9800;
|
|
color: #fff;
|
|
}
|
|
.btn-warning:hover {
|
|
background: #e08600;
|
|
}
|
|
.btn-info {
|
|
background: #2196f3;
|
|
color: #fff;
|
|
}
|
|
.btn-info:hover {
|
|
background: #1976d2;
|
|
}
|
|
|
|
/* Theme Name Input */
|
|
.theme-name-input {
|
|
width: 100%;
|
|
padding: 10px 14px;
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
border-radius: 8px;
|
|
color: #fff;
|
|
font-size: 14px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* Normalize Info */
|
|
.normalize-info {
|
|
margin-top: 10px;
|
|
padding: 10px;
|
|
background: rgba(255, 152, 0, 0.1);
|
|
border: 1px solid rgba(255, 152, 0, 0.3);
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
color: #ffb74d;
|
|
}
|
|
.normalize-info ul {
|
|
margin: 6px 0 0 0;
|
|
padding-right: 18px;
|
|
}
|
|
.normalize-info li {
|
|
margin: 3px 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>🎨 تولیدکننده پوسته xFramework</h1>
|
|
<p>
|
|
مدیریت و ویرایش پوستههای رنگی برای برنامههای کاربردی مبتنی بر
|
|
xFramework
|
|
</p>
|
|
<div class="meta">
|
|
توسعهدهنده: هادی خزاعی اصل | شرکت: فن آوران ساحر علم | تاریخ: ۱ شهریور
|
|
۱۴۰۵
|
|
</div>
|
|
</header>
|
|
|
|
<div class="container">
|
|
<div class="section">
|
|
<h2>✏️ ویرایش پوسته</h2>
|
|
|
|
<div class="form-group" style="margin-bottom: 16px">
|
|
<label
|
|
style="
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
color: #a8d0ff;
|
|
font-weight: 600;
|
|
"
|
|
>نام پوسته</label
|
|
>
|
|
<input
|
|
type="text"
|
|
id="themeName"
|
|
class="theme-name-input"
|
|
value="MyCustomTheme"
|
|
/>
|
|
</div>
|
|
|
|
<div class="source-tabs">
|
|
<div class="source-tab active" onclick="switchSource('preset')">
|
|
📚 پوستههای از پیش تعریفشده (۵۰ مورد)
|
|
</div>
|
|
<div class="source-tab" onclick="switchSource('upload')">
|
|
📁 بارگذاری فایل JSON
|
|
</div>
|
|
</div>
|
|
|
|
<div id="source-preset" class="source-content active">
|
|
<div class="preset-container">
|
|
<div class="preset-grid" id="presetGrid"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="source-upload" class="source-content">
|
|
<div class="upload-section">
|
|
<label for="themeFileInput" class="upload-btn"
|
|
>📁 انتخاب فایل JSON پوسته</label
|
|
>
|
|
<input
|
|
type="file"
|
|
id="themeFileInput"
|
|
accept=".json"
|
|
style="display: none"
|
|
onchange="handleThemeFileUpload(event)"
|
|
/>
|
|
<div id="uploadStatus" class="upload-status"></div>
|
|
<div
|
|
id="normalizeInfo"
|
|
class="normalize-info"
|
|
style="display: none"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 20px">
|
|
<div class="edit-layout">
|
|
<!-- Left: Color Inputs -->
|
|
<div>
|
|
<div class="preview-mode-toggle">
|
|
<div
|
|
class="mode-btn active"
|
|
onclick="switchPreviewMode('light')"
|
|
>
|
|
☀️ Light
|
|
</div>
|
|
<div class="mode-btn" onclick="switchPreviewMode('dark')">
|
|
🌙 Dark
|
|
</div>
|
|
</div>
|
|
<div id="colorInputs"></div>
|
|
</div>
|
|
|
|
<!-- Right: Preview -->
|
|
<div>
|
|
<div class="preview-box" id="previewBox">
|
|
<div class="preview-header" id="previewHeader">
|
|
پیشنمایش زنده
|
|
</div>
|
|
<div class="preview-buttons" id="previewButtons"></div>
|
|
<div class="preview-cards" id="previewCards"></div>
|
|
<div class="preview-info" id="previewInfo"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 20px">
|
|
<div class="tabs">
|
|
<div class="tab active" onclick="switchJsonTab('light')">
|
|
📄 JSON (Light)
|
|
</div>
|
|
<div class="tab" onclick="switchJsonTab('dark')">
|
|
📄 JSON (Dark)
|
|
</div>
|
|
<div class="tab" onclick="switchJsonTab('full')">
|
|
📄 Full JSON
|
|
</div>
|
|
</div>
|
|
<pre class="json-output" id="jsonOutput"></pre>
|
|
</div>
|
|
|
|
<div class="action-buttons">
|
|
<button class="btn btn-success" onclick="downloadTheme()">
|
|
⬇️ دانلود JSON
|
|
</button>
|
|
<button class="btn btn-warning" onclick="copyJson()">
|
|
📋 کپی JSON
|
|
</button>
|
|
<button class="btn btn-info" onclick="resetToEmpty()">
|
|
🔄 بازنشانی
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ============ 50 Preset Themes ============
|
|
const PRESET_THEMES = [
|
|
{ name: "default", p: "#3880ff", s: "#0cd1e8", t: "#7044ff" },
|
|
{ name: "blue", p: "#3880ff", s: "#0cd1e8", t: "#7044ff" },
|
|
{ name: "green", p: "#0f9535", s: "#6d950f", t: "#086fa7" },
|
|
{ name: "brown", p: "#5d4037", s: "#512da8", t: "#a1887f" },
|
|
{ name: "orange", p: "#d88e30", s: "#c83adf", t: "#d3dd33" },
|
|
{ name: "royal-purple", p: "#6a0dad", s: "#9b59b6", t: "#8e44ad" },
|
|
{ name: "teal-ocean", p: "#008080", s: "#20b2aa", t: "#48d1cc" },
|
|
{ name: "rose-garden", p: "#e91e63", s: "#f06292", t: "#c2185b" },
|
|
{ name: "midnight-blue", p: "#191970", s: "#4169e1", t: "#6495ed" },
|
|
{ name: "coral-sunset", p: "#ff7f50", s: "#ffa07a", t: "#ff6347" },
|
|
{ name: "forest-green", p: "#228b22", s: "#32cd32", t: "#006400" },
|
|
{ name: "sunset-orange", p: "#ff4500", s: "#ffa500", t: "#ffd700" },
|
|
{ name: "ocean-blue", p: "#006994", s: "#4682b4", t: "#5f9ea0" },
|
|
{ name: "cherry-blossom", p: "#ff69b4", s: "#ffb6c1", t: "#ff1493" },
|
|
{ name: "emerald", p: "#50c878", s: "#2ecc71", t: "#27ae60" },
|
|
{ name: "lavender", p: "#e6e6fa", s: "#b19cd9", t: "#9370db" },
|
|
{ name: "mint-green", p: "#98ff98", s: "#3eb489", t: "#00a550" },
|
|
{ name: "peach", p: "#ffdab9", s: "#ffcc99", t: "#ff9966" },
|
|
{ name: "sky-blue", p: "#87ceeb", s: "#87cefa", t: "#00bfff" },
|
|
{ name: "olive", p: "#808000", s: "#6b8e23", t: "#556b2f" },
|
|
{ name: "burgundy", p: "#800020", s: "#722f37", t: "#8b0000" },
|
|
{ name: "navy", p: "#000080", s: "#00008b", t: "#0000cd" },
|
|
{ name: "mustard", p: "#ffdb58", s: "#ffdb58", t: "#e1ad01" },
|
|
{ name: "sage", p: "#bcb88a", s: "#9dc183", t: "#6b8e23" },
|
|
{ name: "plum", p: "#8e4585", s: "#673147", t: "#4b0082" },
|
|
{ name: "turquoise", p: "#40e0d0", s: "#00ced1", t: "#00ced1" },
|
|
{ name: "maroon", p: "#800000", s: "#8b0000", t: "#a52a2a" },
|
|
{ name: "slate", p: "#708090", s: "#778899", t: "#2f4f4f" },
|
|
{ name: "gold", p: "#ffd700", s: "#daa520", t: "#b8860b" },
|
|
{ name: "silver", p: "#c0c0c0", s: "#a9a9a9", t: "#808080" },
|
|
{ name: "rust", p: "#b7410e", s: "#a52a2a", t: "#8b4513" },
|
|
{ name: "indigo", p: "#4b0082", s: "#483d8b", t: "#191970" },
|
|
{ name: "periwinkle", p: "#ccccff", s: "#ccccff", t: "#9999ff" },
|
|
{ name: "salmon", p: "#fa8072", s: "#ffa07a", t: "#e9967a" },
|
|
{ name: "aquamarine", p: "#7fffd4", s: "#7fffd4", t: "#00ffff" },
|
|
{ name: "crimson", p: "#dc143c", s: "#dc143c", t: "#b22222" },
|
|
{ name: "cobalt", p: "#0047ab", s: "#0047ab", t: "#00008b" },
|
|
{ name: "bronze", p: "#cd7f32", s: "#cd7f32", t: "#8b4513" },
|
|
{ name: "magenta", p: "#ff00ff", s: "#ff00ff", t: "#c71585" },
|
|
{ name: "cyan", p: "#00ffff", s: "#00ffff", t: "#00ced1" },
|
|
{ name: "amber", p: "#ffbf00", s: "#ffbf00", t: "#ff8c00" },
|
|
{ name: "ivory", p: "#fffff0", s: "#fffff0", t: "#fafad2" },
|
|
{ name: "charcoal", p: "#36454f", s: "#36454f", t: "#2f4f4f" },
|
|
{ name: "lime", p: "#00ff00", s: "#32cd32", t: "#00ff00" },
|
|
{ name: "fuchsia", p: "#ff00ff", s: "#ff00ff", t: "#c71585" },
|
|
{ name: "taupe", p: "#483c32", s: "#483c32", t: "#483c32" },
|
|
{ name: "mauve", p: "#e0b0ff", s: "#e0b0ff", t: "#dda0dd" },
|
|
{ name: "sienna", p: "#a0522d", s: "#a0522d", t: "#8b4513" },
|
|
{ name: "peridot", p: "#e6e200", s: "#e6e200", t: "#c8b800" },
|
|
{ name: "amethyst", p: "#9966cc", s: "#9966cc", t: "#8a2be2" },
|
|
];
|
|
|
|
// ============ Color Utilities ============
|
|
function hexToRgb(hex) {
|
|
hex = hex.trim().replace("#", "");
|
|
if (hex.length === 8) hex = hex.substring(0, 6);
|
|
if (hex.length === 3)
|
|
hex = hex
|
|
.split("")
|
|
.map((c) => c + c)
|
|
.join("");
|
|
const r = parseInt(hex.slice(0, 2), 16);
|
|
const g = parseInt(hex.slice(2, 4), 16);
|
|
const b = parseInt(hex.slice(4, 6), 16);
|
|
return { r, g, b };
|
|
}
|
|
function rgbToHex(r, g, b) {
|
|
return (
|
|
"#" +
|
|
[r, g, b]
|
|
.map((x) => {
|
|
const hex = Math.max(0, Math.min(255, Math.round(x))).toString(
|
|
16,
|
|
);
|
|
return hex.length === 1 ? "0" + hex : hex;
|
|
})
|
|
.join("")
|
|
);
|
|
}
|
|
function rgbToHsl(r, g, b) {
|
|
r /= 255;
|
|
g /= 255;
|
|
b /= 255;
|
|
const max = Math.max(r, g, b),
|
|
min = Math.min(r, g, b);
|
|
let h,
|
|
s,
|
|
l = (max + min) / 2;
|
|
if (max === min) {
|
|
h = s = 0;
|
|
} else {
|
|
const d = max - min;
|
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
switch (max) {
|
|
case r:
|
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
break;
|
|
case g:
|
|
h = (b - r) / d + 2;
|
|
break;
|
|
case b:
|
|
h = (r - g) / d + 4;
|
|
break;
|
|
}
|
|
h /= 6;
|
|
}
|
|
return { h: h * 360, s: s * 100, l: l * 100 };
|
|
}
|
|
function hslToRgb(h, s, l) {
|
|
h /= 360;
|
|
s /= 100;
|
|
l /= 100;
|
|
let r, g, b;
|
|
if (s === 0) {
|
|
r = g = b = l;
|
|
} else {
|
|
const hue2rgb = (p, q, t) => {
|
|
if (t < 0) t += 1;
|
|
if (t > 1) t -= 1;
|
|
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
if (t < 1 / 2) return q;
|
|
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
return p;
|
|
};
|
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
const p = 2 * l - q;
|
|
r = hue2rgb(p, q, h + 1 / 3);
|
|
g = hue2rgb(p, q, h);
|
|
b = hue2rgb(p, q, h - 1 / 3);
|
|
}
|
|
return {
|
|
r: Math.round(r * 255),
|
|
g: Math.round(g * 255),
|
|
b: Math.round(b * 255),
|
|
};
|
|
}
|
|
function generateShade(hex, percent = 18) {
|
|
const rgb = hexToRgb(hex);
|
|
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
hsl.l = Math.max(0, hsl.l - percent);
|
|
hsl.s = Math.min(100, hsl.s + 5);
|
|
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
|
}
|
|
function generateTint(hex, percent = 18) {
|
|
const rgb = hexToRgb(hex);
|
|
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
hsl.l = Math.min(100, hsl.l + percent);
|
|
hsl.s = Math.max(0, hsl.s - 5);
|
|
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
|
}
|
|
function generateContrast(hex) {
|
|
const rgb = hexToRgb(hex);
|
|
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
return luminance > 0.55 ? "#000000" : "#ffffff";
|
|
}
|
|
function normalizeColorValue(value) {
|
|
if (typeof value !== "string") return value;
|
|
let normalized = value.trim();
|
|
if (/^#[0-9A-Fa-f]{8}$/.test(normalized))
|
|
normalized = normalized.substring(0, 7);
|
|
if (normalized && !normalized.startsWith("#"))
|
|
normalized = "#" + normalized;
|
|
if (!/^#[0-9A-Fa-f]{6}$/.test(normalized)) return value;
|
|
return normalized;
|
|
}
|
|
|
|
// ============ Build Theme ============
|
|
function buildTheme(name, primary, secondary, tertiary) {
|
|
const success = "#10dc60",
|
|
warning = "#ffce00",
|
|
danger = "#f04141";
|
|
const dark = "#222428",
|
|
medium = "#989aa2",
|
|
light = "#f4f5f8";
|
|
return {
|
|
name,
|
|
light: {
|
|
type: "light",
|
|
colors: {
|
|
XColorPrimary: primary,
|
|
XColorPrimaryContrast: generateContrast(primary),
|
|
XColorPrimaryShade: generateShade(primary),
|
|
XColorPrimaryShadeContrast: generateContrast(
|
|
generateShade(primary),
|
|
),
|
|
XColorPrimaryTint: generateTint(primary),
|
|
XColorPrimaryTintContrast: generateContrast(
|
|
generateTint(primary),
|
|
),
|
|
XColorSecondary: secondary,
|
|
XColorSecondaryContrast: generateContrast(secondary),
|
|
XColorSecondaryShade: generateShade(secondary),
|
|
XColorSecondaryShadeContrast: generateContrast(
|
|
generateShade(secondary),
|
|
),
|
|
XColorSecondaryTint: generateTint(secondary),
|
|
XColorSecondaryTintContrast: generateContrast(
|
|
generateTint(secondary),
|
|
),
|
|
XColorTertiary: tertiary,
|
|
XColorTertiaryContrast: generateContrast(tertiary),
|
|
XColorTertiaryShade: generateShade(tertiary),
|
|
XColorTertiaryShadeContrast: generateContrast(
|
|
generateShade(tertiary),
|
|
),
|
|
XColorTertiaryTint: generateTint(tertiary),
|
|
XColorTertiaryTintContrast: generateContrast(
|
|
generateTint(tertiary),
|
|
),
|
|
XColorSuccess: success,
|
|
XColorSuccessContrast: "#ffffff",
|
|
XColorSuccessShade: generateShade(success),
|
|
XColorSuccessShadeContrast: "#ffffff",
|
|
XColorSuccessTint: generateTint(success),
|
|
XColorSuccessTintContrast: "#ffffff",
|
|
XColorWarning: warning,
|
|
XColorWarningContrast: "#ffffff",
|
|
XColorWarningShade: generateShade(warning),
|
|
XColorWarningShadeContrast: "#ffffff",
|
|
XColorWarningTint: generateTint(warning),
|
|
XColorWarningTintContrast: "#ffffff",
|
|
XColorDanger: danger,
|
|
XColorDangerContrast: "#ffffff",
|
|
XColorDangerShade: generateShade(danger),
|
|
XColorDangerShadeContrast: "#ffffff",
|
|
XColorDangerTint: generateTint(danger),
|
|
XColorDangerTintContrast: "#ffffff",
|
|
XColorDark: dark,
|
|
XColorDarkContrast: "#ffffff",
|
|
XColorDarkShade: generateShade(dark),
|
|
XColorDarkShadeContrast: "#ffffff",
|
|
XColorDarkTint: generateTint(dark),
|
|
XColorDarkTintContrast: "#ffffff",
|
|
XColorMedium: medium,
|
|
XColorMediumContrast: "#ffffff",
|
|
XColorMediumShade: generateShade(medium),
|
|
XColorMediumShadeContrast: "#ffffff",
|
|
XColorMediumTint: generateTint(medium),
|
|
XColorMediumTintContrast: "#ffffff",
|
|
XColorLight: light,
|
|
XColorLightContrast: "#000000",
|
|
XColorLightShade: generateShade(light),
|
|
XColorLightShadeContrast: "#000000",
|
|
XColorLightTint: generateTint(light),
|
|
XColorLightTintContrast: "#000000",
|
|
},
|
|
},
|
|
dark: {
|
|
type: "dark",
|
|
colors: {
|
|
XColorPrimary: dark,
|
|
XColorPrimaryContrast: "#ffffff",
|
|
XColorPrimaryShade: generateShade(dark),
|
|
XColorPrimaryShadeContrast: "#ffffff",
|
|
XColorPrimaryTint: generateTint(dark),
|
|
XColorPrimaryTintContrast: "#ffffff",
|
|
XColorSecondary: generateShade(dark, 5),
|
|
XColorSecondaryContrast: "#ffffff",
|
|
XColorSecondaryShade: generateShade(dark, 10),
|
|
XColorSecondaryShadeContrast: "#ffffff",
|
|
XColorSecondaryTint: generateTint(dark, 10),
|
|
XColorSecondaryTintContrast: "#ffffff",
|
|
XColorTertiary: tertiary,
|
|
XColorTertiaryContrast: generateContrast(tertiary),
|
|
XColorTertiaryShade: generateShade(tertiary),
|
|
XColorTertiaryShadeContrast: generateContrast(
|
|
generateShade(tertiary),
|
|
),
|
|
XColorTertiaryTint: generateTint(tertiary),
|
|
XColorTertiaryTintContrast: generateContrast(
|
|
generateTint(tertiary),
|
|
),
|
|
XColorSuccess: success,
|
|
XColorSuccessContrast: "#ffffff",
|
|
XColorSuccessShade: generateShade(success),
|
|
XColorSuccessShadeContrast: "#ffffff",
|
|
XColorSuccessTint: generateTint(success),
|
|
XColorSuccessTintContrast: "#ffffff",
|
|
XColorWarning: warning,
|
|
XColorWarningContrast: "#ffffff",
|
|
XColorWarningShade: generateShade(warning),
|
|
XColorWarningShadeContrast: "#ffffff",
|
|
XColorWarningTint: generateTint(warning),
|
|
XColorWarningTintContrast: "#ffffff",
|
|
XColorDanger: danger,
|
|
XColorDangerContrast: "#ffffff",
|
|
XColorDangerShade: generateShade(danger),
|
|
XColorDangerShadeContrast: "#ffffff",
|
|
XColorDangerTint: generateTint(danger),
|
|
XColorDangerTintContrast: "#ffffff",
|
|
XColorDark: dark,
|
|
XColorDarkContrast: "#ffffff",
|
|
XColorDarkShade: generateShade(dark),
|
|
XColorDarkShadeContrast: "#ffffff",
|
|
XColorDarkTint: generateTint(dark),
|
|
XColorDarkTintContrast: "#ffffff",
|
|
XColorMedium: medium,
|
|
XColorMediumContrast: "#ffffff",
|
|
XColorMediumShade: generateShade(medium),
|
|
XColorMediumShadeContrast: "#ffffff",
|
|
XColorMediumTint: generateTint(medium),
|
|
XColorMediumTintContrast: "#ffffff",
|
|
XColorLight: light,
|
|
XColorLightContrast: "#000000",
|
|
XColorLightShade: generateShade(light),
|
|
XColorLightShadeContrast: "#000000",
|
|
XColorLightTint: generateTint(light),
|
|
XColorLightTintContrast: "#000000",
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
// ============ State ============
|
|
let currentTheme = null;
|
|
let previewMode = "light"; // 'light' or 'dark'
|
|
let jsonTab = "light";
|
|
let selectedPresetIndex = -1;
|
|
|
|
// ============ Render Presets ============
|
|
function renderPresets() {
|
|
const grid = document.getElementById("presetGrid");
|
|
grid.innerHTML = "";
|
|
PRESET_THEMES.forEach((preset, idx) => {
|
|
const theme = buildTheme(preset.name, preset.p, preset.s, preset.t);
|
|
const card = document.createElement("div");
|
|
card.className = "theme-card";
|
|
card.dataset.index = idx;
|
|
card.innerHTML = `
|
|
<h3>${preset.name}</h3>
|
|
<div class="color-row">
|
|
<div class="color-swatch" style="background:${theme.light.colors.XColorPrimary}"></div>
|
|
<div class="color-swatch" style="background:${theme.light.colors.XColorSecondary}"></div>
|
|
<div class="color-swatch" style="background:${theme.light.colors.XColorTertiary}"></div>
|
|
</div>
|
|
<div class="color-row">
|
|
<div class="color-swatch" style="background:${theme.light.colors.XColorSuccess}"></div>
|
|
<div class="color-swatch" style="background:${theme.light.colors.XColorWarning}"></div>
|
|
<div class="color-swatch" style="background:${theme.light.colors.XColorDanger}"></div>
|
|
</div>
|
|
`;
|
|
card.onclick = () => selectPreset(idx);
|
|
grid.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function selectPreset(idx) {
|
|
selectedPresetIndex = idx;
|
|
document
|
|
.querySelectorAll(".theme-card")
|
|
.forEach((c) => c.classList.remove("selected"));
|
|
const card = document.querySelector(`.theme-card[data-index="${idx}"]`);
|
|
if (card) card.classList.add("selected");
|
|
|
|
const preset = PRESET_THEMES[idx];
|
|
const theme = buildTheme(preset.name, preset.p, preset.s, preset.t);
|
|
currentTheme = theme;
|
|
|
|
document.getElementById("themeName").value = preset.name;
|
|
renderColorInputs();
|
|
renderPreview();
|
|
renderJson();
|
|
}
|
|
|
|
// ============ Render Color Inputs ============
|
|
function renderColorInputs() {
|
|
const container = document.getElementById("colorInputs");
|
|
if (!currentTheme) {
|
|
container.innerHTML =
|
|
'<p style="opacity:0.5; text-align:center; padding:20px;">یک پوسته انتخاب کنید یا فایل JSON بارگذاری کنید</p>';
|
|
return;
|
|
}
|
|
|
|
const mode = previewMode;
|
|
const colors = currentTheme[mode].colors;
|
|
container.innerHTML = "";
|
|
|
|
const groups = [
|
|
"Primary",
|
|
"Secondary",
|
|
"Tertiary",
|
|
"Success",
|
|
"Warning",
|
|
"Danger",
|
|
"Dark",
|
|
"Medium",
|
|
"Light",
|
|
];
|
|
const groupLabels = {
|
|
Primary: "رنگ اصلی (Primary)",
|
|
Secondary: "رنگ دوم (Secondary)",
|
|
Tertiary: "رنگ سوم (Tertiary)",
|
|
Success: "موفقیت (Success)",
|
|
Warning: "هشدار (Warning)",
|
|
Danger: "خطر (Danger)",
|
|
Dark: "تیره (Dark)",
|
|
Medium: "میانه (Medium)",
|
|
Light: "روشن (Light)",
|
|
};
|
|
|
|
groups.forEach((group) => {
|
|
const groupDiv = document.createElement("div");
|
|
groupDiv.className = "color-group";
|
|
|
|
let html = `<div class="color-group-title">${groupLabels[group]}</div>`;
|
|
|
|
const fields = [
|
|
{ key: `XColor${group}`, label: "Base" },
|
|
{ key: `XColor${group}Shade`, label: "Shade" },
|
|
{ key: `XColor${group}Tint`, label: "Tint" },
|
|
{ key: `XColor${group}Contrast`, label: "Contrast" },
|
|
{ key: `XColor${group}ShadeContrast`, label: "Shade Contrast" },
|
|
{ key: `XColor${group}TintContrast`, label: "Tint Contrast" },
|
|
];
|
|
|
|
fields.forEach((field) => {
|
|
const value = colors[field.key] || "#000000";
|
|
html += `
|
|
<div class="color-input-row">
|
|
<label>${field.label}</label>
|
|
<input type="color" value="${value}" data-key="${field.key}" data-mode="${mode}" onchange="handleColorChange(this)">
|
|
<input type="text" value="${value}" data-key="${field.key}" data-mode="${mode}" onchange="handleColorChange(this)" oninput="handleColorInput(this)">
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
groupDiv.innerHTML = html;
|
|
container.appendChild(groupDiv);
|
|
});
|
|
}
|
|
|
|
function handleColorChange(input) {
|
|
const row = input.parentElement;
|
|
const colorInput = row.querySelector('input[type="color"]');
|
|
const textInput = row.querySelector('input[type="text"]');
|
|
|
|
if (input.type === "color") {
|
|
textInput.value = input.value;
|
|
} else {
|
|
if (/^#[0-9A-Fa-f]{6}$/.test(input.value)) {
|
|
colorInput.value = input.value;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const key = input.dataset.key;
|
|
const mode = input.dataset.mode;
|
|
if (currentTheme && currentTheme[mode]) {
|
|
currentTheme[mode].colors[key] =
|
|
input.type === "color" ? input.value : input.value;
|
|
}
|
|
|
|
renderPreview();
|
|
renderJson();
|
|
}
|
|
|
|
function handleColorInput(input) {
|
|
if (/^#[0-9A-Fa-f]{6}$/.test(input.value)) {
|
|
handleColorChange(input);
|
|
}
|
|
}
|
|
|
|
// ============ Preview Mode Toggle ============
|
|
function switchPreviewMode(mode) {
|
|
previewMode = mode;
|
|
document
|
|
.querySelectorAll(".mode-btn")
|
|
.forEach((b) => b.classList.remove("active"));
|
|
event.target.classList.add("active");
|
|
renderColorInputs();
|
|
renderPreview();
|
|
}
|
|
|
|
// ============ Render Preview ============
|
|
function renderPreview() {
|
|
if (!currentTheme) return;
|
|
|
|
const mode = previewMode;
|
|
const colors = currentTheme[mode].colors;
|
|
|
|
const header = document.getElementById("previewHeader");
|
|
header.style.background = colors.XColorPrimary;
|
|
header.style.color = colors.XColorPrimaryContrast;
|
|
header.textContent = `پیشنمایش پوسته: ${currentTheme.name} (${mode === "light" ? "روشن" : "تیره"})`;
|
|
|
|
const buttons = document.getElementById("previewButtons");
|
|
buttons.innerHTML = `
|
|
<button class="preview-btn" style="background:${colors.XColorPrimary};color:${colors.XColorPrimaryContrast}">Primary</button>
|
|
<button class="preview-btn" style="background:${colors.XColorSecondary};color:${colors.XColorSecondaryContrast}">Secondary</button>
|
|
<button class="preview-btn" style="background:${colors.XColorTertiary};color:${colors.XColorTertiaryContrast}">Tertiary</button>
|
|
<button class="preview-btn" style="background:${colors.XColorSuccess};color:${colors.XColorSuccessContrast}">Success</button>
|
|
<button class="preview-btn" style="background:${colors.XColorWarning};color:${colors.XColorWarningContrast}">Warning</button>
|
|
<button class="preview-btn" style="background:${colors.XColorDanger};color:${colors.XColorDangerContrast}">Danger</button>
|
|
`;
|
|
|
|
const cards = document.getElementById("previewCards");
|
|
cards.innerHTML = `
|
|
<div class="preview-card" style="background:${colors.XColorPrimaryTint};color:${colors.XColorPrimaryTintContrast}">Tint</div>
|
|
<div class="preview-card" style="background:${colors.XColorPrimary};color:${colors.XColorPrimaryContrast}">Base</div>
|
|
<div class="preview-card" style="background:${colors.XColorPrimaryShade};color:${colors.XColorPrimaryShadeContrast}">Shade</div>
|
|
`;
|
|
|
|
const info = document.getElementById("previewInfo");
|
|
info.innerHTML = `
|
|
<div><strong>نام پوسته:</strong> ${currentTheme.name}</div>
|
|
<div><strong>حالت نمایش:</strong> ${mode === "light" ? "روشن (Light)" : "تیره (Dark)"}</div>
|
|
<div><strong>Primary:</strong> <span style="display:inline-block;width:14px;height:14px;background:${colors.XColorPrimary};border-radius:3px;vertical-align:middle;border:1px solid rgba(255,255,255,0.3)"></span> ${colors.XColorPrimary}</div>
|
|
<div><strong>Secondary:</strong> <span style="display:inline-block;width:14px;height:14px;background:${colors.XColorSecondary};border-radius:3px;vertical-align:middle;border:1px solid rgba(255,255,255,0.3)"></span> ${colors.XColorSecondary}</div>
|
|
`;
|
|
}
|
|
|
|
// ============ JSON Tab ============
|
|
function switchJsonTab(tab) {
|
|
jsonTab = tab;
|
|
document
|
|
.querySelectorAll(".tab")
|
|
.forEach((t) => t.classList.remove("active"));
|
|
event.target.classList.add("active");
|
|
renderJson();
|
|
}
|
|
|
|
function renderJson() {
|
|
if (!currentTheme) {
|
|
document.getElementById("jsonOutput").textContent = "";
|
|
return;
|
|
}
|
|
let data;
|
|
if (jsonTab === "light") data = currentTheme.light;
|
|
else if (jsonTab === "dark") data = currentTheme.dark;
|
|
else data = currentTheme;
|
|
document.getElementById("jsonOutput").textContent = JSON.stringify(
|
|
data,
|
|
null,
|
|
1,
|
|
);
|
|
}
|
|
|
|
// ============ Source Tabs ============
|
|
function switchSource(source) {
|
|
document
|
|
.querySelectorAll(".source-tab")
|
|
.forEach((t) => t.classList.remove("active"));
|
|
event.target.classList.add("active");
|
|
document
|
|
.querySelectorAll(".source-content")
|
|
.forEach((c) => c.classList.remove("active"));
|
|
document.getElementById(`source-${source}`).classList.add("active");
|
|
}
|
|
|
|
// ============ File Upload ============
|
|
function handleThemeFileUpload(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function (e) {
|
|
try {
|
|
let raw = JSON.parse(e.target.result);
|
|
|
|
// Normalize
|
|
const { data: normalized, changes } = normalizeThemeJson(raw);
|
|
|
|
const normalizeInfo = document.getElementById("normalizeInfo");
|
|
if (
|
|
changes.keysTrimmed.length > 0 ||
|
|
changes.alphaRemoved.length > 0
|
|
) {
|
|
let info = "<strong>⚠️ فایل قدیمی شناسایی و اصلاح شد:</strong>";
|
|
if (changes.keysTrimmed.length > 0) {
|
|
info += "<br><strong>کلیدهای اصلاحشده:</strong><ul>";
|
|
changes.keysTrimmed
|
|
.slice(0, 5)
|
|
.forEach((c) => (info += `<li>${c}</li>`));
|
|
if (changes.keysTrimmed.length > 5)
|
|
info += `<li>... و ${changes.keysTrimmed.length - 5} مورد دیگر</li>`;
|
|
info += "</ul>";
|
|
}
|
|
if (changes.alphaRemoved.length > 0) {
|
|
info +=
|
|
"<br><strong>رنگهای اصلاحشده (حذف Alpha):</strong><ul>";
|
|
changes.alphaRemoved
|
|
.slice(0, 5)
|
|
.forEach((c) => (info += `<li>${c}</li>`));
|
|
if (changes.alphaRemoved.length > 5)
|
|
info += `<li>... و ${changes.alphaRemoved.length - 5} مورد دیگر</li>`;
|
|
info += "</ul>";
|
|
}
|
|
normalizeInfo.innerHTML = info;
|
|
normalizeInfo.style.display = "block";
|
|
} else {
|
|
normalizeInfo.style.display = "none";
|
|
}
|
|
|
|
if (
|
|
!normalized.name ||
|
|
!normalized.light ||
|
|
!normalized.dark ||
|
|
!normalized.light.colors ||
|
|
!normalized.dark.colors
|
|
) {
|
|
throw new Error("ساختار فایل JSON نامعتبر است");
|
|
}
|
|
|
|
currentTheme = normalized;
|
|
selectedPresetIndex = -1;
|
|
document
|
|
.querySelectorAll(".theme-card")
|
|
.forEach((c) => c.classList.remove("selected"));
|
|
|
|
document.getElementById("themeName").value = normalized.name;
|
|
document.getElementById("uploadStatus").className =
|
|
"upload-status success";
|
|
document.getElementById("uploadStatus").textContent =
|
|
`✅ فایل با موفقیت بارگذاری شد: ${normalized.name}`;
|
|
|
|
renderColorInputs();
|
|
renderPreview();
|
|
renderJson();
|
|
} catch (err) {
|
|
document.getElementById("uploadStatus").className =
|
|
"upload-status error";
|
|
document.getElementById("uploadStatus").textContent =
|
|
`❌ خطا: ${err.message}`;
|
|
document.getElementById("normalizeInfo").style.display = "none";
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
function normalizeThemeJson(themeData) {
|
|
const changes = { keysTrimmed: [], alphaRemoved: [] };
|
|
if (!themeData || typeof themeData !== "object")
|
|
return { data: themeData, changes };
|
|
|
|
const result = {};
|
|
for (const key of Object.keys(themeData)) {
|
|
const nk = key.trim();
|
|
if (nk !== key) changes.keysTrimmed.push(`"${key}" → "${nk}"`);
|
|
|
|
const value = themeData[key];
|
|
if (nk === "colors" && typeof value === "object" && value !== null) {
|
|
const nc = {};
|
|
for (const ck of Object.keys(value)) {
|
|
const nck = ck.trim();
|
|
if (nck !== ck) changes.keysTrimmed.push(`"${ck}" → "${nck}"`);
|
|
const cv = value[ck];
|
|
if (typeof cv === "string") {
|
|
const orig = cv;
|
|
const ncv = normalizeColorValue(cv);
|
|
if (ncv !== orig) {
|
|
changes.alphaRemoved.push(`${nck}: "${orig}" → "${ncv}"`);
|
|
}
|
|
nc[nck] = ncv;
|
|
} else {
|
|
nc[nck] = cv;
|
|
}
|
|
}
|
|
result[nk] = nc;
|
|
} else if (typeof value === "object" && value !== null) {
|
|
const sub = normalizeThemeJson(value);
|
|
result[nk] = sub.data;
|
|
changes.keysTrimmed.push(...sub.changes.keysTrimmed);
|
|
changes.alphaRemoved.push(...sub.changes.alphaRemoved);
|
|
} else {
|
|
result[nk] = value;
|
|
}
|
|
}
|
|
return { data: result, changes };
|
|
}
|
|
|
|
// ============ Actions ============
|
|
function downloadTheme() {
|
|
if (!currentTheme) {
|
|
alert("ابتدا یک پوسته انتخاب کنید");
|
|
return;
|
|
}
|
|
const blob = new Blob([JSON.stringify(currentTheme, null, 1)], {
|
|
type: "application/json",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `x-framework-${currentTheme.name}-theme.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function copyJson() {
|
|
if (!currentTheme) {
|
|
alert("ابتدا یک پوسته انتخاب کنید");
|
|
return;
|
|
}
|
|
let data;
|
|
if (jsonTab === "light") data = currentTheme.light;
|
|
else if (jsonTab === "dark") data = currentTheme.dark;
|
|
else data = currentTheme;
|
|
navigator.clipboard
|
|
.writeText(JSON.stringify(data, null, 1))
|
|
.then(() => alert("✅ JSON در کلیپبورد کپی شد"));
|
|
}
|
|
|
|
function resetToEmpty() {
|
|
currentTheme = null;
|
|
selectedPresetIndex = -1;
|
|
document
|
|
.querySelectorAll(".theme-card")
|
|
.forEach((c) => c.classList.remove("selected"));
|
|
document.getElementById("themeName").value = "MyCustomTheme";
|
|
document.getElementById("uploadStatus").textContent = "";
|
|
document.getElementById("normalizeInfo").style.display = "none";
|
|
document.getElementById("themeFileInput").value = "";
|
|
renderColorInputs();
|
|
renderPreview();
|
|
renderJson();
|
|
}
|
|
|
|
// ============ Init ============
|
|
renderPresets();
|
|
renderColorInputs();
|
|
renderPreview();
|
|
renderJson();
|
|
|
|
// Select first preset by default
|
|
selectPreset(0);
|
|
</script>
|
|
</body>
|
|
</html>
|