From 4611302886b661334ae95fadde1d0f245135dc02 Mon Sep 17 00:00:00 2001 From: designbycode Date: Thu, 14 May 2026 21:44:52 +0200 Subject: [PATCH] add theme installer --- app/Concerns/HasTheme.php | 12 +- app/Http/Controllers/RegistriesController.php | 2 +- app/Http/Controllers/ThemesController.php | 82 +++++- app/Models/Theme.php | 5 +- app/Observers/ThemeObserver.php | 16 ++ .../2026_05_08_174618_create_themes_table.php | 35 +-- resources/js/pages/themes/create.tsx | 11 +- resources/js/pages/themes/show.tsx | 10 +- resources/js/routes/debugbar/cache/index.ts | 90 ------ resources/js/routes/debugbar/index.ts | 265 ------------------ resources/js/routes/debugbar/queries/index.ts | 61 ---- resources/js/routes/index.ts | 14 +- resources/js/routes/themes/index.ts | 163 ++++++++++- routes/web.php | 6 +- 14 files changed, 280 insertions(+), 492 deletions(-) create mode 100644 app/Observers/ThemeObserver.php delete mode 100644 resources/js/routes/debugbar/cache/index.ts delete mode 100644 resources/js/routes/debugbar/index.ts delete mode 100644 resources/js/routes/debugbar/queries/index.ts diff --git a/app/Concerns/HasTheme.php b/app/Concerns/HasTheme.php index d21da34..041be21 100644 --- a/app/Concerns/HasTheme.php +++ b/app/Concerns/HasTheme.php @@ -41,7 +41,7 @@ trait HasTheme public static function fromCss( string $css, string $name, - string $type = 'registry:app' + string $type = 'registry:theme' ): static { static::assertValidType($type); @@ -82,7 +82,7 @@ public static function fromRegistry(array $data): static $instance = new static; $instance->name = $data['name']; - $instance->type = 'registry:app'; + $instance->type = $data['type'] ?? 'registry:theme'; $instance->title = $data['title'] ?? null; $instance->description = $data['description'] ?? null; $instance->author = $data['author'] ?? null; @@ -141,7 +141,7 @@ public function toRegistry(): array $registry = [ '$schema' => 'https://ui.shadcn.com/schema/registry-item.json', 'name' => $this->name, - 'type' => 'registry:app', + 'type' => 'registry:theme', 'title' => $this->title, 'description' => $this->description, 'author' => $this->author, @@ -273,9 +273,9 @@ public function toCss(): string public static function assertValidType(string $type): void { - if (! in_array($type, ['registry:app'], true)) { + if (! in_array($type, ['registry:theme'], true)) { throw new InvalidArgumentException( - "Invalid registry type \"{$type}\". Allowed: registry:app" + "Invalid registry type \"{$type}\". Allowed: registry:theme" ); } } @@ -342,7 +342,7 @@ public function isStyle(): bool public function isTheme(): bool { - return $this->type === 'registry:app'; + return $this->type === 'registry:theme'; } public function isComponent(): bool diff --git a/app/Http/Controllers/RegistriesController.php b/app/Http/Controllers/RegistriesController.php index 6f895cf..b938344 100644 --- a/app/Http/Controllers/RegistriesController.php +++ b/app/Http/Controllers/RegistriesController.php @@ -15,7 +15,7 @@ public function show(string $type, string $name): JsonResponse $model = match ($type) { 'fonts' => Font::class, 'animate-css' => Animate::class, - 'themes', 'app' => Theme::class, + 'themes', 'theme' => Theme::class, default => Registry::class, }; diff --git a/app/Http/Controllers/ThemesController.php b/app/Http/Controllers/ThemesController.php index d8ec5d2..2c32697 100644 --- a/app/Http/Controllers/ThemesController.php +++ b/app/Http/Controllers/ThemesController.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Str; use Inertia\Inertia; class ThemesController extends Controller @@ -29,11 +30,88 @@ public function store(Request $request) $data = $response->json(); - if (empty($data) || ! isset($data['name'])) { + if (empty($data) || ! is_array($data)) { return back()->withErrors(['url' => 'Invalid registry JSON format.']); } - // Check if theme already exists + $errors = []; + + if (! is_string($data['name'] ?? null) || $data['name'] === '') { + $errors[] = 'The registry must contain a non-empty "name" field.'; + } elseif (! preg_match('/^[a-z0-9\-]+$/i', $data['name'])) { + $errors[] = 'The theme name must only contain letters, numbers, and hyphens.'; + } + + if (isset($data['cssVars'])) { + if (! is_array($data['cssVars'])) { + $errors[] = '"cssVars" must be an object.'; + } else { + if (isset($data['cssVars']['light']) && ! is_array($data['cssVars']['light'])) { + $errors[] = '"cssVars.light" must be an object.'; + } + + if (isset($data['cssVars']['dark']) && ! is_array($data['cssVars']['dark'])) { + $errors[] = '"cssVars.dark" must be an object.'; + } + } + } + + if (isset($data['files'])) { + if (! is_array($data['files'])) { + $errors[] = '"files" must be an array.'; + } else { + $fileErrors = Theme::validateFiles($data['files']); + foreach ($fileErrors as $error) { + $errors[] = $error; + } + } + } + + if (isset($data['font'])) { + if (! is_array($data['font'])) { + $errors[] = '"font" must be an object.'; + } else { + foreach (['family', 'provider', 'import', 'variable', 'selector', 'dependency'] as $field) { + if (isset($data['font'][$field]) && ! is_string($data['font'][$field])) { + $errors[] = "\"font.{$field}\" must be a string."; + } + } + + foreach (['weight', 'subsets'] as $field) { + if (isset($data['font'][$field]) && ! is_array($data['font'][$field])) { + $errors[] = "\"font.{$field}\" must be an array."; + } + } + } + } + + if (isset($data['categories'])) { + if (! is_array($data['categories'])) { + $errors[] = '"categories" must be an array.'; + } else { + foreach ($data['categories'] as $i => $category) { + if (! is_string($category)) { + $errors[] = "\"categories.{$i}\" must be a string."; + } + } + } + } + + if (! empty($errors)) { + return back()->withErrors(['url' => implode(' ', $errors)]); + } + + $data['name'] = Str::kebab($data['name']); + $data['type'] = 'registry:theme'; + + if (! ($data['author'] ?? null)) { + $host = Str::of(parse_url($request->url, PHP_URL_HOST)) + ->replaceFirst('www.', '') + ->before('.') + ->toString(); + $data['author'] = $host; + } + if (Theme::where('name', $data['name'])->exists()) { return back()->withErrors(['url' => "A theme named [{$data['name']}] already exists."]); } diff --git a/app/Models/Theme.php b/app/Models/Theme.php index 41052c8..1c1a1f3 100644 --- a/app/Models/Theme.php +++ b/app/Models/Theme.php @@ -3,13 +3,15 @@ namespace App\Models; use App\Concerns\HasTheme; +use App\Observers\ThemeObserver; use Illuminate\Database\Eloquent\Attributes\Fillable; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; #[Fillable([ - 'name', 'title', 'description', 'author', + 'name', 'type', 'title', 'description', 'author', 'dependencies', 'devDependencies', 'registryDependencies', 'files', 'css', 'css_base', 'tailwind', @@ -21,6 +23,7 @@ 'extends', 'style', 'icon_library', 'base_color', 'theme', ])] +#[ObservedBy(ThemeObserver::class)] class Theme extends Model { use HasTheme, SoftDeletes; diff --git a/app/Observers/ThemeObserver.php b/app/Observers/ThemeObserver.php new file mode 100644 index 0000000..43fba32 --- /dev/null +++ b/app/Observers/ThemeObserver.php @@ -0,0 +1,16 @@ +title) { + $theme->title = Str::headline($theme->name); + } + } +} diff --git a/database/migrations/2026_05_08_174618_create_themes_table.php b/database/migrations/2026_05_08_174618_create_themes_table.php index 627db29..08a5438 100644 --- a/database/migrations/2026_05_08_174618_create_themes_table.php +++ b/database/migrations/2026_05_08_174618_create_themes_table.php @@ -17,8 +17,8 @@ public function up(): void $table->string('name')->unique(); $table->string('title')->nullable(); $table->text('description')->nullable(); - $table->string('author')->nullable(); - + $table->string('author')->default('designbycode')->nullable(); + $table->string('type')->nullable()->default('registry:theme'); $table->json('dependencies')->nullable(); $table->json('devDependencies')->nullable(); $table->json('registryDependencies')->nullable(); @@ -50,7 +50,7 @@ public function up(): void $table->string('style')->nullable(); $table->string('icon_library')->nullable(); $table->string('base_color')->nullable(); - $table->json('app')->nullable(); + $table->json('theme')->nullable(); $table->timestamps(); $table->softDeletes(); @@ -58,35 +58,6 @@ public function up(): void $table->index('created_at'); }); - DB::statement(' - INSERT INTO themes ( - name, title, description, author, - dependencies, devDependencies, registryDependencies, - files, css, css_base, - vars_theme, vars_light, vars_dark, - font_family, font_mono, font_serif, - font_provider, font_import, font_variable, - font_weight, font_subsets, font_selector, font_dependency, - tailwind, meta, docs, categories, - extends, style, icon_library, base_color, app, - created_at, updated_at - ) - SELECT - name, title, description, author, - dependencies, devDependencies, registryDependencies, - files, css, css_base, - vars_theme, vars_light, vars_dark, - font_family, font_mono, font_serif, - font_provider, font_import, font_variable, - font_weight, font_subsets, font_selector, font_dependency, - tailwind, meta, docs, categories, - extends, style, icon_library, base_color, app, - created_at, updated_at - FROM registries - WHERE type = \'registry:app\' - '); - - DB::table('registries')->where('type', 'registry:app')->delete(); } public function down(): void diff --git a/resources/js/pages/themes/create.tsx b/resources/js/pages/themes/create.tsx index 23c19f3..4e71077 100644 --- a/resources/js/pages/themes/create.tsx +++ b/resources/js/pages/themes/create.tsx @@ -34,7 +34,6 @@ export default function ThemeCreate() { @@ -48,7 +47,9 @@ export default function ThemeCreate() {
- + - +