Skip to content

Commit dba6f4f

Browse files
committed
theme maker
1 parent 8800eef commit dba6f4f

91 files changed

Lines changed: 2907 additions & 238 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Tools;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\ThemeStoreRequest;
7+
use App\Models\Theme;
8+
use Illuminate\Http\RedirectResponse;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class ThemeController extends Controller
13+
{
14+
public function index(): Response
15+
{
16+
$themes = Theme::query()
17+
->when(auth()->check(), fn ($q) => $q->where('user_id', auth()->id()))
18+
->latest()
19+
->get();
20+
21+
return Inertia::render('tools/theme/index', [
22+
'themes' => $themes,
23+
]);
24+
}
25+
26+
public function create(): Response
27+
{
28+
return Inertia::render('tools/theme/create');
29+
}
30+
31+
public function store(ThemeStoreRequest $request): RedirectResponse
32+
{
33+
$theme = Theme::create([
34+
'name' => $request->validated()['name'],
35+
'title' => $request->validated()['title'],
36+
'description' => $request->validated()['description'] ?? null,
37+
'user_id' => auth()->id(),
38+
'css_vars' => $request->validated()['css_vars'],
39+
'font' => $request->validated()['font'] ?? null,
40+
'css' => $request->validated()['css'] ?? null,
41+
'meta' => $request->validated()['meta'] ?? null,
42+
]);
43+
44+
return to_route('tools.themes.show', $theme)
45+
->with('toast', ['type' => 'success', 'message' => 'Theme created successfully.']);
46+
}
47+
48+
public function show(Theme $theme): Response
49+
{
50+
return Inertia::render('tools/theme/show', [
51+
'theme' => [
52+
'id' => $theme->id,
53+
'name' => $theme->name,
54+
'title' => $theme->title,
55+
'description' => $theme->description,
56+
'css_vars' => $theme->css_vars,
57+
'font' => $theme->font,
58+
'css' => $theme->css,
59+
'meta' => $theme->meta,
60+
'registry_json' => $theme->toRegistryJson(),
61+
'css_output' => $theme->toCssString(),
62+
],
63+
]);
64+
}
65+
66+
public function edit(Theme $theme): Response
67+
{
68+
return Inertia::render('tools/theme/edit', [
69+
'theme' => [
70+
'id' => $theme->id,
71+
'name' => $theme->name,
72+
'title' => $theme->title,
73+
'description' => $theme->description,
74+
'css_vars' => $theme->css_vars,
75+
'font' => $theme->font,
76+
'css' => $theme->css,
77+
'meta' => $theme->meta,
78+
],
79+
]);
80+
}
81+
82+
public function update(ThemeStoreRequest $request, Theme $theme): RedirectResponse
83+
{
84+
$theme->update([
85+
'name' => $request->validated()['name'],
86+
'title' => $request->validated()['title'],
87+
'description' => $request->validated()['description'] ?? null,
88+
'css_vars' => $request->validated()['css_vars'],
89+
'font' => $request->validated()['font'] ?? null,
90+
'css' => $request->validated()['css'] ?? null,
91+
'meta' => $request->validated()['meta'] ?? null,
92+
]);
93+
94+
return to_route('tools.themes.show', $theme)
95+
->with('toast', ['type' => 'success', 'message' => 'Theme updated successfully.']);
96+
}
97+
98+
public function destroy(Theme $theme): RedirectResponse
99+
{
100+
$theme->delete();
101+
102+
return to_route('tools.themes.index')
103+
->with('toast', ['type' => 'success', 'message' => 'Theme deleted successfully.']);
104+
}
105+
106+
public function cssPreview(Theme $theme)
107+
{
108+
return response($theme->toCssString(), 200, [
109+
'Content-Type' => 'text/css',
110+
]);
111+
}
112+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace App\Http\Requests;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
7+
class ThemeStoreRequest extends FormRequest
8+
{
9+
public function authorize(): bool
10+
{
11+
return true;
12+
}
13+
14+
public function rules(): array
15+
{
16+
return [
17+
'name' => ['required', 'string', 'max:255', 'unique:themes,name'],
18+
'title' => ['required', 'string', 'max:255'],
19+
'description' => ['nullable', 'string'],
20+
'css_vars' => ['required', 'array'],
21+
'css_vars.light' => ['required', 'array'],
22+
'css_vars.dark' => ['required', 'array'],
23+
'font' => ['nullable', 'array'],
24+
'font.sans' => ['nullable', 'string', 'max:255'],
25+
'font.serif' => ['nullable', 'string', 'max:255'],
26+
'font.mono' => ['nullable', 'string', 'max:255'],
27+
'css' => ['nullable', 'array'],
28+
'meta' => ['nullable', 'array'],
29+
];
30+
}
31+
}

app/Models/Theme.php

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Database\Factories\ThemeFactory;
6+
use Illuminate\Database\Eloquent\Attributes\Fillable;
7+
use Illuminate\Database\Eloquent\Factories\HasFactory;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
10+
11+
#[Fillable([
12+
'name', 'title', 'description', 'user_id',
13+
'css_vars', 'font', 'css', 'meta',
14+
])]
15+
class Theme extends Model
16+
{
17+
/** @use HasFactory<ThemeFactory> */
18+
use HasFactory;
19+
20+
protected $casts = [
21+
'css_vars' => 'array',
22+
'font' => 'array',
23+
'css' => 'array',
24+
'meta' => 'array',
25+
];
26+
27+
public function user(): BelongsTo
28+
{
29+
return $this->belongsTo(User::class);
30+
}
31+
32+
public function toRegistryJson(): array
33+
{
34+
$cssVars = $this->css_vars ?? [];
35+
36+
return [
37+
'$schema' => 'https://ui.shadcn.com/schema/registry-item.json',
38+
'name' => $this->name,
39+
'type' => 'registry:style',
40+
'css' => $this->css ?? new \stdClass,
41+
'cssVars' => [
42+
'theme' => $this->buildThemeVars(),
43+
'light' => $cssVars['light'] ?? [],
44+
'dark' => $cssVars['dark'] ?? [],
45+
],
46+
];
47+
}
48+
49+
protected function buildThemeVars(): array
50+
{
51+
$vars = [];
52+
$light = $this->css_vars['light'] ?? [];
53+
$dark = $this->css_vars['dark'] ?? [];
54+
55+
$keys = array_unique([...array_keys($light), ...array_keys($dark)]);
56+
57+
foreach ($keys as $key) {
58+
if (str_starts_with($key, 'shadow-') || in_array($key, ['tracking-normal', 'spacing', 'letter-spacing'])) {
59+
$vars[$key] = $light[$key] ?? ($dark[$key] ?? '');
60+
}
61+
}
62+
63+
return $vars;
64+
}
65+
66+
public function toCssString(): string
67+
{
68+
$cssVars = $this->css_vars ?? [];
69+
$light = $cssVars['light'] ?? [];
70+
$dark = $cssVars['dark'] ?? [];
71+
$font = $this->font ?? [];
72+
73+
$lines = [];
74+
75+
$lines[] = '@import "tailwindcss";';
76+
$lines[] = '';
77+
$lines[] = '@custom-variant dark (&:is(.dark *));';
78+
$lines[] = '';
79+
80+
// :root
81+
$lines[] = ':root {';
82+
foreach ($light as $key => $value) {
83+
$lines[] = " --{$key}: {$value};";
84+
}
85+
if (! empty($font['sans'])) {
86+
$lines[] = " --font-sans: {$font['sans']};";
87+
}
88+
if (! empty($font['serif'])) {
89+
$lines[] = " --font-serif: {$font['serif']};";
90+
}
91+
if (! empty($font['mono'])) {
92+
$lines[] = " --font-mono: {$font['mono']};";
93+
}
94+
$lines[] = '}';
95+
$lines[] = '';
96+
97+
// .dark
98+
$lines[] = '.dark {';
99+
foreach ($dark as $key => $value) {
100+
$lines[] = " --{$key}: {$value};";
101+
}
102+
if (! empty($font['sans'])) {
103+
$lines[] = " --font-sans: {$font['sans']};";
104+
}
105+
if (! empty($font['serif'])) {
106+
$lines[] = " --font-serif: {$font['serif']};";
107+
}
108+
if (! empty($font['mono'])) {
109+
$lines[] = " --font-mono: {$font['mono']};";
110+
}
111+
$lines[] = '};';
112+
$lines[] = '';
113+
114+
// @theme inline
115+
$lines[] = '@theme inline {';
116+
117+
$colorMappings = [
118+
'background' => 'background',
119+
'foreground' => 'foreground',
120+
'card' => 'card',
121+
'card-foreground' => 'card-foreground',
122+
'popover' => 'popover',
123+
'popover-foreground' => 'popover-foreground',
124+
'primary' => 'primary',
125+
'primary-foreground' => 'primary-foreground',
126+
'secondary' => 'secondary',
127+
'secondary-foreground' => 'secondary-foreground',
128+
'muted' => 'muted',
129+
'muted-foreground' => 'muted-foreground',
130+
'accent' => 'accent',
131+
'accent-foreground' => 'accent-foreground',
132+
'destructive' => 'destructive',
133+
'destructive-foreground' => 'destructive-foreground',
134+
'border' => 'border',
135+
'input' => 'input',
136+
'ring' => 'ring',
137+
'chart-1' => 'chart-1',
138+
'chart-2' => 'chart-2',
139+
'chart-3' => 'chart-3',
140+
'chart-4' => 'chart-4',
141+
'chart-5' => 'chart-5',
142+
'sidebar' => 'sidebar',
143+
'sidebar-foreground' => 'sidebar-foreground',
144+
'sidebar-primary' => 'sidebar-primary',
145+
'sidebar-primary-foreground' => 'sidebar-primary-foreground',
146+
'sidebar-accent' => 'sidebar-accent',
147+
'sidebar-accent-foreground' => 'sidebar-accent-foreground',
148+
'sidebar-border' => 'sidebar-border',
149+
'sidebar-ring' => 'sidebar-ring',
150+
];
151+
152+
foreach ($colorMappings as $var => $name) {
153+
if (isset($light[$var])) {
154+
$lines[] = " --color-{$name}: var(--{$var});";
155+
}
156+
}
157+
158+
if (! empty($font['sans'])) {
159+
$lines[] = ' --font-sans: var(--font-sans);';
160+
}
161+
if (! empty($font['serif'])) {
162+
$lines[] = ' --font-serif: var(--font-serif);';
163+
}
164+
if (! empty($font['mono'])) {
165+
$lines[] = ' --font-mono: var(--font-mono);';
166+
}
167+
168+
$radius = $light['radius'] ?? '0.5rem';
169+
$lines[] = ' --radius-sm: calc(var(--radius) - 4px);';
170+
$lines[] = ' --radius-md: calc(var(--radius) - 2px);';
171+
$lines[] = ' --radius-lg: var(--radius);';
172+
$lines[] = ' --radius-xl: calc(var(--radius) + 4px);';
173+
174+
$shadowVars = ['shadow-2xs', 'shadow-xs', 'shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl', 'shadow-2xl'];
175+
foreach ($shadowVars as $shadow) {
176+
if (isset($light[$shadow])) {
177+
$lines[] = " --{$shadow}: var(--{$shadow});";
178+
}
179+
}
180+
181+
$lines[] = '}';
182+
$lines[] = '';
183+
184+
// @layer base
185+
if (! empty($this->css) && ! empty($this->css['@layer base'])) {
186+
$lines[] = '@layer base {';
187+
foreach ($this->css['@layer base'] as $selector => $props) {
188+
$lines[] = " {$selector} {";
189+
if (is_array($props)) {
190+
foreach ($props as $prop => $value) {
191+
$lines[] = " {$prop}: {$value};";
192+
}
193+
}
194+
$lines[] = ' }';
195+
}
196+
$lines[] = '}';
197+
}
198+
199+
return implode("\n", $lines);
200+
}
201+
}

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Database\Factories;
4+
5+
use App\Models\Theme;
6+
use Illuminate\Database\Eloquent\Factories\Factory;
7+
8+
/**
9+
* @extends Factory<Theme>
10+
*/
11+
class ThemeFactory extends Factory
12+
{
13+
/**
14+
* Define the model's default state.
15+
*
16+
* @return array<string, mixed>
17+
*/
18+
public function definition(): array
19+
{
20+
return [
21+
//
22+
];
23+
}
24+
}

0 commit comments

Comments
 (0)