Files
xSaherelmWorkspace/Documents/Resources/Themes/Generator/xFrameworkThemeGenerator.html
T

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>