FlatlyPage
Version 1.0.0 • 54 files • 724.77 KB
Files
.htaccess
.last_check
admin/account.php
admin/dashboard.php
admin/easyedit.js
admin/extensions.php
admin/generate-hash.php
admin/index.php
admin/logout.php
admin/preview.php
admin/scripts.php
admin/theme-edit/builder.php
admin/theme-edit/generator.php
admin/theme-edit/index.php
admin/themes.php
assets/fonts/inter/inter.css
assets/fonts/space-grotesk/space-grotesk.css
config.php
contact-handler.php
contact.php
css/admin.css
css/contact.css
css/styles.css
css/theme.css
data/.htaccess
data/index.php
data/settings.php
data/sitemap-config.php
engine/index.php
engine/renderion.php
extensions-loader.php
extensions/privimetrics/main.php
extensions/privimetrics/manifest.xml
extensions/scroll_to_top/main.php
extensions/scroll_to_top/manifest.xml
extensions/seo_image_master/main.php
extensions/seo_image_master/manifest.xml
favicons.txt
index.php
newsletter/.htaccess
newsletter/confirm.php
newsletter/manager.php
newsletter/newsletter-form.js
newsletter/newsletter-styles.css
newsletter/newsletter-unavailable.php
newsletter/newsletter.sql
newsletter/settings.php
newsletter/subscribe.php
newsletter/unsubscribe.php
page.php
robots.txt.php
sitemap.php
updater/index.php
version.txt
admin/theme-edit/builder.php
<?php
require_once __DIR__ . '/../../config.php';
require_login();
session_start();
// Security headers
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
// CSRF Token
if (empty($_SESSION['theme_builder_token'])) {
$_SESSION['theme_builder_token'] = bin2hex(random_bytes(32));
}
// Escape function
if (!function_exists('e')) {
function e($str) {
return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8');
}
}
// ============================================================================
// THEME PROCESSING
// ============================================================================
$generated_css = '';
$error_message = '';
$success_message = '';
$current_colors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
// CSRF validation
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['theme_builder_token'], $_POST['csrf_token'])) {
$error_message = 'Security token validation failed. Please refresh the page.';
} else {
try {
// Sanitize and validate inputs
$theme_name = trim(filter_input(INPUT_POST, 'theme_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? '');
$author = trim(filter_input(INPUT_POST, 'author', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? '');
if (empty($theme_name) || empty($author)) {
throw new Exception('Theme name and author are required fields.');
}
if (strlen($theme_name) > 100 || strlen($author) > 100) {
throw new Exception('Theme name and author must be less than 100 characters.');
}
// All theme properties with dark and light modes
$theme_properties = [
// Dark mode colors
'dark_background', 'dark_foreground', 'dark_card', 'dark_card_hover',
'dark_muted', 'dark_border', 'dark_success',
'dark_avatar_from', 'dark_avatar_to',
// Light mode colors
'light_background', 'light_foreground', 'light_card', 'light_card_hover',
'light_muted', 'light_border', 'light_success',
'light_avatar_from', 'light_avatar_to',
];
$colors = [];
foreach ($theme_properties as $field) {
$value = filter_input(INPUT_POST, $field, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
// Skip if empty
if ($value === null || $value === '') {
continue;
}
// Color fields validation
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $value)) {
throw new Exception("Invalid color format for {$field}");
}
$colors[$field] = strtolower($value);
}
$current_colors = $colors;
// Generate CSS
$dark_bg = $colors['dark_background'] ?? '#111111';
$dark_fg = $colors['dark_foreground'] ?? '#fafafa';
$dark_card = $colors['dark_card'] ?? '#1a1a1a';
$dark_card_hover = $colors['dark_card_hover'] ?? '#222222';
$dark_muted = $colors['dark_muted'] ?? '#888888';
$dark_border = $colors['dark_border'] ?? '#2a2a2a';
$dark_success = $colors['dark_success'] ?? '#22c55e';
$dark_avatar_from = $colors['dark_avatar_from'] ?? '#333333';
$dark_avatar_to = $colors['dark_avatar_to'] ?? '#555555';
$light_bg = $colors['light_background'] ?? '#ffffff';
$light_fg = $colors['light_foreground'] ?? '#111111';
$light_card = $colors['light_card'] ?? '#f5f5f5';
$light_card_hover = $colors['light_card_hover'] ?? '#efefef';
$light_muted = $colors['light_muted'] ?? '#666666';
$light_border = $colors['light_border'] ?? '#e0e0e0';
$light_success = $colors['light_success'] ?? '#16a34a';
$light_avatar_from = $colors['light_avatar_from'] ?? '#dddddd';
$light_avatar_to = $colors['light_avatar_to'] ?? '#bbbbbb';
$color_string = "Dark[Bg:{$dark_bg}, Fg:{$dark_fg}, Card:{$dark_card}] | Light[Bg:{$light_bg}, Fg:{$light_fg}, Card:{$light_card}]";
// Create CSS
$header = "/* ============================================================================\n";
$header .= " Theme: " . e($theme_name) . "\n";
$header .= " Author: " . e($author) . "\n";
$header .= " Colors: " . e($color_string) . "\n";
$header .= " ============================================================================ */\n\n";
$header .= " Generator: FlatlyPage Visual Theme Builder\n";
$css_content = ":root {\n";
$css_content .= " color-scheme: dark;\n";
$css_content .= " --background: {$dark_bg};\n";
$css_content .= " --foreground: {$dark_fg};\n";
$css_content .= " --card: {$dark_card};\n";
$css_content .= " --card-hover: {$dark_card_hover};\n";
$css_content .= " --muted: {$dark_muted};\n";
$css_content .= " --border: {$dark_border};\n";
$css_content .= " --success: {$dark_success};\n";
$css_content .= " --avatar-from: {$dark_avatar_from};\n";
$css_content .= " --avatar-to: {$dark_avatar_to};\n";
$css_content .= "}\n\n";
$css_content .= ":root[data-theme=\"light\"] {\n";
$css_content .= " color-scheme: light;\n";
$css_content .= " --background: {$light_bg};\n";
$css_content .= " --foreground: {$light_fg};\n";
$css_content .= " --card: {$light_card};\n";
$css_content .= " --card-hover: {$light_card_hover};\n";
$css_content .= " --muted: {$light_muted};\n";
$css_content .= " --border: {$light_border};\n";
$css_content .= " --success: {$light_success};\n";
$css_content .= " --avatar-from: {$light_avatar_from};\n";
$css_content .= " --avatar-to: {$light_avatar_to};\n";
$css_content .= "}\n\n";
$css_content .= "body {\n";
$css_content .= " background-color: var(--background);\n";
$css_content .= " color: var(--foreground);\n";
$css_content .= "}\n";
$generated_css = $header . $css_content;
$success_message = 'Theme generated successfully!';
} catch (Exception $e) {
$error_message = $e->getMessage();
}
}
}
// Default values for DARK mode
$dark_defaults = [
'dark_background' => '#111111',
'dark_foreground' => '#fafafa',
'dark_card' => '#1a1a1a',
'dark_card_hover' => '#222222',
'dark_muted' => '#888888',
'dark_border' => '#2a2a2a',
'dark_success' => '#22c55e',
'dark_avatar_from' => '#333333',
'dark_avatar_to' => '#555555',
];
// Default values for LIGHT mode
$light_defaults = [
'light_background' => '#ffffff',
'light_foreground' => '#111111',
'light_card' => '#f5f5f5',
'light_card_hover' => '#efefef',
'light_muted' => '#666666',
'light_border' => '#e0e0e0',
'light_success' => '#16a34a',
'light_avatar_from' => '#dddddd',
'light_avatar_to' => '#bbbbbb',
];
// Merge all defaults
$all_defaults = array_merge($dark_defaults, $light_defaults);
$active_colors = !empty($current_colors) ? array_merge($all_defaults, $current_colors) : $all_defaults;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Visual Theme Builder - FlatlyPage CMS</title>
<link rel="preload" href="/assets/fonts/inter/inter.ttf" as="font" type="font/ttf" crossorigin>
<link rel="stylesheet" href="/assets/fonts/inter/inter.css">
<style>
/* Theme Builder Pro Styles */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--brand-primary: #6366f1;
--brand-primary-hover: #4f46e5;
--gray-950: #0a0a0a;
--gray-900: #171717;
--gray-800: #262626;
--gray-700: #404040;
--gray-600: #525252;
--gray-500: #737373;
--gray-400: #a3a3a3;
--gray-200: #e5e5e5;
--gray-100: #f5f5f5;
--gray-50: #fafafa;
--success: #22c55e;
--error: #ef4444;
--bg-primary: var(--gray-950);
--bg-secondary: #0d0d0d;
--bg-tertiary: var(--gray-900);
--bg-elevated: #1f1f1f;
--bg-hover: #2a2a2a;
--border-primary: var(--gray-800);
--border-secondary: var(--gray-700);
--text-primary: var(--gray-50);
--text-secondary: var(--gray-400);
--text-tertiary: var(--gray-500);
--sidebar-width: 460px;
--header-height: 64px;
--radius-md: 10px;
--radius-lg: 14px;
--transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
html { font-size: 16px; -webkit-font-smoothing: antialiased; }
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
.app-wrapper { display: flex; min-height: 100vh; }
.sidebar {
width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border-primary);
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 100;
overflow: hidden;
}
.sidebar-header {
padding: 24px;
border-bottom: 1px solid var(--border-primary);
background: var(--bg-tertiary);
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.sidebar-footer {
padding: 20px 24px;
border-top: 1px solid var(--border-primary);
background: var(--bg-tertiary);
}
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
display: flex;
flex-direction: column;
}
.content-header {
height: var(--header-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32px;
position: sticky;
top: 0;
z-index: 50;
}
.content-body { flex: 1; overflow-y: auto; background: var(--bg-primary); }
/* Brand */
.brand { display: flex; align-items: center; gap: 12px; }
.brand-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--brand-primary), var(--brand-primary-hover));
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
}
.brand-icon img { width: 20px; height: 20px; fill: white; }
.brand-text h1 {
font-size: 1.125rem;
font-weight: 700;
color: var(--text-primary);
}
.brand-text p {
font-size: 0.75rem;
color: var(--text-tertiary);
font-weight: 500;
}
/* Alerts */
.alert {
padding: 12px 16px;
border-radius: var(--radius-md);
font-size: 0.875rem;
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 20px;
}
.alert-success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: var(--success);
}
.alert-error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: var(--error);
}
.alert-icon { width: 20px; height: 20px; flex-shrink: 0; }
/* Forms */
.form-section { margin-bottom: 32px; }
.section-title {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-tertiary);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title::before {
content: '';
width: 3px;
height: 12px;
background: var(--brand-primary);
border-radius: 2px;
}
.form-group { margin-bottom: 20px; }
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.form-label.required::after { content: " *"; color: var(--error); }
.form-help {
font-size: 0.8125rem;
color: var(--text-tertiary);
margin-top: 6px;
}
.form-input {
width: 100%;
padding: 11px 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 0.9375rem;
font-family: inherit;
transition: all var(--transition-base);
}
.form-input:focus {
border-color: var(--brand-primary);
outline: none;
background: var(--bg-elevated);
}
/* Color Picker */
.color-input-group {
display: flex;
align-items: center;
gap: 12px;
}
.color-swatch {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
border: 2px solid var(--border-primary);
cursor: pointer;
transition: all var(--transition-base);
position: relative;
overflow: hidden;
flex-shrink: 0;
}
.color-swatch:hover {
border-color: var(--brand-primary);
transform: scale(1.05);
}
.color-swatch input[type="color"] {
position: absolute;
inset: -2px;
width: calc(100% + 4px);
height: calc(100% + 4px);
border: none;
cursor: pointer;
opacity: 0;
}
.color-swatch-preview {
width: 100%;
height: 100%;
border-radius: calc(var(--radius-md) - 2px);
}
.color-text-input {
flex: 1;
font-family: monospace;
text-transform: uppercase;
font-weight: 600;
}
.color-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
/* Mode Tabs */
.mode-tabs {
display: flex;
background: var(--bg-tertiary);
padding: 4px;
border-radius: var(--radius-md);
gap: 4px;
margin-bottom: 24px;
}
.mode-tab {
flex: 1;
padding: 10px 16px;
background: transparent;
border: none;
border-radius: calc(var(--radius-md) - 2px);
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-base);
}
.mode-tab:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
.mode-tab.active {
background: var(--bg-elevated);
color: var(--text-primary);
}
.mode-content { display: none; }
.mode-content.active { display: block; }
/* Preset Buttons */
.preset-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.preset-button {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-md);
padding: 14px 12px;
cursor: pointer;
transition: all var(--transition-base);
text-align: left;
display: flex;
flex-direction: column;
gap: 10px;
}
.preset-button:hover {
background: var(--bg-elevated);
border-color: var(--brand-primary);
transform: translateY(-2px);
}
.preset-name {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-primary);
}
.preset-colors {
display: flex;
gap: 6px;
}
.preset-color-dot {
width: 24px;
height: 24px;
border-radius: 6px;
border: 1px solid var(--border-primary);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 11px 20px;
font-size: 0.875rem;
font-weight: 600;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-base);
border: none;
font-family: inherit;
}
.btn svg { width: 16px; height: 16px; }
.btn-primary {
background: var(--brand-primary);
color: white;
}
.btn-primary:hover {
background: var(--brand-primary-hover);
transform: translateY(-1px);
}
.btn-secondary {
background: var(--bg-elevated);
color: var(--text-primary);
border: 1px solid var(--border-primary);
}
.btn-secondary:hover {
background: var(--bg-hover);
}
.btn-block { width: 100%; }
.btn-group {
display: flex;
gap: 10px;
flex-direction: column;
}
/* Tabs */
.tabs {
display: flex;
background: var(--bg-tertiary);
padding: 4px;
border-radius: var(--radius-md);
gap: 4px;
}
.tab-button {
flex: 1;
padding: 8px 16px;
background: transparent;
border: none;
border-radius: calc(var(--radius-md) - 2px);
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-base);
}
.tab-button:hover {
color: var(--text-primary);
}
.tab-button.active {
background: var(--bg-elevated);
color: var(--text-primary);
}
.tab-content { display: none; padding-top: 20px; }
.tab-content.active { display: block; }
/* Preview */
.preview-container {
padding: 40px;
min-height: calc(100vh - var(--header-height));
}
.preview-frame {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
}
.preview-toolbar {
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-primary);
padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.preview-dots { display: flex; gap: 8px; }
.preview-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.preview-dot:nth-child(1) { background: #ff5f56; }
.preview-dot:nth-child(2) { background: #ffbd2e; }
.preview-dot:nth-child(3) { background: #27c93f; }
.preview-url {
font-size: 0.8125rem;
color: var(--text-tertiary);
font-family: monospace;
}
.preview-content {
overflow-y: auto;
max-height: calc(100vh - var(--header-height) - 140px);
}
/* Code Output */
.code-container {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
overflow: hidden;
margin: 20px;
}
.code-header {
background: var(--bg-elevated);
border-bottom: 1px solid var(--border-primary);
padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.code-title {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
}
.code-output {
padding: 24px;
font-family: monospace;
font-size: 0.8125rem;
line-height: 1.7;
color: var(--text-secondary);
overflow-x: auto;
white-space: pre-wrap;
max-height: 600px;
overflow-y: auto;
}
/* Scrollbar */
.sidebar-content::-webkit-scrollbar,
.code-output::-webkit-scrollbar,
.preview-content::-webkit-scrollbar {
width: 8px;
}
.sidebar-content::-webkit-scrollbar-thumb,
.code-output::-webkit-scrollbar-thumb,
.preview-content::-webkit-scrollbar-thumb {
background: var(--border-primary);
border-radius: 4px;
}
.badge {
display: inline-flex;
padding: 4px 10px;
background: rgba(99, 102, 241, 0.1);
color: var(--brand-primary);
border-radius: 100px;
font-size: 0.75rem;
font-weight: 600;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-tertiary);
}
.empty-state-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 8px;
}
/* Preview iframe wrapper */
.preview-iframe-wrapper {
width: 100%;
min-height: 800px;
padding: 60px 24px;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6;
}
.preview-iframe-wrapper[data-theme="dark"] {
background: <?= e($active_colors['dark_background']) ?>;
color: <?= e($active_colors['dark_foreground']) ?>;
}
.preview-iframe-wrapper[data-theme="light"] {
background: <?= e($active_colors['light_background']) ?>;
color: <?= e($active_colors['light_foreground']) ?>;
}
.preview-hero {
text-align: center;
max-width: 800px;
margin: 0 auto 80px;
}
.preview-badge {
display: inline-block;
padding: 6px 16px;
border-radius: 100px;
font-size: 0.875rem;
margin-bottom: 24px;
}
[data-theme="dark"] .preview-badge {
background: <?= e($active_colors['dark_card']) ?>;
border: 1px solid <?= e($active_colors['dark_border']) ?>;
color: <?= e($active_colors['dark_foreground']) ?>;
}
[data-theme="light"] .preview-badge {
background: <?= e($active_colors['light_card']) ?>;
border: 1px solid <?= e($active_colors['light_border']) ?>;
color: <?= e($active_colors['light_foreground']) ?>;
}
.preview-hero h1 {
font-size: 3.5rem;
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.1;
margin-bottom: 24px;
}
.preview-hero p {
font-size: 1.25rem;
margin-bottom: 32px;
}
[data-theme="dark"] .preview-hero p {
color: <?= e($active_colors['dark_muted']) ?>;
}
[data-theme="light"] .preview-hero p {
color: <?= e($active_colors['light_muted']) ?>;
}
.preview-buttons {
display: flex;
gap: 16px;
justify-content: center;
}
.preview-btn {
padding: 14px 28px;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border: none;
}
[data-theme="dark"] .preview-btn-primary {
background: <?= e($active_colors['dark_foreground']) ?>;
color: <?= e($active_colors['dark_background']) ?>;
}
[data-theme="light"] .preview-btn-primary {
background: <?= e($active_colors['light_foreground']) ?>;
color: <?= e($active_colors['light_background']) ?>;
}
[data-theme="dark"] .preview-btn-secondary {
background: transparent;
border: 1px solid <?= e($active_colors['dark_border']) ?>;
color: <?= e($active_colors['dark_foreground']) ?>;
}
[data-theme="light"] .preview-btn-secondary {
background: transparent;
border: 1px solid <?= e($active_colors['light_border']) ?>;
color: <?= e($active_colors['light_foreground']) ?>;
}
.preview-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 48px;
max-width: 1000px;
margin: 0 auto 80px;
text-align: center;
padding: 60px 0;
}
[data-theme="dark"] .preview-stats {
border-top: 1px solid <?= e($active_colors['dark_border']) ?>;
border-bottom: 1px solid <?= e($active_colors['dark_border']) ?>;
}
[data-theme="light"] .preview-stats {
border-top: 1px solid <?= e($active_colors['light_border']) ?>;
border-bottom: 1px solid <?= e($active_colors['light_border']) ?>;
}
.preview-stat h3 {
font-size: 3rem;
font-weight: 700;
margin-bottom: 8px;
}
.preview-stat p {
font-size: 0.875rem;
}
[data-theme="dark"] .preview-stat p {
color: <?= e($active_colors['dark_muted']) ?>;
}
[data-theme="light"] .preview-stat p {
color: <?= e($active_colors['light_muted']) ?>;
}
.preview-features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
max-width: 1200px;
margin: 0 auto;
}
.preview-feature {
border-radius: 16px;
padding: 32px;
transition: all 0.3s;
}
[data-theme="dark"] .preview-feature {
background: <?= e($active_colors['dark_card']) ?>;
border: 1px solid <?= e($active_colors['dark_border']) ?>;
}
[data-theme="light"] .preview-feature {
background: <?= e($active_colors['light_card']) ?>;
border: 1px solid <?= e($active_colors['light_border']) ?>;
}
[data-theme="dark"] .preview-feature:hover {
background: <?= e($active_colors['dark_card_hover']) ?>;
}
[data-theme="light"] .preview-feature:hover {
background: <?= e($active_colors['light_card_hover']) ?>;
}
.preview-feature:hover {
transform: translateY(-4px);
}
.preview-feature-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
margin-bottom: 20px;
font-size: 24px;
}
.preview-feature-icon{
padding: 12px;
}
[data-theme="dark"] .preview-feature-icon {
background: <?= e($active_colors['dark_background']) ?>;
border: 1px solid <?= e($active_colors['dark_border']) ?>;
}
[data-theme="light"] .preview-feature-icon {
background: <?= e($active_colors['light_background']) ?>;
border: 1px solid <?= e($active_colors['light_border']) ?>;
}
.preview-feature h3 {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 12px;
}
.preview-feature p {
line-height: 1.6;
}
[data-theme="dark"] .preview-feature p {
color: <?= e($active_colors['dark_muted']) ?>;
}
[data-theme="light"] .preview-feature p {
color: <?= e($active_colors['light_muted']) ?>;
}
</style>
</head>
<body>
<div class="app-wrapper">
<aside class="sidebar">
<div class="sidebar-header">
<div class="brand">
<div class="brand-icon">
<img src="../../logos/flatlypage_light.svg" alt="Logo">
</div>
<div class="brand-text">
<h1>Theme Builder Pro</h1>
<p>FlatlyPage CMS v2.5</p>
</div>
</div>
</div>
<div class="sidebar-content">
<?php if ($error_message): ?>
<div class="alert alert-error">
<svg class="alert-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<span><?= e($error_message) ?></span>
</div>
<?php endif; ?>
<?php if ($success_message): ?>
<div class="alert alert-success">
<svg class="alert-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<span><?= e($success_message) ?></span>
</div>
<?php endif; ?>
<form method="POST" id="themeBuilderForm">
<input type="hidden" name="csrf_token" value="<?= e($_SESSION['theme_builder_token']) ?>">
<input type="hidden" name="action" value="generate">
<!-- THEME INFO -->
<div class="form-section">
<div class="section-title">Theme Information</div>
<div class="form-group">
<label class="form-label required" for="theme_name">Theme Name</label>
<input type="text" id="theme_name" name="theme_name" class="form-input"
placeholder="e.g., Midnight Blue" maxlength="100" required
value="<?= e($_POST['theme_name'] ?? '') ?>">
</div>
<div class="form-group">
<label class="form-label required" for="author">Author</label>
<input type="text" id="author" name="author" class="form-input"
placeholder="Your name" maxlength="100" required
value="<?= e($_POST['author'] ?? '') ?>">
</div>
</div>
<!-- MODE TABS -->
<div class="form-section">
<div class="section-title">Color Modes</div>
<div class="mode-tabs">
<button type="button" class="mode-tab active" onclick="switchMode(event, 'dark')">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24" style="display: inline-block; vertical-align: middle; margin-right: 6px;">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
Dark Mode
</button>
<button type="button" class="mode-tab" onclick="switchMode(event, 'light')">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24" style="display: inline-block; vertical-align: middle; margin-right: 6px;">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="12" y1="21" x2="12" y2="23" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="1" y1="12" x2="3" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="21" y1="12" x2="23" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Light Mode
</button>
</div>
<!-- DARK MODE COLORS -->
<div id="dark-mode" class="mode-content active">
<div class="preset-grid" style="margin-bottom: 20px;">
<button type="button" class="preset-button" onclick="applyPreset('dark', 'classic')">
<div class="preset-name">Classic Dark</div>
<div class="preset-colors">
<div class="preset-color-dot" style="background: #111111"></div>
<div class="preset-color-dot" style="background: #fafafa"></div>
<div class="preset-color-dot" style="background: #22c55e"></div>
</div>
</button>
<button type="button" class="preset-button" onclick="applyPreset('dark', 'midnight')">
<div class="preset-name">Midnight Blue</div>
<div class="preset-colors">
<div class="preset-color-dot" style="background: #0a0a1a"></div>
<div class="preset-color-dot" style="background: #3b82f6"></div>
<div class="preset-color-dot" style="background: #1e40af"></div>
</div>
</button>
<button type="button" class="preset-button" onclick="applyPreset('dark', 'purple')">
<div class="preset-name">Purple Night</div>
<div class="preset-colors">
<div class="preset-color-dot" style="background: #1a0a1a"></div>
<div class="preset-color-dot" style="background: #a855f7"></div>
<div class="preset-color-dot" style="background: #7c3aed"></div>
</div>
</button>
<button type="button" class="preset-button" onclick="applyPreset('dark', 'green')">
<div class="preset-name">Forest</div>
<div class="preset-colors">
<div class="preset-color-dot" style="background: #0f1c14"></div>
<div class="preset-color-dot" style="background: #4ade80"></div>
<div class="preset-color-dot" style="background: #166534"></div>
</div>
</button>
</div>
<div class="color-grid">
<?php
$dark_fields = [
['name' => 'dark_background', 'label' => 'Background', 'help' => 'Main background'],
['name' => 'dark_foreground', 'label' => 'Foreground', 'help' => 'Text color'],
['name' => 'dark_card', 'label' => 'Card', 'help' => 'Card backgrounds'],
['name' => 'dark_card_hover', 'label' => 'Card Hover', 'help' => 'Hover state'],
['name' => 'dark_muted', 'label' => 'Muted', 'help' => 'Secondary text'],
['name' => 'dark_border', 'label' => 'Border', 'help' => 'Border color'],
['name' => 'dark_success', 'label' => 'Success', 'help' => 'Success color'],
['name' => 'dark_avatar_from', 'label' => 'Avatar Start', 'help' => 'Gradient start'],
['name' => 'dark_avatar_to', 'label' => 'Avatar End', 'help' => 'Gradient end'],
];
foreach ($dark_fields as $field):
$value = $active_colors[$field['name']];
?>
<div class="form-group">
<label class="form-label" for="<?= e($field['name']) ?>"><?= e($field['label']) ?></label>
<div class="color-input-group">
<div class="color-swatch">
<input type="color" id="<?= e($field['name']) ?>_picker" value="<?= e($value) ?>">
<div class="color-swatch-preview" style="background-color: <?= e($value) ?>"></div>
</div>
<input type="text" id="<?= e($field['name']) ?>" name="<?= e($field['name']) ?>"
class="form-input color-text-input" pattern="^#[0-9A-Fa-f]{6}$"
value="<?= e($value) ?>" required>
</div>
<div class="form-help"><?= e($field['help']) ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- LIGHT MODE COLORS -->
<div id="light-mode" class="mode-content">
<div class="preset-grid" style="margin-bottom: 20px;">
<button type="button" class="preset-button" onclick="applyPreset('light', 'classic')">
<div class="preset-name">Classic Light</div>
<div class="preset-colors">
<div class="preset-color-dot" style="background: #ffffff"></div>
<div class="preset-color-dot" style="background: #111111"></div>
<div class="preset-color-dot" style="background: #16a34a"></div>
</div>
</button>
<button type="button" class="preset-button" onclick="applyPreset('light', 'blue')">
<div class="preset-name">Sky Blue</div>
<div class="preset-colors">
<div class="preset-color-dot" style="background: #f0f9ff"></div>
<div class="preset-color-dot" style="background: #0c4a6e"></div>
<div class="preset-color-dot" style="background: #0284c7"></div>
</div>
</button>
<button type="button" class="preset-button" onclick="applyPreset('light', 'warm')">
<div class="preset-name">Warm Beige</div>
<div class="preset-colors">
<div class="preset-color-dot" style="background: #fef3c7"></div>
<div class="preset-color-dot" style="background: #78350f"></div>
<div class="preset-color-dot" style="background: #d97706"></div>
</div>
</button>
<button type="button" class="preset-button" onclick="applyPreset('light', 'mint')">
<div class="preset-name">Fresh Mint</div>
<div class="preset-colors">
<div class="preset-color-dot" style="background: #ecfdf5"></div>
<div class="preset-color-dot" style="background: #064e3b"></div>
<div class="preset-color-dot" style="background: #059669"></div>
</div>
</button>
</div>
<div class="color-grid">
<?php
$light_fields = [
['name' => 'light_background', 'label' => 'Background', 'help' => 'Main background'],
['name' => 'light_foreground', 'label' => 'Foreground', 'help' => 'Text color'],
['name' => 'light_card', 'label' => 'Card', 'help' => 'Card backgrounds'],
['name' => 'light_card_hover', 'label' => 'Card Hover', 'help' => 'Hover state'],
['name' => 'light_muted', 'label' => 'Muted', 'help' => 'Secondary text'],
['name' => 'light_border', 'label' => 'Border', 'help' => 'Border color'],
['name' => 'light_success', 'label' => 'Success', 'help' => 'Success color'],
['name' => 'light_avatar_from', 'label' => 'Avatar Start', 'help' => 'Gradient start'],
['name' => 'light_avatar_to', 'label' => 'Avatar End', 'help' => 'Gradient end'],
];
foreach ($light_fields as $field):
$value = $active_colors[$field['name']];
?>
<div class="form-group">
<label class="form-label" for="<?= e($field['name']) ?>"><?= e($field['label']) ?></label>
<div class="color-input-group">
<div class="color-swatch">
<input type="color" id="<?= e($field['name']) ?>_picker" value="<?= e($value) ?>">
<div class="color-swatch-preview" style="background-color: <?= e($value) ?>"></div>
</div>
<input type="text" id="<?= e($field['name']) ?>" name="<?= e($field['name']) ?>"
class="form-input color-text-input" pattern="^#[0-9A-Fa-f]{6}$"
value="<?= e($value) ?>" required>
</div>
<div class="form-help"><?= e($field['help']) ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</form>
</div>
<div class="sidebar-footer">
<div class="btn-group">
<button type="submit" form="themeBuilderForm" class="btn btn-primary btn-block">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
Generate Theme
</button>
<?php if ($generated_css): ?>
<button type="button" onclick="copyToClipboard()" class="btn btn-secondary btn-block">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
Copy CSS
</button>
<button type="button" onclick="downloadTheme()" class="btn btn-secondary btn-block">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Download CSS
</button>
<?php endif; ?>
</div>
</div>
</aside>
<main class="main-content">
<header class="content-header">
<h2 style="font-size: 1.125rem; font-weight: 600;">Preview & Export</h2>
<div class="tabs">
<button type="button" class="tab-button active" onclick="switchTab(event, 'preview')">
Live Preview
</button>
<button type="button" class="tab-button" onclick="switchTab(event, 'code')">
CSS Code
</button>
</div>
</header>
<div class="content-body">
<div id="preview-tab" class="tab-content active">
<div class="preview-container">
<div class="preview-frame">
<div class="preview-toolbar">
<div class="preview-dots">
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
</div>
<div class="preview-url">localhost:8000/preview</div>
<button type="button" class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.8125rem;" onclick="togglePreviewMode()">
<svg id="preview-mode-icon" width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<span id="preview-mode-text">Dark</span>
</button>
</div>
<div class="preview-content">
<div id="livePreview" class="preview-iframe-wrapper" data-theme="dark">
<div class="preview-hero">
<div class="preview-badge">
<svg width="14" height="14" fill="currentColor" viewBox="0 0 24 24" style="display: inline-block; vertical-align: middle; margin-right: 4px;">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
Theme Preview
</div>
<h1>Your Custom Theme</h1>
<p>Experience your color palette in action with this live preview</p>
<div class="preview-buttons">
<button class="preview-btn preview-btn-primary">Get Started</button>
<button class="preview-btn preview-btn-secondary">Learn More</button>
</div>
</div>
<div class="preview-stats">
<div class="preview-stat">
<h3>99%</h3>
<p>Satisfaction</p>
</div>
<div class="preview-stat">
<h3>50K+</h3>
<p>Users</p>
</div>
<div class="preview-stat">
<h3>24/7</h3>
<p>Support</p>
</div>
<div class="preview-stat">
<h3>100+</h3>
<p>Features</p>
</div>
</div>
<div class="preview-features">
<div class="preview-feature">
<div class="preview-feature-icon">
<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
</div>
<h3>Fast Performance</h3>
<p>Optimized for speed and efficiency</p>
</div>
<div class="preview-feature">
<div class="preview-feature-icon">
<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h3>Secure</h3>
<p>Enterprise-grade security</p>
</div>
<div class="preview-feature">
<div class="preview-feature-icon">
<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
</div>
<h3>Responsive</h3>
<p>Works on all devices</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="code-tab" class="tab-content">
<?php if ($generated_css): ?>
<div class="code-container">
<div class="code-header">
<div class="code-title">styles.css</div>
<span class="badge">Ready to export</span>
</div>
<div class="code-output" id="cssOutput"><?= e($generated_css) ?></div>
</div>
<?php else: ?>
<div class="empty-state">
<div class="empty-state-title">No CSS Generated Yet</div>
<div style="color: var(--text-tertiary); font-size: 0.9375rem;">
Configure your theme and click "Generate Theme"
</div>
</div>
<?php endif; ?>
</div>
</div>
</main>
</div>
<script>
// Mode switching for sidebar tabs
function switchMode(event, mode) {
event.preventDefault();
document.querySelectorAll('.mode-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.mode-content').forEach(content => content.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(mode + '-mode').classList.add('active');
}
// Tab switching for main content
function switchTab(event, tab) {
event.preventDefault();
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tab + '-tab').classList.add('active');
}
// Color synchronization
function syncColor(fieldName) {
const picker = document.getElementById(fieldName + '_picker');
const input = document.getElementById(fieldName);
const preview = picker?.nextElementSibling;
if (!picker || !input || !preview) return;
picker.addEventListener('input', function() {
input.value = this.value.toUpperCase();
preview.style.backgroundColor = this.value;
});
input.addEventListener('input', function() {
if (/^#[0-9A-Fa-f]{6}$/.test(this.value)) {
picker.value = this.value;
preview.style.backgroundColor = this.value;
}
});
}
// Initialize all color fields
const allColorFields = [
'dark_background', 'dark_foreground', 'dark_card', 'dark_card_hover',
'dark_muted', 'dark_border', 'dark_success', 'dark_avatar_from', 'dark_avatar_to',
'light_background', 'light_foreground', 'light_card', 'light_card_hover',
'light_muted', 'light_border', 'light_success', 'light_avatar_from', 'light_avatar_to'
];
allColorFields.forEach(syncColor);
// Preview mode toggle
function togglePreviewMode() {
const preview = document.getElementById('livePreview');
const currentTheme = preview.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
preview.setAttribute('data-theme', newTheme);
const iconSvg = document.getElementById('preview-mode-icon');
const textSpan = document.getElementById('preview-mode-text');
if (newTheme === 'dark') {
// Moon icon
iconSvg.innerHTML = '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>';
textSpan.textContent = 'Dark';
} else {
// Sun icon
iconSvg.innerHTML = '<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="12" y1="21" x2="12" y2="23" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="1" y1="12" x2="3" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="21" y1="12" x2="23" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>';
textSpan.textContent = 'Light';
}
}
// Preset application
const presets = {
dark: {
classic: {
dark_background: '#111111', dark_foreground: '#fafafa', dark_card: '#1a1a1a',
dark_card_hover: '#222222', dark_muted: '#888888', dark_border: '#2a2a2a',
dark_success: '#22c55e', dark_avatar_from: '#333333', dark_avatar_to: '#555555'
},
midnight: {
dark_background: '#0a0a1a', dark_foreground: '#e0e7ff', dark_card: '#1a1a2e',
dark_card_hover: '#252538', dark_muted: '#94a3b8', dark_border: '#2a2a40',
dark_success: '#3b82f6', dark_avatar_from: '#1e3a8a', dark_avatar_to: '#3b82f6'
},
purple: {
dark_background: '#1a0a1a', dark_foreground: '#f3e8ff', dark_card: '#2a1a2a',
dark_card_hover: '#3a2a3a', dark_muted: '#c084fc', dark_border: '#4a2a4a',
dark_success: '#a855f7', dark_avatar_from: '#7c3aed', dark_avatar_to: '#a855f7'
},
green: {
dark_background: '#0f1c14', dark_foreground: '#e8f5e9', dark_card: '#1a2e1f',
dark_card_hover: '#253a2a', dark_muted: '#81c784', dark_border: '#2a4030',
dark_success: '#4ade80', dark_avatar_from: '#166534', dark_avatar_to: '#4ade80'
}
},
light: {
classic: {
light_background: '#ffffff', light_foreground: '#111111', light_card: '#f5f5f5',
light_card_hover: '#efefef', light_muted: '#666666', light_border: '#e0e0e0',
light_success: '#16a34a', light_avatar_from: '#dddddd', light_avatar_to: '#bbbbbb'
},
blue: {
light_background: '#f0f9ff', light_foreground: '#0c4a6e', light_card: '#e0f2fe',
light_card_hover: '#bae6fd', light_muted: '#475569', light_border: '#7dd3fc',
light_success: '#0284c7', light_avatar_from: '#38bdf8', light_avatar_to: '#0284c7'
},
warm: {
light_background: '#fef3c7', light_foreground: '#78350f', light_card: '#fef08a',
light_card_hover: '#fde047', light_muted: '#854d0e', light_border: '#fde68a',
light_success: '#d97706', light_avatar_from: '#fbbf24', light_avatar_to: '#d97706'
},
mint: {
light_background: '#ecfdf5', light_foreground: '#064e3b', light_card: '#d1fae5',
light_card_hover: '#a7f3d0', light_muted: '#047857', light_border: '#6ee7b7',
light_success: '#059669', light_avatar_from: '#34d399', light_avatar_to: '#059669'
}
}
};
function applyPreset(mode, preset) {
const colors = presets[mode][preset];
Object.keys(colors).forEach(key => {
const input = document.getElementById(key);
const picker = document.getElementById(key + '_picker');
const preview = picker?.nextElementSibling;
if (input) {
input.value = colors[key].toUpperCase();
if (picker) picker.value = colors[key];
if (preview) preview.style.backgroundColor = colors[key];
}
});
}
// Clipboard
function copyToClipboard() {
const output = document.getElementById('cssOutput');
if (!output?.textContent) {
alert('Please generate a theme first!');
return;
}
navigator.clipboard.writeText(output.textContent).then(() => {
alert('✓ CSS copied to clipboard!');
}).catch(() => {
alert('Failed to copy CSS');
});
}
function downloadTheme() {
const output = document.getElementById('cssOutput');
if (!output?.textContent) {
alert('Please generate a theme first!');
return;
}
const themeName = document.getElementById('theme_name').value || 'theme';
const filename = themeName.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '.css';
const blob = new Blob([output.textContent], { type: 'text/css' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
}
</script>
</body>
</html>