Skip to content

Commit 66e1b15

Browse files
feat: enhance theme creator with TweakCN feel and metadata management
- Integrated `CardsPreview` for comprehensive shadcn-style component previews. - Added "Info" tab to `ControlsPanel` for editing theme title, slug, description, and tags. - Updated `useThemeStore` to manage theme metadata. - Added "Save" functionality to persist themes to the database. - Improved layout with real-time title editing and a minimalist side-by-side design. Co-authored-by: claudemyburgh <6057076+claudemyburgh@users.noreply.github.com>
1 parent 2966f79 commit 66e1b15

4 files changed

Lines changed: 104 additions & 91 deletions

File tree

resources/js/components/themes/controls-panel.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
2+
import { useThemeStore } from '@/lib/theme/store';
3+
import { usePage } from '@inertiajs/react';
24
import { ColorControls } from './color-controls';
35
import { RadiusControls } from './raduis-controles';
6+
import ThemeInfo from './theme-info';
47
import { TypographyControls } from './typography-controls';
58

69
export function ControlsPanel() {
10+
const { title, name, description, tags, setInfo } = useThemeStore();
11+
const { availableTags = [] } = usePage<{ availableTags: string[] }>().props;
12+
713
return (
814
<Tabs defaultValue="colors" className="flex h-full flex-col">
9-
<TabsList className="m-3 grid grid-cols-3">
15+
<TabsList className="m-3 grid grid-cols-4">
1016
<TabsTrigger value="colors">Colors</TabsTrigger>
1117
<TabsTrigger value="radius">Radius</TabsTrigger>
1218
<TabsTrigger value="type">Type</TabsTrigger>
19+
<TabsTrigger value="info">Info</TabsTrigger>
1320
</TabsList>
1421
<div className="flex-1 overflow-y-auto px-4 pb-6">
1522
<TabsContent value="colors" className="mt-0">
@@ -21,6 +28,13 @@ export function ControlsPanel() {
2128
<TabsContent value="type" className="mt-0">
2229
<TypographyControls />
2330
</TabsContent>
31+
<TabsContent value="info" className="mt-0">
32+
<ThemeInfo
33+
theme={{ title, name, description, tags }}
34+
availableTags={availableTags}
35+
onChange={setInfo}
36+
/>
37+
</TabsContent>
2438
</div>
2539
</Tabs>
2640
);

resources/js/components/themes/theme-preview.tsx

Lines changed: 23 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { useThemeStore } from '@/lib/theme/store';
22
import { tokenValueToCss } from '@/lib/theme/color';
3-
import { Badge } from '@/components/ui/badge';
43
import { Button } from '@/components/ui/button';
5-
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
6-
import { Input } from '@/components/ui/input';
7-
import { Label } from '@/components/ui/label';
84
import { Sun, Moon } from 'lucide-react';
95
import { useMemo, useState } from 'react';
6+
import CardsPreview from '@/components/preview/cards-preview';
107

118
const COLOR_TOKENS = [
129
'background', 'foreground',
@@ -68,93 +65,38 @@ export function ThemePreview() {
6865

6966
return (
7067
<div className="flex h-full flex-col">
71-
<div className="flex items-center justify-between border-b px-4 py-2" style={{ borderColor: 'var(--color-border)' }}>
72-
<span className="text-sm font-medium text-muted-foreground">Preview</span>
68+
<div
69+
className="flex items-center justify-between border-b px-4 py-2"
70+
style={{ borderColor: 'var(--color-border)' }}
71+
>
72+
<span className="text-sm font-medium text-muted-foreground">
73+
Preview
74+
</span>
7375
<Button variant="outline" size="sm" onClick={toggleMode}>
7476
{previewMode === 'light' ? (
75-
<><Moon className="mr-1 size-3.5" /> Dark</>
77+
<>
78+
<Moon className="mr-1 size-3.5" /> Dark
79+
</>
7680
) : (
77-
<><Sun className="mr-1 size-3.5" /> Light</>
81+
<>
82+
<Sun className="mr-1 size-3.5" /> Light
83+
</>
7884
)}
7985
</Button>
8086
</div>
8187

8288
<div
8389
className="flex-1 overflow-y-auto"
84-
style={{ background: bgCss, color: fgCss } as React.CSSProperties}
90+
style={
91+
{
92+
background: bgCss,
93+
color: fgCss,
94+
...cssVars,
95+
} as React.CSSProperties
96+
}
8597
>
86-
<div className="min-h-full p-6" style={cssVars as React.CSSProperties}>
87-
<p
88-
className="text-2xl font-bold"
89-
style={{ fontFamily: `"${fonts.serif}"` }}
90-
>
91-
Theme Preview
92-
</p>
93-
<p className="mt-1 mb-6 text-sm" style={{ color: 'var(--color-muted-foreground)' }}>
94-
Sample UI components using your current palette.
95-
</p>
96-
97-
<div className="flex flex-wrap gap-2">
98-
<Button>Primary</Button>
99-
<Button variant="secondary">Secondary</Button>
100-
<Button variant="outline">Outline</Button>
101-
<Button variant="ghost">Ghost</Button>
102-
<Button variant="destructive">Destructive</Button>
103-
</div>
104-
105-
<Card className="mt-6 max-w-sm">
106-
<CardHeader>
107-
<CardTitle>Example Card</CardTitle>
108-
</CardHeader>
109-
<CardContent>
110-
<p className="text-sm" style={{ color: 'var(--color-muted-foreground)' }}>
111-
This card demonstrates how your theme appears on standard
112-
shadcn/ui components.
113-
</p>
114-
</CardContent>
115-
<CardFooter className="flex justify-between">
116-
<Button variant="outline">Cancel</Button>
117-
<Button>Save</Button>
118-
</CardFooter>
119-
</Card>
120-
121-
<Card className="mt-4 max-w-sm">
122-
<CardHeader>
123-
<CardTitle>Form Example</CardTitle>
124-
</CardHeader>
125-
<CardContent className="space-y-3">
126-
<div className="space-y-1">
127-
<Label htmlFor="pv-email">Email</Label>
128-
<Input id="pv-email" placeholder="you@example.com" />
129-
</div>
130-
<div className="space-y-1">
131-
<Label htmlFor="pv-pw">Password</Label>
132-
<Input id="pv-pw" type="password" placeholder="••••••••" />
133-
</div>
134-
</CardContent>
135-
</Card>
136-
137-
<div className="mt-6 flex flex-wrap gap-2">
138-
<Badge>Default</Badge>
139-
<Badge variant="secondary">Secondary</Badge>
140-
<Badge variant="outline">Outline</Badge>
141-
<Badge variant="destructive">Destructive</Badge>
142-
</div>
143-
144-
<div
145-
className="mt-6 rounded-lg border p-4 space-y-1"
146-
style={{ borderColor: 'var(--color-border)' }}
147-
>
148-
<p className="font-mono text-xs" style={{ color: 'var(--color-muted-foreground)' }}>
149-
SYSTEM — font-mono ({fonts.mono})
150-
</p>
151-
<p style={{ fontFamily: `"${fonts.serif}"` }}>
152-
The quick brown fox jumps over the lazy dog — serif
153-
</p>
154-
<p style={{ fontFamily: `"${fonts.sans}"` }}>
155-
The quick brown fox jumps over the lazy dog — sans
156-
</p>
157-
</div>
98+
<div className="min-h-full p-6">
99+
<CardsPreview />
158100
</div>
159101
</div>
160102
</div>

resources/js/pages/themes/create.tsx

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1-
import { Head, Link } from '@inertiajs/react';
2-
import { ArrowLeft, Pencil } from 'lucide-react';
1+
import { Head, Link, router } from '@inertiajs/react';
2+
import { ArrowLeft, Check, Pencil, Save } from 'lucide-react';
33
import { useCallback, useEffect, useRef, useState } from 'react';
44
import { ControlsPanel } from '@/components/themes/controls-panel';
55
import { ExportDialog } from '@/components/themes/export-dialog';
66
import { ThemePreview } from '@/components/themes/theme-preview';
7+
import { Button } from '@/components/ui/button';
78
import { Separator } from '@/components/ui/separator';
89
import ThemeCreatorLayout from '@/layouts/theme-creator-layout';
910
import { cn } from '@/lib/utils';
10-
import { index } from '@/routes/themes';
11+
import { index, store } from '@/routes/themes';
1112
import { useThemeCreatorStore } from '@/store/theme-creator';
13+
import { useThemeStore } from '@/lib/theme/store';
14+
import { toast } from 'sonner';
1215

1316
const MOBILE_TABS = ['Editor', 'Preview'] as const;
1417
const MIN_SIDEBAR = 330;
1518
const MAX_SIDEBAR_PCT = 0.5;
1619

1720
export default function ThemeCreate() {
1821
const [mobileTab, setMobileTab] = useState<string>('Editor');
19-
const [themeName, setThemeName] = useState('My New Theme');
22+
const { title, setTitle, name, description, tags, light, dark, radius, fonts } = useThemeStore();
2023
const [isEditing, setIsEditing] = useState(false);
2124
const inputRef = useRef<HTMLInputElement>(null);
2225

@@ -60,9 +63,38 @@ export default function ThemeCreate() {
6063
};
6164
}, []);
6265

66+
const handleSave = () => {
67+
const themeData = {
68+
name,
69+
title,
70+
description,
71+
tags,
72+
cssVars: {
73+
light,
74+
dark,
75+
},
76+
font: {
77+
family: fonts.sans,
78+
serif: fonts.serif,
79+
mono: fonts.mono,
80+
},
81+
radius,
82+
};
83+
84+
router.post(store().url, { theme_data: themeData }, {
85+
onSuccess: () => {
86+
toast.success('Theme saved successfully!');
87+
},
88+
onError: (errors) => {
89+
const message = Object.values(errors).flat().join(' ');
90+
toast.error(message || 'Failed to save theme.');
91+
}
92+
});
93+
};
94+
6395
return (
6496
<>
65-
<Head title="Create Theme" />
97+
<Head title={`Create Theme - ${title}`} />
6698
<nav className="flex h-14 shrink-0 items-center gap-3 border-b px-4">
6799
<Link
68100
href={index().url}
@@ -83,8 +115,8 @@ export default function ThemeCreate() {
83115
{isEditing ? (
84116
<input
85117
ref={inputRef}
86-
value={themeName}
87-
onChange={(e) => setThemeName(e.target.value)}
118+
value={title}
119+
onChange={(e) => setTitle(e.target.value)}
88120
onBlur={() => setIsEditing(false)}
89121
onKeyDown={(e) => {
90122
if (e.key === 'Enter') {
@@ -97,7 +129,7 @@ export default function ThemeCreate() {
97129
) : (
98130
<>
99131
<span className="text-sm font-medium">
100-
{themeName}
132+
{title}
101133
</span>
102134
<button
103135
onClick={() => setIsEditing(true)}
@@ -109,7 +141,10 @@ export default function ThemeCreate() {
109141
)}
110142
</div>
111143

112-
<div className="ml-auto">
144+
<div className="ml-auto flex items-center gap-2">
145+
<Button variant="outline" size="sm" onClick={handleSave}>
146+
<Save className="mr-2 size-4" /> Save
147+
</Button>
113148
<ExportDialog />
114149
</div>
115150
</nav>

resources/js/store/theme.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import { DEFAULT_LIGHT, DEFAULT_DARK, type TokenMap } from '@/lib/theme/defaults
55
export type ThemeFonts = { sans: string; serif: string; mono: string };
66

77
export interface ThemeState {
8+
title: string;
9+
name: string;
10+
description: string;
11+
tags: string[];
12+
813
light: TokenMap;
914
dark: TokenMap;
1015
radius: number;
@@ -14,6 +19,12 @@ export interface ThemeState {
1419
lineHeight: number;
1520
letterSpacing: number;
1621

22+
setTitle: (v: string) => void;
23+
setName: (v: string) => void;
24+
setDescription: (v: string) => void;
25+
setTags: (v: string[]) => void;
26+
setInfo: (info: Partial<Pick<ThemeState, 'title' | 'name' | 'description' | 'tags'>>) => void;
27+
1728
setToken: (mode: 'light' | 'dark', token: string, value: string) => void;
1829
setManyTokens: (mode: 'light' | 'dark', map: Record<string, string>) => void;
1930
setRadius: (v: number) => void;
@@ -27,6 +38,11 @@ export interface ThemeState {
2738
export const useThemeStore = create<ThemeState>()(
2839
persist(
2940
(set) => ({
41+
title: 'My New Theme',
42+
name: 'my-new-theme',
43+
description: '',
44+
tags: [],
45+
3046
light: { ...DEFAULT_LIGHT },
3147
dark: { ...DEFAULT_DARK },
3248
radius: 0.625,
@@ -36,6 +52,12 @@ export const useThemeStore = create<ThemeState>()(
3652
lineHeight: 1.5,
3753
letterSpacing: 0,
3854

55+
setTitle: (v) => set({ title: v }),
56+
setName: (v) => set({ name: v }),
57+
setDescription: (v) => set({ description: v }),
58+
setTags: (v) => set({ tags: v }),
59+
setInfo: (info) => set((s) => ({ ...s, ...info })),
60+
3961
setToken: (mode, token, value) =>
4062
set((s) => ({
4163
[mode]: { ...s[mode], [token]: value },

0 commit comments

Comments
 (0)