mirror of
https://github.com/JGH0/Todo-App-Backend.git
synced 2026-06-03 13:28:47 +02:00
Merge branch 'main' into APIhardening
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -126,3 +126,5 @@ _modules/*
|
|||||||
/phpunit*.xml
|
/phpunit*.xml
|
||||||
.env
|
.env
|
||||||
env
|
env
|
||||||
|
.claude/
|
||||||
|
.claude/*
|
||||||
@@ -16,7 +16,7 @@ class App extends BaseConfig
|
|||||||
*
|
*
|
||||||
* E.g., http://example.com/
|
* E.g., http://example.com/
|
||||||
*/
|
*/
|
||||||
public string $baseURL = 'http://localhost:8080/';
|
public string $baseURL = 'http://localhost/Todo-App-Backend/public/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
|
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class Cors extends BaseConfig
|
|||||||
*
|
*
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
|
||||||
*/
|
*/
|
||||||
'supportsCredentials' => false,
|
'supportsCredentials' => true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set headers to allow.
|
* Set headers to allow.
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ use CodeIgniter\Router\RouteCollection;
|
|||||||
* @var RouteCollection $routes
|
* @var RouteCollection $routes
|
||||||
*/
|
*/
|
||||||
$routes->get('/', 'Home::index');
|
$routes->get('/', 'Home::index');
|
||||||
|
$routes->get('/themes', 'ThemeStore::index');
|
||||||
|
$routes->post('/themes/upload', 'ThemeStore::upload');
|
||||||
|
$routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1');
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// API Routes - Version 1.0
|
// API Routes - Version 1.0
|
||||||
@@ -91,3 +94,20 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\V1', 'filter' => [
|
|||||||
$routes->put('user/themes/(:segment)', 'UserThemeController::update/$1');
|
$routes->put('user/themes/(:segment)', 'UserThemeController::update/$1');
|
||||||
$routes->delete('user/themes/(:segment)', 'UserThemeController::delete/$1');
|
$routes->delete('user/themes/(:segment)', 'UserThemeController::delete/$1');
|
||||||
});
|
});
|
||||||
|
$routes->get('/themes', 'ThemeStore::index');
|
||||||
|
$routes->options('/themes', static function () {
|
||||||
|
header('Access-Control-Allow-Origin: http://localhost:5173');
|
||||||
|
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type, Accept, Fetch');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
return response()->setStatusCode(204);
|
||||||
|
});
|
||||||
|
$routes->post('/themes/upload', 'ThemeStore::upload');
|
||||||
|
$routes->options('/themes/upload', static function () {
|
||||||
|
header('Access-Control-Allow-Origin: http://localhost:5173');
|
||||||
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type, Accept, Fetch');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
return response()->setStatusCode(204);
|
||||||
|
});
|
||||||
|
$routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1');
|
||||||
|
|||||||
266
app/Controllers/ThemeStore.php
Normal file
266
app/Controllers/ThemeStore.php
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\MarketplaceThemeModel;
|
||||||
|
use App\Models\UserThemeModel;
|
||||||
|
use CodeIgniter\HTTP\Response;
|
||||||
|
|
||||||
|
class ThemeStore extends BaseController
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$model = new MarketplaceThemeModel();
|
||||||
|
$themes = $model->where('is_published', 1)->findAll();
|
||||||
|
|
||||||
|
foreach ($themes as &$theme) {
|
||||||
|
$meta = json_decode($theme['metadata'] ?? '{}', true);
|
||||||
|
$theme['colors'] = $meta['colors'] ?? [];
|
||||||
|
$theme['tags'] = $meta['tags'] ?? [];
|
||||||
|
$theme['vars'] = $meta['vars'] ?? [];
|
||||||
|
|
||||||
|
// Provide a preview array compatible with the frontend
|
||||||
|
$theme['preview'] = !empty($theme['colors']) ? array_values($theme['colors']) : ['#ffffff', '#f0f0f0', '#007acc'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->isAJAX() || $this->request->hasHeader('Fetch') || str_contains($this->request->getHeaderLine('Accept'), 'application/json')) {
|
||||||
|
header('Access-Control-Allow-Origin: http://localhost:5173');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
return $this->response->setJSON($themes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('theme_store', [
|
||||||
|
'themes' => $themes,
|
||||||
|
'flash_success' => session()->getFlashdata('success'),
|
||||||
|
'flash_error' => session()->getFlashdata('error'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload(): Response
|
||||||
|
{
|
||||||
|
header('Access-Control-Allow-Origin: http://localhost:5173');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
|
||||||
|
$file = $this->request->getFile('theme_css');
|
||||||
|
$displayName = trim($this->request->getPost('display_name') ?? '');
|
||||||
|
$description = trim($this->request->getPost('description') ?? '');
|
||||||
|
|
||||||
|
if ($displayName === '') {
|
||||||
|
if ($this->request->isAJAX() || $this->request->hasHeader('Fetch')) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['error' => 'Display name is required.']);
|
||||||
|
}
|
||||||
|
return redirect()->to('themes')->with('error', 'Display name is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $file || ! $file->isValid() || $file->hasMoved()) {
|
||||||
|
if ($this->request->isAJAX() || $this->request->hasHeader('Fetch')) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['error' => 'Please upload a valid CSS file.']);
|
||||||
|
}
|
||||||
|
return redirect()->to('themes')->with('error', 'Please upload a valid CSS file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strtolower($file->getExtension()) !== 'css') {
|
||||||
|
if ($this->request->isAJAX() || $this->request->hasHeader('Fetch')) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['error' => 'Only .css files are allowed.']);
|
||||||
|
}
|
||||||
|
return redirect()->to('themes')->with('error', 'Only .css files are allowed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $displayName));
|
||||||
|
$slug = trim($slug, '-');
|
||||||
|
$filename = $slug . '-' . substr(bin2hex(random_bytes(3)), 0, 6) . '.css';
|
||||||
|
|
||||||
|
$file->move(FCPATH . 'themes', $filename, true);
|
||||||
|
|
||||||
|
// Extract CSS variables and colors from the uploaded file
|
||||||
|
$cssContent = file_get_contents(FCPATH . 'themes/' . $filename);
|
||||||
|
preg_match_all('/(--[a-zA-Z0-9-]+)\s*:\s*([^;]+);/', $cssContent, $matches);
|
||||||
|
|
||||||
|
$vars = [];
|
||||||
|
if (!empty($matches[1])) {
|
||||||
|
foreach ($matches[1] as $index => $key) {
|
||||||
|
$vars[$key] = trim($matches[2][$index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate 3-color preview based on standard variables
|
||||||
|
$colors = [];
|
||||||
|
if (isset($vars['--bg'])) $colors['bg'] = $vars['--bg'];
|
||||||
|
if (isset($vars['--surface'])) $colors['surface'] = $vars['--surface'];
|
||||||
|
if (isset($vars['--accent'])) $colors['accent'] = $vars['--accent'];
|
||||||
|
|
||||||
|
$model = new MarketplaceThemeModel();
|
||||||
|
$model->insert([
|
||||||
|
'id' => $this->uuid4(),
|
||||||
|
'name' => $slug,
|
||||||
|
'display_name' => $displayName,
|
||||||
|
'description' => $description ?: 'Custom community theme.',
|
||||||
|
'author' => 'Community',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'thumbnail_url' => null,
|
||||||
|
'download_url' => '/themes/' . $filename,
|
||||||
|
'price' => 0,
|
||||||
|
'is_published' => true,
|
||||||
|
'metadata' => json_encode([
|
||||||
|
'tags' => ['custom', 'community'],
|
||||||
|
'colors' => $colors,
|
||||||
|
'vars' => $vars
|
||||||
|
]),
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->request->isAJAX() || $this->request->hasHeader('Fetch')) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'success' => true,
|
||||||
|
'message' => '"' . esc($displayName) . '" uploaded successfully!'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return redirect()->to('themes')->with('success', '"' . esc($displayName) . '" uploaded successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preview(string $id): Response
|
||||||
|
{
|
||||||
|
$model = new MarketplaceThemeModel();
|
||||||
|
$theme = $model->find($id);
|
||||||
|
|
||||||
|
if (! $theme) {
|
||||||
|
return $this->response->setStatusCode(404)->setBody('<p style="font-family:sans-serif;padding:2rem">Theme not found.</p>');
|
||||||
|
}
|
||||||
|
|
||||||
|
$distIndex = '/home/came/Nextcloud/arch-work/Projects/Todo-App/dist/index.html';
|
||||||
|
|
||||||
|
if (! file_exists($distIndex)) {
|
||||||
|
return $this->response->setBody(
|
||||||
|
'<html><body style="font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#0f0f17;color:#94a3b8">'
|
||||||
|
. '<div style="text-align:center"><p>Todo app dist not found.</p></div></body></html>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$todoHtml = file_get_contents($distIndex);
|
||||||
|
|
||||||
|
// Rewrite asset paths from /assets/ to the public symlink so Apache serves them
|
||||||
|
$assetBase = rtrim(base_url('todo-preview'), '/');
|
||||||
|
$todoHtml = str_replace('="/assets/', '="' . $assetBase . '/assets/', $todoHtml);
|
||||||
|
|
||||||
|
// Build CSS variable overrides from the stored vars map
|
||||||
|
$meta = json_decode($theme['metadata'] ?? '{}', true);
|
||||||
|
$vars = $meta['vars'] ?? [];
|
||||||
|
|
||||||
|
$cssVars = ":root {\n";
|
||||||
|
foreach ($vars as $prop => $value) {
|
||||||
|
$cssVars .= " {$prop}: {$value};\n";
|
||||||
|
}
|
||||||
|
$cssVars .= "}\n";
|
||||||
|
|
||||||
|
// Also inject any raw CSS from the downloaded file (for custom/uploaded themes)
|
||||||
|
$cssPath = FCPATH . ltrim($theme['download_url'], '/');
|
||||||
|
$rawCss = file_exists($cssPath) ? file_get_contents($cssPath) : '';
|
||||||
|
|
||||||
|
$styleTag = "<style>\n/* Theme Store: {$theme['display_name']} */\n{$cssVars}\n{$rawCss}\n</style>";
|
||||||
|
|
||||||
|
$todoHtml = str_replace('</head>', $styleTag . "\n</head>", $todoHtml);
|
||||||
|
|
||||||
|
return $this->response
|
||||||
|
->setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
|
->setBody($todoHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function install(string $id): Response
|
||||||
|
{
|
||||||
|
$model = new MarketplaceThemeModel();
|
||||||
|
$theme = $model->find($id);
|
||||||
|
|
||||||
|
if (! $theme) {
|
||||||
|
return $this->response->setStatusCode(404)->setJSON(['error' => 'Theme not found in the marketplace.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using session user_id or a default placeholder since standard auth might be configured separately
|
||||||
|
$userId = session()->get('user_id') ?? 'default-user-id';
|
||||||
|
|
||||||
|
$userThemeModel = new UserThemeModel();
|
||||||
|
|
||||||
|
if (! $userThemeModel->isInstalled($userId, $id)) {
|
||||||
|
$userThemeModel->installTheme($userId, $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'success' => true,
|
||||||
|
'message' => '"' . esc($theme['display_name']) . '" has been installed to your account.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activate(string $id): Response
|
||||||
|
{
|
||||||
|
$userId = session()->get('user_id') ?? 'default-user-id';
|
||||||
|
|
||||||
|
$userThemeModel = new UserThemeModel();
|
||||||
|
|
||||||
|
if (! $userThemeModel->isInstalled($userId, $id)) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['error' => 'Theme must be installed before it can be activated.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userThemeModel->setActiveTheme($userId, $id);
|
||||||
|
|
||||||
|
return $this->response->setJSON(['success' => true, 'message' => 'Theme activated successfully.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uninstall(string $id): Response
|
||||||
|
{
|
||||||
|
$userId = session()->get('user_id') ?? 'default-user-id';
|
||||||
|
$userThemeModel = new UserThemeModel();
|
||||||
|
|
||||||
|
$userThemeModel->uninstallTheme($userId, $id);
|
||||||
|
|
||||||
|
return $this->response->setJSON(['success' => true, 'message' => 'Theme successfully uninstalled.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function myThemes(): Response
|
||||||
|
{
|
||||||
|
$userId = session()->get('user_id') ?? 'default-user-id';
|
||||||
|
$userThemeModel = new UserThemeModel();
|
||||||
|
|
||||||
|
return $this->response->setJSON(['success' => true, 'data' => $userThemeModel->getUserThemes($userId)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serveCss(string $filename): Response
|
||||||
|
{
|
||||||
|
// Ensure it's just a file name (prevent directory traversal)
|
||||||
|
$filename = basename($filename);
|
||||||
|
$cssPath = FCPATH . 'themes/' . $filename;
|
||||||
|
|
||||||
|
// If the file actually exists on disk (e.g. newly uploaded themes)
|
||||||
|
if (file_exists($cssPath)) {
|
||||||
|
$css = file_get_contents($cssPath);
|
||||||
|
return $this->response->setContentType('text/css')->setBody($css);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate dynamically for seeded themes that don't have a physical file
|
||||||
|
$model = new MarketplaceThemeModel();
|
||||||
|
$name = preg_replace('/\.css$/i', '', $filename);
|
||||||
|
$theme = $model->where('name', $name)->first();
|
||||||
|
|
||||||
|
if (! $theme) {
|
||||||
|
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = json_decode($theme['metadata'] ?? '{}', true);
|
||||||
|
$vars = $meta['vars'] ?? [];
|
||||||
|
|
||||||
|
$css = "/* Theme: {$theme['display_name']} */\n:root {\n";
|
||||||
|
foreach ($vars as $prop => $value) {
|
||||||
|
$css .= " {$prop}: {$value};\n";
|
||||||
|
}
|
||||||
|
$css .= "}\n";
|
||||||
|
|
||||||
|
return $this->response->setContentType('text/css')->setBody($css);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function uuid4(): string
|
||||||
|
{
|
||||||
|
$data = random_bytes(16);
|
||||||
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||||
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||||
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,34 +8,296 @@ class MarketplaceThemesSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
public function run()
|
public function run()
|
||||||
{
|
{
|
||||||
|
$this->db->query('SET FOREIGN_KEY_CHECKS=0');
|
||||||
|
$this->db->table('marketplace_themes')->truncate();
|
||||||
|
$this->db->query('SET FOREIGN_KEY_CHECKS=1');
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
[
|
[
|
||||||
'id' => '550e8400-e29b-41d4-a716-446655440010',
|
'id' => '550e8400-e29b-41d4-a716-446655440010',
|
||||||
'name' => 'default-light',
|
'name' => 'ocean-breeze',
|
||||||
'display_name' => 'Default Light',
|
'display_name' => 'Ocean Breeze',
|
||||||
'description' => 'Clean and simple light theme',
|
'description' => 'A refreshing light theme inspired by the open sea. Soft teals and ocean blues create a calm, productive workspace that\'s easy on the eyes during long work sessions.',
|
||||||
'author' => 'System',
|
'author' => 'ThemeForge',
|
||||||
'version' => '1.0.0',
|
'version' => '1.2.0',
|
||||||
'thumbnail_url' => null,
|
'thumbnail_url' => null,
|
||||||
'download_url' => '/themes/default-light.zip',
|
'download_url' => '/themes/ocean-breeze.css',
|
||||||
'price' => 0,
|
'price' => 0,
|
||||||
'is_published' => true,
|
'is_published' => true,
|
||||||
'metadata' => json_encode(['tags' => ['light', 'clean']]),
|
'metadata' => json_encode([
|
||||||
|
'tags' => ['light', 'blue', 'calm', 'minimal'],
|
||||||
|
'colors' => [
|
||||||
|
'Primary' => '#0077B6',
|
||||||
|
'Secondary' => '#00B4D8',
|
||||||
|
'Background' => '#E0F4FF',
|
||||||
|
'Surface' => '#FFFFFF',
|
||||||
|
'Text' => '#1A2B3C',
|
||||||
|
'Accent' => '#48CAE4',
|
||||||
|
],
|
||||||
|
'vars' => [
|
||||||
|
'--bg' => '#E0F4FF',
|
||||||
|
'--surface' => '#FFFFFF',
|
||||||
|
'--surface-strong' => '#FFFFFF',
|
||||||
|
'--surface-muted' => '#F0F9FF',
|
||||||
|
'--border' => '#BAE0F2',
|
||||||
|
'--line' => '#90C8E0',
|
||||||
|
'--text' => '#1A2B3C',
|
||||||
|
'--text-muted' => '#4A6B7A',
|
||||||
|
'--text-strong' => '#0D1B26',
|
||||||
|
'--accent' => '#0077B6',
|
||||||
|
'--accent-text' => '#FFFFFF',
|
||||||
|
'--accent-soft' => '#CCE9F5',
|
||||||
|
'--sidebar-bg' => '#FFFFFF',
|
||||||
|
'--sidebar-border' => '#BAE0F2',
|
||||||
|
'--sidebar-text' => '#1A2B3C',
|
||||||
|
'--sidebar-text-muted' => '#4A6B7A',
|
||||||
|
'--input-bg' => '#FFFFFF',
|
||||||
|
'--input-border' => '#BAE0F2',
|
||||||
|
'--modal-bg' => '#FFFFFF',
|
||||||
|
'--chip' => '#C8E8F0',
|
||||||
|
'--success' => '#D4F0E4',
|
||||||
|
],
|
||||||
|
]),
|
||||||
'created_at' => date('Y-m-d H:i:s'),
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'id' => '550e8400-e29b-41d4-a716-446655440011',
|
'id' => '550e8400-e29b-41d4-a716-446655440011',
|
||||||
'name' => 'default-dark',
|
'name' => 'midnight-void',
|
||||||
'display_name' => 'Default Dark',
|
'display_name' => 'Midnight Void',
|
||||||
'description' => 'Dark theme for night owls',
|
'description' => 'Deep space dark theme for night owls and late-night coders. Rich dark purples and blues with vibrant neon accents give this theme a premium, modern feel.',
|
||||||
'author' => 'System',
|
'author' => 'ThemeForge',
|
||||||
'version' => '1.0.0',
|
'version' => '2.0.1',
|
||||||
'thumbnail_url' => null,
|
'thumbnail_url' => null,
|
||||||
'download_url' => '/themes/default-dark.zip',
|
'download_url' => '/themes/midnight-void.css',
|
||||||
'price' => 0,
|
'price' => 0,
|
||||||
'is_published' => true,
|
'is_published' => true,
|
||||||
'metadata' => json_encode(['tags' => ['dark', 'night']]),
|
'metadata' => json_encode([
|
||||||
|
'tags' => ['dark', 'purple', 'neon', 'night'],
|
||||||
|
'colors' => [
|
||||||
|
'Primary' => '#7C3AED',
|
||||||
|
'Secondary' => '#A78BFA',
|
||||||
|
'Background' => '#0D0D1A',
|
||||||
|
'Surface' => '#1A1A2E',
|
||||||
|
'Text' => '#E2E8F0',
|
||||||
|
'Accent' => '#F472B6',
|
||||||
|
],
|
||||||
|
'vars' => [
|
||||||
|
'--bg' => '#0D0D1A',
|
||||||
|
'--surface' => '#1A1A2E',
|
||||||
|
'--surface-strong' => '#222234',
|
||||||
|
'--surface-muted' => '#121220',
|
||||||
|
'--border' => '#2A2A44',
|
||||||
|
'--line' => '#333350',
|
||||||
|
'--text' => '#E2E8F0',
|
||||||
|
'--text-muted' => '#94A3B8',
|
||||||
|
'--text-strong' => '#F1F5F9',
|
||||||
|
'--accent' => '#7C3AED',
|
||||||
|
'--accent-text' => '#FFFFFF',
|
||||||
|
'--accent-soft' => '#2D1A5E',
|
||||||
|
'--sidebar-bg' => '#16162A',
|
||||||
|
'--sidebar-border' => '#2A2A44',
|
||||||
|
'--sidebar-text' => '#E2E8F0',
|
||||||
|
'--sidebar-text-muted' => '#94A3B8',
|
||||||
|
'--input-bg' => '#0D0D1A',
|
||||||
|
'--input-border' => '#2A2A44',
|
||||||
|
'--modal-bg' => '#1A1A2E',
|
||||||
|
'--chip' => '#2A2A44',
|
||||||
|
'--success' => '#0D2A1A',
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => '550e8400-e29b-41d4-a716-446655440012',
|
||||||
|
'name' => 'forest-grove',
|
||||||
|
'display_name' => 'Forest Grove',
|
||||||
|
'description' => 'Earthy greens and warm neutrals bring the tranquility of a woodland retreat to your workspace. A grounding, nature-inspired theme designed for focused productivity.',
|
||||||
|
'author' => 'NaturePalette',
|
||||||
|
'version' => '1.0.5',
|
||||||
|
'thumbnail_url' => null,
|
||||||
|
'download_url' => '/themes/forest-grove.css',
|
||||||
|
'price' => 0,
|
||||||
|
'is_published' => true,
|
||||||
|
'metadata' => json_encode([
|
||||||
|
'tags' => ['light', 'green', 'earthy', 'nature'],
|
||||||
|
'colors' => [
|
||||||
|
'Primary' => '#2D6A4F',
|
||||||
|
'Secondary' => '#52B788',
|
||||||
|
'Background' => '#F0F7EE',
|
||||||
|
'Surface' => '#FFFFFF',
|
||||||
|
'Text' => '#1B2E22',
|
||||||
|
'Accent' => '#B7E4C7',
|
||||||
|
],
|
||||||
|
'vars' => [
|
||||||
|
'--bg' => '#F0F7EE',
|
||||||
|
'--surface' => '#FFFFFF',
|
||||||
|
'--surface-strong' => '#FFFFFF',
|
||||||
|
'--surface-muted' => '#F5FAF4',
|
||||||
|
'--border' => '#C0DACB',
|
||||||
|
'--line' => '#A0C4B0',
|
||||||
|
'--text' => '#1B2E22',
|
||||||
|
'--text-muted' => '#527A62',
|
||||||
|
'--text-strong' => '#0D1F14',
|
||||||
|
'--accent' => '#2D6A4F',
|
||||||
|
'--accent-text' => '#FFFFFF',
|
||||||
|
'--accent-soft' => '#C0E8D4',
|
||||||
|
'--sidebar-bg' => '#FFFFFF',
|
||||||
|
'--sidebar-border' => '#C0DACB',
|
||||||
|
'--sidebar-text' => '#1B2E22',
|
||||||
|
'--sidebar-text-muted' => '#527A62',
|
||||||
|
'--input-bg' => '#FFFFFF',
|
||||||
|
'--input-border' => '#C0DACB',
|
||||||
|
'--modal-bg' => '#FFFFFF',
|
||||||
|
'--chip' => '#B8E0C8',
|
||||||
|
'--success' => '#CCF0DC',
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => '550e8400-e29b-41d4-a716-446655440013',
|
||||||
|
'name' => 'sunset-ember',
|
||||||
|
'display_name' => 'Sunset Ember',
|
||||||
|
'description' => 'Warm oranges, deep reds, and golden highlights capture the magic of a perfect sunset. This vibrant theme adds energy and warmth to every interaction.',
|
||||||
|
'author' => 'ChromaCraft',
|
||||||
|
'version' => '1.1.2',
|
||||||
|
'thumbnail_url' => null,
|
||||||
|
'download_url' => '/themes/sunset-ember.css',
|
||||||
|
'price' => 0,
|
||||||
|
'is_published' => true,
|
||||||
|
'metadata' => json_encode([
|
||||||
|
'tags' => ['warm', 'orange', 'vibrant', 'sunset'],
|
||||||
|
'colors' => [
|
||||||
|
'Primary' => '#D62828',
|
||||||
|
'Secondary' => '#F77F00',
|
||||||
|
'Background' => '#FFF5E4',
|
||||||
|
'Surface' => '#FFFFFF',
|
||||||
|
'Text' => '#2D1B00',
|
||||||
|
'Accent' => '#FCBF49',
|
||||||
|
],
|
||||||
|
'vars' => [
|
||||||
|
'--bg' => '#FFF5E4',
|
||||||
|
'--surface' => '#FFFFFF',
|
||||||
|
'--surface-strong' => '#FFFFFF',
|
||||||
|
'--surface-muted' => '#FFF8F0',
|
||||||
|
'--border' => '#F0D0A8',
|
||||||
|
'--line' => '#E0B880',
|
||||||
|
'--text' => '#2D1B00',
|
||||||
|
'--text-muted' => '#8A6040',
|
||||||
|
'--text-strong' => '#1A0A00',
|
||||||
|
'--accent' => '#D62828',
|
||||||
|
'--accent-text' => '#FFFFFF',
|
||||||
|
'--accent-soft' => '#FFE0CC',
|
||||||
|
'--sidebar-bg' => '#FFFFFF',
|
||||||
|
'--sidebar-border' => '#F0D0A8',
|
||||||
|
'--sidebar-text' => '#2D1B00',
|
||||||
|
'--sidebar-text-muted' => '#8A6040',
|
||||||
|
'--input-bg' => '#FFFFFF',
|
||||||
|
'--input-border' => '#F0D0A8',
|
||||||
|
'--modal-bg' => '#FFFFFF',
|
||||||
|
'--chip' => '#F8D8B0',
|
||||||
|
'--success' => '#DDFADC',
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => '550e8400-e29b-41d4-a716-446655440014',
|
||||||
|
'name' => 'arctic-frost',
|
||||||
|
'display_name' => 'Arctic Frost',
|
||||||
|
'description' => 'Ultra-clean whites and icy blues inspired by frozen tundras. A minimalist theme that maximises clarity and focus with crisp contrast and breathable spacing.',
|
||||||
|
'author' => 'MinimalStudio',
|
||||||
|
'version' => '3.0.0',
|
||||||
|
'thumbnail_url' => null,
|
||||||
|
'download_url' => '/themes/arctic-frost.css',
|
||||||
|
'price' => 0,
|
||||||
|
'is_published' => true,
|
||||||
|
'metadata' => json_encode([
|
||||||
|
'tags' => ['light', 'minimal', 'clean', 'ice'],
|
||||||
|
'colors' => [
|
||||||
|
'Primary' => '#2176AE',
|
||||||
|
'Secondary' => '#57C4E5',
|
||||||
|
'Background' => '#F8FBFF',
|
||||||
|
'Surface' => '#FFFFFF',
|
||||||
|
'Text' => '#1C2B3A',
|
||||||
|
'Accent' => '#A8DADC',
|
||||||
|
],
|
||||||
|
'vars' => [
|
||||||
|
'--bg' => '#F8FBFF',
|
||||||
|
'--surface' => '#FFFFFF',
|
||||||
|
'--surface-strong' => '#FFFFFF',
|
||||||
|
'--surface-muted' => '#F0F5FC',
|
||||||
|
'--border' => '#C0D4E8',
|
||||||
|
'--line' => '#A0BCDA',
|
||||||
|
'--text' => '#1C2B3A',
|
||||||
|
'--text-muted' => '#4E6478',
|
||||||
|
'--text-strong' => '#0D1B2A',
|
||||||
|
'--accent' => '#2176AE',
|
||||||
|
'--accent-text' => '#FFFFFF',
|
||||||
|
'--accent-soft' => '#CCE0F0',
|
||||||
|
'--sidebar-bg' => '#FFFFFF',
|
||||||
|
'--sidebar-border' => '#C0D4E8',
|
||||||
|
'--sidebar-text' => '#1C2B3A',
|
||||||
|
'--sidebar-text-muted' => '#4E6478',
|
||||||
|
'--input-bg' => '#FFFFFF',
|
||||||
|
'--input-border' => '#C0D4E8',
|
||||||
|
'--modal-bg' => '#FFFFFF',
|
||||||
|
'--chip' => '#B8D4E8',
|
||||||
|
'--success' => '#D4F0E4',
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => '550e8400-e29b-41d4-a716-446655440015',
|
||||||
|
'name' => 'obsidian-rose',
|
||||||
|
'display_name' => 'Obsidian Rose',
|
||||||
|
'description' => 'A sophisticated dark theme blending deep charcoal blacks with rose gold accents. Elegant and bold, this theme is built for those who want style without sacrificing readability.',
|
||||||
|
'author' => 'ChromaCraft',
|
||||||
|
'version' => '1.3.0',
|
||||||
|
'thumbnail_url' => null,
|
||||||
|
'download_url' => '/themes/obsidian-rose.css',
|
||||||
|
'price' => 0,
|
||||||
|
'is_published' => true,
|
||||||
|
'metadata' => json_encode([
|
||||||
|
'tags' => ['dark', 'elegant', 'rose', 'premium'],
|
||||||
|
'colors' => [
|
||||||
|
'Primary' => '#C9184A',
|
||||||
|
'Secondary' => '#FF4D6D',
|
||||||
|
'Background' => '#0A0A0F',
|
||||||
|
'Surface' => '#1C1C28',
|
||||||
|
'Text' => '#F1E3E4',
|
||||||
|
'Accent' => '#B5838D',
|
||||||
|
],
|
||||||
|
'vars' => [
|
||||||
|
'--bg' => '#0A0A0F',
|
||||||
|
'--surface' => '#1C1C28',
|
||||||
|
'--surface-strong' => '#242430',
|
||||||
|
'--surface-muted' => '#14141E',
|
||||||
|
'--border' => '#2A2A38',
|
||||||
|
'--line' => '#383848',
|
||||||
|
'--text' => '#F1E3E4',
|
||||||
|
'--text-muted' => '#B5939A',
|
||||||
|
'--text-strong' => '#FAF0F1',
|
||||||
|
'--accent' => '#C9184A',
|
||||||
|
'--accent-text' => '#FFFFFF',
|
||||||
|
'--accent-soft' => '#3D0A1A',
|
||||||
|
'--sidebar-bg' => '#161620',
|
||||||
|
'--sidebar-border' => '#2A2A38',
|
||||||
|
'--sidebar-text' => '#F1E3E4',
|
||||||
|
'--sidebar-text-muted' => '#B5939A',
|
||||||
|
'--input-bg' => '#0A0A0F',
|
||||||
|
'--input-border' => '#2A2A38',
|
||||||
|
'--modal-bg' => '#1C1C28',
|
||||||
|
'--chip' => '#2A2030',
|
||||||
|
'--success' => '#0A2016',
|
||||||
|
],
|
||||||
|
]),
|
||||||
'created_at' => date('Y-m-d H:i:s'),
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
],
|
],
|
||||||
|
|||||||
861
app/Views/theme_store.php
Normal file
861
app/Views/theme_store.php
Normal file
@@ -0,0 +1,861 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Theme Store</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f17;
|
||||||
|
color: #e2e8f0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ── */
|
||||||
|
header {
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||||
|
padding: 60px 24px 50px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(ellipse at 50% 0%, rgba(124,58,237,0.15) 0%, transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
font-size: clamp(2rem, 5vw, 3.2rem);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
background: linear-gradient(135deg, #a78bfa, #60a5fa, #f472b6);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
header p {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 20px;
|
||||||
|
position: relative;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.header-badge {
|
||||||
|
background: rgba(124,58,237,0.2);
|
||||||
|
border: 1px solid rgba(124,58,237,0.4);
|
||||||
|
color: #a78bfa;
|
||||||
|
padding: 4px 14px;
|
||||||
|
border-radius: 100px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.btn-upload-header {
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 100px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.btn-upload-header:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||||
|
|
||||||
|
/* ── Flash messages ── */
|
||||||
|
.flash {
|
||||||
|
max-width: 680px;
|
||||||
|
margin: 24px auto 0;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.flash-success { background: rgba(52,211,153,0.12); border: 1px solid rgba(52,211,153,0.3); color: #34d399; }
|
||||||
|
.flash-error { background: rgba(248,113,113,0.12); border: 1px solid rgba(248,113,113,0.3); color: #f87171; }
|
||||||
|
|
||||||
|
/* ── Grid ── */
|
||||||
|
.grid-wrapper {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 48px 24px 80px;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card ── */
|
||||||
|
.card {
|
||||||
|
background: #1a1a2e;
|
||||||
|
border: 1px solid rgba(255,255,255,0.07);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 20px 48px rgba(0,0,0,0.5);
|
||||||
|
border-color: rgba(167,139,250,0.3);
|
||||||
|
}
|
||||||
|
.card-preview {
|
||||||
|
height: 90px;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-preview .swatch { flex: 1; transition: flex 0.3s ease; }
|
||||||
|
.card:hover .card-preview .swatch { flex: 1.4; }
|
||||||
|
|
||||||
|
.card-body { padding: 22px; }
|
||||||
|
.card-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.card-name { font-size: 1.15rem; font-weight: 700; color: #f1f5f9; }
|
||||||
|
.card-version {
|
||||||
|
font-size: 0.75rem; color: #64748b;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
padding: 2px 8px; border-radius: 100px;
|
||||||
|
}
|
||||||
|
.card-author { font-size: 0.8rem; color: #94a3b8; margin-bottom: 10px; }
|
||||||
|
.card-desc {
|
||||||
|
font-size: 0.88rem; color: #94a3b8; line-height: 1.55;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 18px; }
|
||||||
|
.tag {
|
||||||
|
background: rgba(167,139,250,0.1);
|
||||||
|
border: 1px solid rgba(167,139,250,0.25);
|
||||||
|
color: #a78bfa;
|
||||||
|
font-size: 0.72rem; padding: 2px 9px; border-radius: 100px; font-weight: 500;
|
||||||
|
}
|
||||||
|
.card-actions { display: flex; gap: 10px; }
|
||||||
|
.btn {
|
||||||
|
flex: 1; padding: 10px 0; border-radius: 8px; border: none;
|
||||||
|
cursor: pointer; font-size: 0.85rem; font-weight: 600;
|
||||||
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.btn:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||||
|
.btn-download {
|
||||||
|
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||||
|
color: #fff; text-decoration: none;
|
||||||
|
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||||
|
}
|
||||||
|
.btn-details {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
.btn-details:hover { background: rgba(255,255,255,0.1); }
|
||||||
|
|
||||||
|
/* ── Backdrop shared ── */
|
||||||
|
.modal-backdrop {
|
||||||
|
display: none;
|
||||||
|
position: fixed; inset: 0;
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
z-index: 1000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.modal-backdrop.open { display: flex; }
|
||||||
|
|
||||||
|
/* ── Details Modal ── */
|
||||||
|
.details-modal {
|
||||||
|
background: #1a1a2e;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 95vw;
|
||||||
|
max-width: 1100px;
|
||||||
|
height: 88vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: popIn 0.22s cubic-bezier(0.34,1.56,0.64,1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@keyframes popIn {
|
||||||
|
from { opacity: 0; transform: scale(0.92) translateY(20px); }
|
||||||
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* colour stripe at top */
|
||||||
|
.modal-preview-stripe {
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.modal-preview-stripe .swatch { flex: 1; }
|
||||||
|
|
||||||
|
/* tab bar */
|
||||||
|
.modal-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
.modal-tab {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
background: none;
|
||||||
|
border-top: none; border-left: none; border-right: none;
|
||||||
|
display: flex; align-items: center; gap: 7px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
.modal-tab:hover { color: #94a3b8; }
|
||||||
|
.modal-tab.active { color: #a78bfa; border-bottom-color: #7c3aed; }
|
||||||
|
|
||||||
|
/* tab panels */
|
||||||
|
.modal-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.tab-panel {
|
||||||
|
display: none;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.tab-panel.active { display: block; }
|
||||||
|
|
||||||
|
/* details panel */
|
||||||
|
.details-panel { padding: 28px; }
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.modal-title { font-size: 1.4rem; font-weight: 800; color: #f1f5f9; }
|
||||||
|
.modal-close {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
color: #94a3b8;
|
||||||
|
border-radius: 8px; width: 34px; height: 34px;
|
||||||
|
cursor: pointer; font-size: 1.1rem;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: background 0.15s; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.modal-close:hover { background: rgba(255,255,255,0.12); color: #fff; }
|
||||||
|
.modal-meta { font-size: 0.82rem; color: #64748b; margin-bottom: 16px; }
|
||||||
|
.modal-desc { font-size: 0.92rem; color: #94a3b8; line-height: 1.65; margin-bottom: 22px; }
|
||||||
|
.section-label {
|
||||||
|
font-size: 0.75rem; font-weight: 700;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.08em;
|
||||||
|
color: #64748b; margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.colors-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
.color-chip { border-radius: 10px; overflow: hidden; border: 1px solid rgba(255,255,255,0.07); }
|
||||||
|
.color-chip-swatch { height: 48px; }
|
||||||
|
.color-chip-info { padding: 6px 8px; background: rgba(255,255,255,0.03); }
|
||||||
|
.color-chip-name { font-size: 0.7rem; color: #94a3b8; display: block; }
|
||||||
|
.color-chip-hex { font-size: 0.75rem; font-weight: 600; color: #e2e8f0; font-family: 'SF Mono', monospace; }
|
||||||
|
.modal-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 24px; }
|
||||||
|
.modal-actions { display: flex; gap: 10px; }
|
||||||
|
.btn-download-lg {
|
||||||
|
flex: 1; padding: 13px; font-size: 0.95rem; border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||||
|
color: #fff; font-weight: 700; text-decoration: none;
|
||||||
|
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
|
border: none; cursor: pointer;
|
||||||
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.btn-download-lg:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||||
|
|
||||||
|
/* preview panel */
|
||||||
|
.preview-panel {
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.preview-panel.active { display: flex; }
|
||||||
|
.preview-toolbar {
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.preview-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||||
|
.preview-url {
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.preview-iframe-wrap {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.preview-iframe-wrap iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.preview-loading {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #0f0f17;
|
||||||
|
color: #94a3b8;
|
||||||
|
gap: 14px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
border: 3px solid rgba(167,139,250,0.2);
|
||||||
|
border-top-color: #7c3aed;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
/* ── Upload Modal ── */
|
||||||
|
.upload-modal {
|
||||||
|
background: #1a1a2e;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
animation: popIn 0.22s cubic-bezier(0.34,1.56,0.64,1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.upload-header {
|
||||||
|
padding: 24px 28px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.upload-title { font-size: 1.25rem; font-weight: 700; color: #f1f5f9; }
|
||||||
|
.upload-body { padding: 0 28px 28px; }
|
||||||
|
.field { margin-bottom: 18px; }
|
||||||
|
.field label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.field input[type="text"],
|
||||||
|
.field textarea {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.field input[type="text"]:focus,
|
||||||
|
.field textarea:focus {
|
||||||
|
border-color: rgba(124,58,237,0.5);
|
||||||
|
}
|
||||||
|
.field textarea { resize: vertical; min-height: 80px; }
|
||||||
|
.file-drop {
|
||||||
|
border: 2px dashed rgba(124,58,237,0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
position: relative;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
.file-drop:hover,
|
||||||
|
.file-drop.drag-over {
|
||||||
|
border-color: rgba(124,58,237,0.7);
|
||||||
|
background: rgba(124,58,237,0.05);
|
||||||
|
}
|
||||||
|
.file-drop input[type="file"] {
|
||||||
|
position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%;
|
||||||
|
}
|
||||||
|
.file-drop-icon { font-size: 1.8rem; margin-bottom: 8px; display: block; }
|
||||||
|
.file-drop strong { color: #a78bfa; }
|
||||||
|
.file-name-preview {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #34d399;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.btn-submit {
|
||||||
|
width: 100%;
|
||||||
|
padding: 13px;
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.btn-submit:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||||
|
|
||||||
|
/* ── Footer ── */
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 24px;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar { width: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: rgba(167,139,250,0.3); border-radius: 3px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Theme Store</h1>
|
||||||
|
<p>Beautiful, ready-to-use themes for your application</p>
|
||||||
|
<div class="header-row">
|
||||||
|
<span class="header-badge"><?= count($themes) ?> free themes</span>
|
||||||
|
<button class="btn-upload-header" onclick="openUploadModal()">
|
||||||
|
<span>+</span> Upload Theme
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<?php if ($flash_success): ?>
|
||||||
|
<div style="max-width:1200px;margin:0 auto;padding:0 24px">
|
||||||
|
<div class="flash flash-success">✓ <?= esc($flash_success) ?></div>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($flash_error): ?>
|
||||||
|
<div style="max-width:1200px;margin:0 auto;padding:0 24px">
|
||||||
|
<div class="flash flash-error">⚠ <?= esc($flash_error) ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="grid-wrapper">
|
||||||
|
<div class="grid">
|
||||||
|
<?php foreach ($themes as $theme): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-preview">
|
||||||
|
<?php foreach (array_values($theme['colors']) as $hex): ?>
|
||||||
|
<div class="swatch" style="background:<?= esc($hex) ?>"></div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($theme['colors'])): ?>
|
||||||
|
<div class="swatch" style="background:linear-gradient(135deg,#7c3aed,#f472b6)"></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-name"><?= esc($theme['display_name']) ?></span>
|
||||||
|
<span class="card-version">v<?= esc($theme['version']) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-author">by <?= esc($theme['author']) ?></div>
|
||||||
|
<p class="card-desc"><?= esc($theme['description']) ?></p>
|
||||||
|
<div class="card-tags">
|
||||||
|
<?php foreach ($theme['tags'] as $tag): ?>
|
||||||
|
<span class="tag"><?= esc($tag) ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn btn-download" onclick="installTheme(<?= htmlspecialchars(json_encode([
|
||||||
|
'id' => $theme['id'],
|
||||||
|
'display_name' => $theme['display_name'],
|
||||||
|
'description' => $theme['description'],
|
||||||
|
'author' => $theme['author'],
|
||||||
|
'version' => $theme['version'],
|
||||||
|
'download_url' => $theme['download_url'],
|
||||||
|
'colors' => $theme['colors'],
|
||||||
|
'tags' => $theme['tags'],
|
||||||
|
'vars' => $theme['vars'],
|
||||||
|
]), ENT_QUOTES, 'UTF-8') ?>)">
|
||||||
|
⇓ Install Theme
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-details"
|
||||||
|
onclick="openDetailsModal(<?= htmlspecialchars(json_encode([
|
||||||
|
'id' => $theme['id'],
|
||||||
|
'display_name' => $theme['display_name'],
|
||||||
|
'description' => $theme['description'],
|
||||||
|
'author' => $theme['author'],
|
||||||
|
'version' => $theme['version'],
|
||||||
|
'download_url' => $theme['download_url'],
|
||||||
|
'colors' => $theme['colors'],
|
||||||
|
'tags' => $theme['tags'],
|
||||||
|
'vars' => $theme['vars'],
|
||||||
|
]), ENT_QUOTES, 'UTF-8') ?>)">
|
||||||
|
Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Details Modal ── -->
|
||||||
|
<div class="modal-backdrop" id="details-backdrop" onclick="closeDetailsOnBackdrop(event)">
|
||||||
|
<div class="details-modal" id="details-modal">
|
||||||
|
|
||||||
|
<div class="modal-preview-stripe" id="dm-stripe"></div>
|
||||||
|
|
||||||
|
<div class="modal-tabs">
|
||||||
|
<button class="modal-tab active" id="tab-details" onclick="switchTab('details')">
|
||||||
|
✎ Details
|
||||||
|
</button>
|
||||||
|
<button class="modal-tab" id="tab-preview" onclick="switchTab('preview')">
|
||||||
|
▶ Live Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<!-- Details panel -->
|
||||||
|
<div class="tab-panel active" id="panel-details">
|
||||||
|
<div class="details-panel">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title" id="dm-title"></span>
|
||||||
|
<button class="modal-close" onclick="closeDetailsModal()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-meta" id="dm-meta"></div>
|
||||||
|
<p class="modal-desc" id="dm-desc"></p>
|
||||||
|
|
||||||
|
<div id="dm-colors-section">
|
||||||
|
<div class="section-label">Colour Palette</div>
|
||||||
|
<div class="colors-grid" id="dm-colors"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-label" style="margin-bottom:10px">Tags</div>
|
||||||
|
<div class="modal-tags" id="dm-tags"></div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-download-lg" id="dm-download" onclick="installCurrentTheme()">
|
||||||
|
⇓ Install Theme
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview panel -->
|
||||||
|
<div class="tab-panel" id="panel-preview" style="height:100%">
|
||||||
|
<div class="preview-panel" id="preview-panel-inner">
|
||||||
|
<div class="preview-toolbar">
|
||||||
|
<div class="preview-dot" style="background:#ff5f57"></div>
|
||||||
|
<div class="preview-dot" style="background:#febc2e"></div>
|
||||||
|
<div class="preview-dot" style="background:#28c840"></div>
|
||||||
|
<div class="preview-url" id="preview-url">localhost:5173 — with theme applied</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-iframe-wrap">
|
||||||
|
<div class="preview-loading" id="preview-loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span>Loading preview…</span>
|
||||||
|
</div>
|
||||||
|
<iframe id="preview-iframe" title="Theme Preview" onload="iframeLoaded()"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- .modal-body -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Upload Modal ── -->
|
||||||
|
<div class="modal-backdrop" id="upload-backdrop" onclick="closeUploadOnBackdrop(event)">
|
||||||
|
<div class="upload-modal">
|
||||||
|
<div class="upload-header">
|
||||||
|
<span class="upload-title">Upload Custom Theme</span>
|
||||||
|
<button class="modal-close" onclick="closeUploadModal()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="upload-body">
|
||||||
|
<form method="post" action="<?= site_url('themes/upload') ?>" enctype="multipart/form-data">
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>Display Name *</label>
|
||||||
|
<input type="text" name="display_name" placeholder="e.g. Neon Sunset" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea name="description" placeholder="Describe your theme's mood and colours…"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>CSS File *</label>
|
||||||
|
<div class="file-drop" id="file-drop">
|
||||||
|
<input type="file" name="theme_css" accept=".css" required onchange="previewFileName(this)">
|
||||||
|
<span class="file-drop-icon">💾</span>
|
||||||
|
<div>Drop your <strong>.css</strong> file here<br>or <strong>click to browse</strong></div>
|
||||||
|
<div class="file-name-preview" id="file-name-preview"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-submit">↑ Upload Theme</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
© <?= date('Y') ?> Theme Store — All themes are free to use
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentThemeId = null;
|
||||||
|
let currentThemeVars = {};
|
||||||
|
let currentThemeData = null;
|
||||||
|
let previewLoaded = false;
|
||||||
|
|
||||||
|
/* ── Theme Installation ── */
|
||||||
|
function installTheme(theme) {
|
||||||
|
// Convert theme data to match Todo-App format
|
||||||
|
const themeData = {
|
||||||
|
name: theme.display_name,
|
||||||
|
description: theme.description,
|
||||||
|
preview: Object.values(theme.colors || {}).length ? Object.values(theme.colors) : ['#ffffff', '#f0f0f0', '#007acc'],
|
||||||
|
vars: theme.vars || {},
|
||||||
|
source: 'theme-store'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Installing theme:', themeData);
|
||||||
|
|
||||||
|
// Send install message to parent Todo-App
|
||||||
|
if (window.parent && window.parent !== window) {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'THEME_DOWNLOAD_REQUEST',
|
||||||
|
data: themeData
|
||||||
|
}, '*');
|
||||||
|
|
||||||
|
// Show success feedback
|
||||||
|
showInstallFeedback(theme.display_name);
|
||||||
|
} else {
|
||||||
|
// Fallback: redirect to the frontend with the theme data
|
||||||
|
const installUrl = "http://localhost:5173/#theme-install:" + encodeURIComponent(JSON.stringify(themeData));
|
||||||
|
window.location.href = installUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installCurrentTheme() {
|
||||||
|
if (currentThemeData) {
|
||||||
|
installTheme(currentThemeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showInstallFeedback(themeName) {
|
||||||
|
// Create a temporary success message
|
||||||
|
const feedback = document.createElement('div');
|
||||||
|
feedback.className = 'install-feedback';
|
||||||
|
feedback.innerHTML = `✅ "${themeName}" is being installed...`;
|
||||||
|
feedback.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 10000;
|
||||||
|
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(feedback);
|
||||||
|
|
||||||
|
// Remove after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.style.animation = 'slideOut 0.3s ease-out';
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(feedback);
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add animations
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes slideOut {
|
||||||
|
from { transform: translateX(0); opacity: 1; }
|
||||||
|
to { transform: translateX(100%); opacity: 0; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
/* ── Details modal ── */
|
||||||
|
function openDetailsModal(theme) {
|
||||||
|
currentThemeId = theme.id;
|
||||||
|
currentThemeVars = theme.vars || {};
|
||||||
|
currentThemeData = theme; // Store full theme data for installation
|
||||||
|
previewLoaded = false;
|
||||||
|
|
||||||
|
const colors = theme.colors || {};
|
||||||
|
const tags = theme.tags || [];
|
||||||
|
|
||||||
|
document.getElementById('dm-title').textContent = theme.display_name;
|
||||||
|
document.getElementById('dm-meta').textContent = 'by ' + theme.author + ' · v' + theme.version;
|
||||||
|
document.getElementById('dm-desc').textContent = theme.description;
|
||||||
|
document.getElementById('preview-url').textContent = 'localhost — ' + theme.display_name + ' applied';
|
||||||
|
|
||||||
|
// stripe
|
||||||
|
const stripe = document.getElementById('dm-stripe');
|
||||||
|
const colorValues = Object.values(colors);
|
||||||
|
stripe.innerHTML = colorValues.length
|
||||||
|
? colorValues.map(c => `<div class="swatch" style="background:${c}"></div>`).join('')
|
||||||
|
: '<div class="swatch" style="background:linear-gradient(135deg,#7c3aed,#f472b6)"></div>';
|
||||||
|
|
||||||
|
// colour chips
|
||||||
|
const colorsSection = document.getElementById('dm-colors-section');
|
||||||
|
const grid = document.getElementById('dm-colors');
|
||||||
|
if (Object.keys(colors).length) {
|
||||||
|
colorsSection.style.display = '';
|
||||||
|
grid.innerHTML = Object.entries(colors).map(([name, hex]) => `
|
||||||
|
<div class="color-chip">
|
||||||
|
<div class="color-chip-swatch" style="background:${hex}"></div>
|
||||||
|
<div class="color-chip-info">
|
||||||
|
<span class="color-chip-name">${name}</span>
|
||||||
|
<span class="color-chip-hex">${hex}</span>
|
||||||
|
</div>
|
||||||
|
</div>`).join('');
|
||||||
|
} else {
|
||||||
|
colorsSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// tags
|
||||||
|
document.getElementById('dm-tags').innerHTML =
|
||||||
|
tags.length ? tags.map(t => `<span class="tag">${t}</span>`).join('') : '<span style="color:#475569;font-size:0.82rem">No tags</span>';
|
||||||
|
|
||||||
|
// reset to details tab
|
||||||
|
switchTab('details');
|
||||||
|
|
||||||
|
document.getElementById('details-backdrop').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetailsModal() {
|
||||||
|
document.getElementById('details-backdrop').classList.remove('open');
|
||||||
|
document.getElementById('preview-iframe').src = '';
|
||||||
|
document.getElementById('preview-loading').style.display = 'flex';
|
||||||
|
currentThemeVars = {};
|
||||||
|
previewLoaded = false;
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetailsOnBackdrop(e) {
|
||||||
|
if (e.target === document.getElementById('details-backdrop')) closeDetailsModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tabs ── */
|
||||||
|
function switchTab(name) {
|
||||||
|
document.querySelectorAll('.modal-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||||
|
|
||||||
|
document.getElementById('tab-' + name).classList.add('active');
|
||||||
|
document.getElementById('panel-' + name).classList.add('active');
|
||||||
|
|
||||||
|
if (name === 'preview') {
|
||||||
|
document.getElementById('preview-panel-inner').classList.add('active');
|
||||||
|
if (!previewLoaded && currentThemeId) {
|
||||||
|
document.getElementById('preview-loading').style.display = 'flex';
|
||||||
|
const themeData = {
|
||||||
|
name: currentThemeData.display_name,
|
||||||
|
vars: currentThemeVars
|
||||||
|
};
|
||||||
|
const encoded = btoa(JSON.stringify(themeData));
|
||||||
|
document.getElementById('preview-iframe').src = 'http://localhost:5173/?__theme_preview=' + encoded + '&__preview_mode=true';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById('preview-panel-inner').classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function iframeLoaded() {
|
||||||
|
previewLoaded = true;
|
||||||
|
document.getElementById('preview-loading').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Upload modal ── */
|
||||||
|
function openUploadModal() {
|
||||||
|
document.getElementById('upload-backdrop').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
function closeUploadModal() {
|
||||||
|
document.getElementById('upload-backdrop').classList.remove('open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
function closeUploadOnBackdrop(e) {
|
||||||
|
if (e.target === document.getElementById('upload-backdrop')) closeUploadModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewFileName(input) {
|
||||||
|
const el = document.getElementById('file-name-preview');
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
el.textContent = '✓ ' + input.files[0].name;
|
||||||
|
el.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag-over styling
|
||||||
|
const drop = document.getElementById('file-drop');
|
||||||
|
drop.addEventListener('dragover', () => drop.classList.add('drag-over'));
|
||||||
|
drop.addEventListener('dragleave', () => drop.classList.remove('drag-over'));
|
||||||
|
drop.addEventListener('drop', () => drop.classList.remove('drag-over'));
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeDetailsModal(); closeUploadModal(); } });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
env.example
12
env.example
@@ -30,13 +30,13 @@
|
|||||||
# DATABASE
|
# DATABASE
|
||||||
#--------------------------------------------------------------------
|
#--------------------------------------------------------------------
|
||||||
|
|
||||||
# database.default.hostname = localhost
|
database.default.hostname = localhost
|
||||||
# database.default.database = ci4
|
database.default.database = ci4
|
||||||
# database.default.username = root
|
database.default.username = root
|
||||||
# database.default.password = root
|
database.default.password = root
|
||||||
# database.default.DBDriver = MySQLi
|
database.default.DBDriver = MySQLi
|
||||||
# database.default.DBPrefix =
|
# database.default.DBPrefix =
|
||||||
# database.default.port = 3306
|
database.default.port = 3306
|
||||||
|
|
||||||
# If you use MySQLi as tests, first update the values of Config\Database::$tests.
|
# If you use MySQLi as tests, first update the values of Config\Database::$tests.
|
||||||
# database.tests.hostname = localhost
|
# database.tests.hostname = localhost
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ Options -Indexes
|
|||||||
# such as an image or css document, if this isn't true it sends the
|
# such as an image or css document, if this isn't true it sends the
|
||||||
# request to the front controller, index.php
|
# request to the front controller, index.php
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA]
|
RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA]
|
||||||
|
|
||||||
# Ensure Authorization header is passed along
|
# Ensure Authorization header is passed along
|
||||||
|
|||||||
38
public/themes/2341342134-1441f7.css
Normal file
38
public/themes/2341342134-1441f7.css
Normal file
File diff suppressed because one or more lines are too long
16
public/themes/arctic-frost.css
Normal file
16
public/themes/arctic-frost.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Arctic Frost Theme — MinimalStudio v3.0.0 */
|
||||||
|
:root {
|
||||||
|
--color-primary: #2176AE;
|
||||||
|
--color-secondary: #57C4E5;
|
||||||
|
--color-background: #F8FBFF;
|
||||||
|
--color-surface: #FFFFFF;
|
||||||
|
--color-text: #1C2B3A;
|
||||||
|
--color-accent: #A8DADC;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||||
|
a, .link { color: var(--color-primary); }
|
||||||
|
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||||
|
.btn-primary:hover { background: var(--color-secondary); }
|
||||||
|
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 1px 6px rgba(33,118,174,0.08); padding: 20px; border: 1px solid #DDE8F0; }
|
||||||
|
.tag { background: var(--color-accent); color: var(--color-text); border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||||
37
public/themes/extract-test-theme-5fae6e.css
Normal file
37
public/themes/extract-test-theme-5fae6e.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "Extract Test Theme",
|
||||||
|
"id": "custom-1778676985034",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#f5f5f5",
|
||||||
|
"#ffffff",
|
||||||
|
"#274f69"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-strong: #ffffff;
|
||||||
|
--surface-muted: #fafafa;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--line: #b9b9b5;
|
||||||
|
--text: #1f1f1f;
|
||||||
|
--text-muted: #686866;
|
||||||
|
--text-strong: #111111;
|
||||||
|
--accent: #274f69;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--accent-soft: #d6e4ec;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
|
--sidebar-border: #d0d0d0;
|
||||||
|
--sidebar-text: #222222;
|
||||||
|
--sidebar-text-muted: #686866;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #cfcfcf;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #d8d8d8;
|
||||||
|
--success: #dff7e7;
|
||||||
|
}
|
||||||
16
public/themes/forest-grove.css
Normal file
16
public/themes/forest-grove.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Forest Grove Theme — NaturePalette v1.0.5 */
|
||||||
|
:root {
|
||||||
|
--color-primary: #2D6A4F;
|
||||||
|
--color-secondary: #52B788;
|
||||||
|
--color-background: #F0F7EE;
|
||||||
|
--color-surface: #FFFFFF;
|
||||||
|
--color-text: #1B2E22;
|
||||||
|
--color-accent: #B7E4C7;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||||
|
a, .link { color: var(--color-primary); }
|
||||||
|
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||||
|
.btn-primary:hover { background: var(--color-secondary); }
|
||||||
|
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 8px rgba(45,106,79,0.12); padding: 20px; }
|
||||||
|
.tag { background: var(--color-accent); color: var(--color-text); border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||||
38
public/themes/manual-game-update-2-e1a77a.css
Normal file
38
public/themes/manual-game-update-2-e1a77a.css
Normal file
File diff suppressed because one or more lines are too long
38
public/themes/manual-game-update-7cc79d.css
Normal file
38
public/themes/manual-game-update-7cc79d.css
Normal file
File diff suppressed because one or more lines are too long
16
public/themes/midnight-void.css
Normal file
16
public/themes/midnight-void.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Midnight Void Theme — ThemeForge v2.0.1 */
|
||||||
|
:root {
|
||||||
|
--color-primary: #7C3AED;
|
||||||
|
--color-secondary: #A78BFA;
|
||||||
|
--color-background: #0D0D1A;
|
||||||
|
--color-surface: #1A1A2E;
|
||||||
|
--color-text: #E2E8F0;
|
||||||
|
--color-accent: #F472B6;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||||
|
a, .link { color: var(--color-secondary); }
|
||||||
|
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||||
|
.btn-primary:hover { background: var(--color-accent); }
|
||||||
|
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 16px rgba(124,58,237,0.25); padding: 20px; }
|
||||||
|
.tag { background: var(--color-accent); color: #0D0D1A; border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||||
16
public/themes/obsidian-rose.css
Normal file
16
public/themes/obsidian-rose.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Obsidian Rose Theme — ChromaCraft v1.3.0 */
|
||||||
|
:root {
|
||||||
|
--color-primary: #C9184A;
|
||||||
|
--color-secondary: #FF4D6D;
|
||||||
|
--color-background: #0A0A0F;
|
||||||
|
--color-surface: #1C1C28;
|
||||||
|
--color-text: #F1E3E4;
|
||||||
|
--color-accent: #B5838D;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||||
|
a, .link { color: var(--color-secondary); }
|
||||||
|
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||||
|
.btn-primary:hover { background: var(--color-secondary); }
|
||||||
|
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 16px rgba(201,24,74,0.2); padding: 20px; }
|
||||||
|
.tag { background: var(--color-accent); color: #0A0A0F; border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||||
16
public/themes/ocean-breeze.css
Normal file
16
public/themes/ocean-breeze.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Ocean Breeze Theme — ThemeForge v1.2.0 */
|
||||||
|
:root {
|
||||||
|
--color-primary: #0077B6;
|
||||||
|
--color-secondary: #00B4D8;
|
||||||
|
--color-background: #E0F4FF;
|
||||||
|
--color-surface: #FFFFFF;
|
||||||
|
--color-text: #1A2B3C;
|
||||||
|
--color-accent: #48CAE4;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||||
|
a, .link { color: var(--color-primary); }
|
||||||
|
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||||
|
.btn-primary:hover { background: var(--color-secondary); }
|
||||||
|
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 8px rgba(0,119,182,0.1); padding: 20px; }
|
||||||
|
.tag { background: var(--color-accent); color: var(--color-text); border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||||
37
public/themes/red-extract-theme-a3aabe.css
Normal file
37
public/themes/red-extract-theme-a3aabe.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "Red Extract Theme",
|
||||||
|
"id": "custom-1778677606575",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#f5f5f5",
|
||||||
|
"#ffffff",
|
||||||
|
"#274f69"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-strong: #ffffff;
|
||||||
|
--surface-muted: #fafafa;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--line: #b9b9b5;
|
||||||
|
--text: #1f1f1f;
|
||||||
|
--text-muted: #686866;
|
||||||
|
--text-strong: #111111;
|
||||||
|
--accent: #274f69;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--accent-soft: #d6e4ec;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
|
--sidebar-border: #d0d0d0;
|
||||||
|
--sidebar-text: #222222;
|
||||||
|
--sidebar-text-muted: #686866;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #cfcfcf;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #d8d8d8;
|
||||||
|
--success: #dff7e7;
|
||||||
|
}
|
||||||
16
public/themes/sunset-ember.css
Normal file
16
public/themes/sunset-ember.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Sunset Ember Theme — ChromaCraft v1.1.2 */
|
||||||
|
:root {
|
||||||
|
--color-primary: #D62828;
|
||||||
|
--color-secondary: #F77F00;
|
||||||
|
--color-background: #FFF5E4;
|
||||||
|
--color-surface: #FFFFFF;
|
||||||
|
--color-text: #2D1B00;
|
||||||
|
--color-accent: #FCBF49;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||||
|
a, .link { color: var(--color-primary); }
|
||||||
|
.btn-primary { background: var(--color-secondary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||||
|
.btn-primary:hover { background: var(--color-primary); }
|
||||||
|
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 8px rgba(214,40,40,0.1); padding: 20px; }
|
||||||
|
.tag { background: var(--color-accent); color: var(--color-text); border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||||
37
public/themes/test-theme-103fb1.css
Normal file
37
public/themes/test-theme-103fb1.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "Test Theme",
|
||||||
|
"id": "custom-1778671955013",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#f5f5f5",
|
||||||
|
"#ffffff",
|
||||||
|
"#274f69"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-strong: #ffffff;
|
||||||
|
--surface-muted: #fafafa;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--line: #b9b9b5;
|
||||||
|
--text: #1f1f1f;
|
||||||
|
--text-muted: #686866;
|
||||||
|
--text-strong: #111111;
|
||||||
|
--accent: #274f69;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--accent-soft: #d6e4ec;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
|
--sidebar-border: #d0d0d0;
|
||||||
|
--sidebar-text: #222222;
|
||||||
|
--sidebar-text-muted: #686866;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #cfcfcf;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #d8d8d8;
|
||||||
|
--success: #dff7e7;
|
||||||
|
}
|
||||||
37
public/themes/test-theme-6fcabb.css
Normal file
37
public/themes/test-theme-6fcabb.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "Test Theme",
|
||||||
|
"id": "custom-1778671955013",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#f5f5f5",
|
||||||
|
"#ffffff",
|
||||||
|
"#274f69"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-strong: #ffffff;
|
||||||
|
--surface-muted: #fafafa;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--line: #b9b9b5;
|
||||||
|
--text: #1f1f1f;
|
||||||
|
--text-muted: #686866;
|
||||||
|
--text-strong: #111111;
|
||||||
|
--accent: #274f69;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--accent-soft: #d6e4ec;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
|
--sidebar-border: #d0d0d0;
|
||||||
|
--sidebar-text: #222222;
|
||||||
|
--sidebar-text-muted: #686866;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #cfcfcf;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #d8d8d8;
|
||||||
|
--success: #dff7e7;
|
||||||
|
}
|
||||||
37
public/themes/themestore-theme-by-came-0da6fd.css
Normal file
37
public/themes/themestore-theme-by-came-0da6fd.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "THemestore theme by Came",
|
||||||
|
"id": "custom-1778674717129",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#17acde",
|
||||||
|
"#222020",
|
||||||
|
"#00bbff"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #17acde;
|
||||||
|
--surface: #222020;
|
||||||
|
--surface-strong: #2bc582;
|
||||||
|
--surface-muted: #38363a;
|
||||||
|
--border: #000000;
|
||||||
|
--line: #ffffff;
|
||||||
|
--text: #d12e57;
|
||||||
|
--text-muted: #2f84c1;
|
||||||
|
--text-strong: #ff0000;
|
||||||
|
--accent: #00bbff;
|
||||||
|
--accent-text: #570000;
|
||||||
|
--accent-soft: #005f85;
|
||||||
|
--sidebar-bg: #004370;
|
||||||
|
--sidebar-border: #2a5070;
|
||||||
|
--sidebar-text: #d8f0ff;
|
||||||
|
--sidebar-text-muted: #7ab0d0;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #a0cce0;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #b0d8ec;
|
||||||
|
--success: #d0f0e0;
|
||||||
|
}
|
||||||
1
public/todo-preview
Symbolic link
1
public/todo-preview
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/home/came/Nextcloud/arch-work/Projects/Todo-App/dist
|
||||||
0
writable/.htaccess
Normal file → Executable file
0
writable/.htaccess
Normal file → Executable file
0
writable/cache/index.html
vendored
Normal file → Executable file
0
writable/cache/index.html
vendored
Normal file → Executable file
0
writable/debugbar/index.html
Normal file → Executable file
0
writable/debugbar/index.html
Normal file → Executable file
0
writable/index.html
Normal file → Executable file
0
writable/index.html
Normal file → Executable file
0
writable/logs/index.html
Normal file → Executable file
0
writable/logs/index.html
Normal file → Executable file
0
writable/session/index.html
Normal file → Executable file
0
writable/session/index.html
Normal file → Executable file
0
writable/uploads/index.html
Normal file → Executable file
0
writable/uploads/index.html
Normal file → Executable file
Reference in New Issue
Block a user