Skip to content

Commit d594348

Browse files
committed
feat(style-controls): add style preset controls and integrate with component preview
refactor(component-selector): remove category filter and simplify component selection fix(keyboard-shortcuts): improve key matching robustness refactor(component-store): add style preset state management style(globals): add utility classes for style presets
1 parent 692377f commit d594348

File tree

13 files changed

+428
-55
lines changed

13 files changed

+428
-55
lines changed

.vscode/settings.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
// Suppress unknown at-rule warnings from VS Code's CSS language server
3+
"css.lint.unknownAtRules": "ignore",
4+
"scss.lint.unknownAtRules": "ignore",
5+
"less.lint.unknownAtRules": "ignore",
6+
7+
// Treat CSS files in this project as PostCSS for better compatibility with Tailwind v4 directives
8+
"files.associations": {
9+
"**/src/app/**/*.css": "postcss",
10+
"**/globals.css": "postcss"
11+
}
12+
}

src/app/globals.css

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,81 @@
120120
@apply bg-background text-foreground;
121121
}
122122
}
123+
124+
/* Custom utility classes to guarantee runtime styling from the attribute panel */
125+
@layer utilities {
126+
/* Background colors */
127+
.bg-blue { background-color: #3b82f6; }
128+
.bg-red { background-color: #ef4444; }
129+
.bg-green { background-color: #22c55e; }
130+
.bg-yellow { background-color: #f59e0b; }
131+
.bg-purple { background-color: #a855f7; }
132+
.bg-pink { background-color: #ec4899; }
133+
.bg-gray { background-color: #6b7280; }
134+
135+
/* Text colors */
136+
.text-white { color: #ffffff; }
137+
.text-black { color: #000000; }
138+
.text-blue { color: #3b82f6; }
139+
.text-red { color: #ef4444; }
140+
.text-green { color: #22c55e; }
141+
.text-gray { color: #6b7280; }
142+
143+
/* Borders */
144+
.border { border-width: 1px; border-style: solid; border-color: var(--border); }
145+
.border-2 { border-width: 2px; border-style: solid; border-color: var(--border); }
146+
.border-4 { border-width: 4px; border-style: solid; border-color: var(--border); }
147+
.border-none { border: none; }
148+
.border-blue { border-color: #3b82f6; }
149+
.border-red { border-color: #ef4444; }
150+
.border-green { border-color: #22c55e; }
151+
.border-gray { border-color: #6b7280; }
152+
153+
/* Padding and Margin */
154+
.p-0 { padding: 0px; }
155+
.p-2 { padding: 0.5rem; }
156+
.p-4 { padding: 1rem; }
157+
.p-6 { padding: 1.5rem; }
158+
.m-0 { margin: 0px; }
159+
.m-2 { margin: 0.5rem; }
160+
.m-4 { margin: 1rem; }
161+
.m-6 { margin: 1.5rem; }
162+
163+
/* Font size and weight */
164+
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
165+
.text-base { font-size: 1rem; line-height: 1.5rem; }
166+
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
167+
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
168+
.font-normal { font-weight: 400; }
169+
.font-medium { font-weight: 500; }
170+
.font-semibold { font-weight: 600; }
171+
.font-bold { font-weight: 700; }
172+
173+
/* Radius */
174+
.rounded-none { border-radius: 0px; }
175+
.rounded-sm { border-radius: var(--radius-sm); }
176+
.rounded-md { border-radius: var(--radius-md); }
177+
.rounded-lg { border-radius: var(--radius-lg); }
178+
.rounded-full { border-radius: 9999px; }
179+
180+
/* Hover effects */
181+
.hover-brighten { transition: filter 150ms ease; }
182+
.hover-brighten:hover { filter: brightness(1.1); }
183+
.hover-shadow { transition: box-shadow 150ms ease; }
184+
.hover-shadow:hover { box-shadow: 0 8px 20px rgba(0,0,0,0.12); }
185+
186+
/* Shadows */
187+
.shadow-sm { box-shadow: 0 1px 2px rgba(0,0,0,0.06); }
188+
.shadow { box-shadow: 0 4px 12px rgba(0,0,0,0.12); }
189+
.shadow-lg { box-shadow: 0 12px 32px rgba(0,0,0,0.18); }
190+
191+
/* Spacing for layout containers */
192+
.gap-2 { gap: 0.5rem; }
193+
.gap-4 { gap: 1rem; }
194+
.gap-6 { gap: 1.5rem; }
195+
196+
/* Text alignment */
197+
.text-left { text-align: left; }
198+
.text-center { text-align: center; }
199+
.text-right { text-align: right; }
200+
}

src/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ export default function RootLayout({
2626
return (
2727
<html lang="en">
2828
<body
29+
suppressHydrationWarning={true}
2930
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3031
>
3132
{children}
33+
{/* Global toaster with default settings so keyboard shortcut toasts render without being affected by per-component customizer */}
3234
<Toaster />
3335
</body>
3436
</html>

src/components/features/action-buttons.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@ import { useState } from 'react';
99
import { toast } from 'sonner';
1010

1111
export function ActionButtons() {
12-
const { selectedComponent, customProps } = useComponentStore();
12+
const { selectedComponent, customProps, stylePresetClassName } = useComponentStore();
1313
const [copied, setCopied] = useState(false);
1414
const [downloaded, setDownloaded] = useState(false);
1515

1616
if (!selectedComponent) {
1717
return null;
1818
}
1919

20+
const combinedProps = {
21+
...customProps,
22+
className: `${stylePresetClassName ?? ''} ${typeof customProps.className === 'string' ? customProps.className : ''}`.trim()
23+
};
24+
2025
const handleCopyCode = async () => {
21-
const code = generateFullCode(selectedComponent, customProps);
26+
const code = generateFullCode(selectedComponent, combinedProps);
2227
const success = await copyToClipboard(code);
2328
if (success) {
2429
setCopied(true);
@@ -30,14 +35,14 @@ export function ActionButtons() {
3035
};
3136

3237
const handleDownloadComponent = () => {
33-
downloadComponent(selectedComponent, customProps);
38+
downloadComponent(selectedComponent, combinedProps);
3439
setDownloaded(true);
3540
setTimeout(() => setDownloaded(false), 2000);
3641
toast.success(`${selectedComponent.displayName} component downloaded!`);
3742
};
3843

3944
const handleCopyJSX = async () => {
40-
const code = generateFullCode(selectedComponent, customProps);
45+
const code = generateFullCode(selectedComponent, combinedProps);
4146
const jsxOnly = code.split('\n').slice(1, -1).join('\n'); // Remove import and export lines
4247
const success = await copyToClipboard(jsxOnly);
4348
if (success) {

src/components/features/component-preview.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const componentMap: ComponentMap = {
129129
};
130130

131131
export function ComponentPreview() {
132-
const { selectedComponent, customProps } = useComponentStore();
132+
const { selectedComponent, customProps, stylePresetClassName } = useComponentStore();
133133
// customProps now supports string[] for multi-value props
134134

135135
const renderedComponent = useMemo(() => {
@@ -144,7 +144,9 @@ export function ComponentPreview() {
144144
if (key === 'children' && (typeof value === 'string' || Array.isArray(value))) {
145145
acc.children = value;
146146
} else if (key === 'className') {
147-
acc.className = value;
147+
const preset = stylePresetClassName ?? '';
148+
const freeform = (value as string) ?? '';
149+
acc.className = `${preset} ${freeform}`.trim();
148150
} else if (key === 'disabled' || key === 'required' || key === 'checked' || key === 'open' || key === 'collapsible') {
149151
acc[key] = Boolean(value);
150152
} else if (key === 'rows' || key === 'defaultSize' || key === 'minSize' || key === 'duration') {
@@ -388,17 +390,22 @@ export function ComponentPreview() {
388390
console.error('Error rendering component:', error);
389391
return <div className="text-red-500">Error rendering component</div>;
390392
}
391-
}, [selectedComponent, customProps]);
393+
}, [selectedComponent, customProps, stylePresetClassName]);
392394

393395
const generatedCode = useMemo(() => {
394396
if (!selectedComponent) return '';
395397

396398
// Special case for sonner (toast) component
397399
if (selectedComponent.name === 'sonner') {
398400
const duration = typeof customProps.duration === 'number' ? customProps.duration : 4000;
399-
return `<Button onClick={() => toast.success('Hello World!', { duration: ${duration} })}>
400-
Show Toast
401-
</Button>`;
401+
const position = (customProps.position as import('sonner').ToasterProps['position']) ?? 'bottom-right';
402+
const richColors = Boolean(customProps.richColors);
403+
return `<>
404+
<Toaster position="${position}" richColors={${richColors}} />
405+
<Button onClick={() => toast.success('Hello World!', { duration: ${duration} })}>
406+
Show Toast
407+
</Button>
408+
</>`;
402409
}
403410

404411
return generateComponentCode(selectedComponent, customProps);
@@ -443,9 +450,16 @@ export function ComponentPreview() {
443450
<CardContent>
444451
<div className="border rounded-lg p-8 bg-gray-50 dark:bg-gray-900 min-h-[220px] flex items-start justify-start overflow-auto">
445452
{selectedComponent?.name === 'sonner' ? (
446-
<Button onClick={() => toast.success('Hello World!', { duration: typeof customProps.duration === 'number' ? customProps.duration : 4000 })}>
447-
Show Toast
448-
</Button>
453+
<div className="space-y-4">
454+
{/* Local toaster configured from custom properties to scope changes to this preview only */}
455+
<Toaster
456+
position={(customProps.position as import('sonner').ToasterProps['position']) ?? 'bottom-right'}
457+
richColors={Boolean(customProps.richColors)}
458+
/>
459+
<Button onClick={() => toast.success('Hello World!', { duration: typeof customProps.duration === 'number' ? customProps.duration : 4000 })}>
460+
Show Toast
461+
</Button>
462+
</div>
449463
) : (
450464
renderedComponent
451465
)}

src/components/features/component-selector.tsx

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,17 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
55
import { Input } from '@/components/ui/input';
66
import { Badge } from '@/components/ui/badge';
77
import { ScrollArea } from '@/components/ui/scroll-area';
8-
import { Button } from '@/components/ui/button';
98
import { Search, Package, AlertCircle } from 'lucide-react';
109
import { useComponentStore } from '@/stores/component-store';
1110
import { getAvailableComponents } from '@/lib/component-data';
1211
import { ShadcnComponent } from '@/types/component';
1312
import { useDebounce } from '@/hooks/use-debounce';
1413
import { LoadingSpinner } from '@/components/ui/loading-spinner';
1514

16-
const CATEGORIES = [
17-
{ id: 'all', name: 'All Components', color: 'bg-blue-500' },
18-
{ id: 'ui', name: 'UI Components', color: 'bg-green-500' },
19-
{ id: 'form', name: 'Form Components', color: 'bg-purple-500' },
20-
{ id: 'layout', name: 'Layout Components', color: 'bg-orange-500' },
21-
{ id: 'feedback', name: 'Feedback', color: 'bg-red-500' },
22-
{ id: 'navigation', name: 'Navigation', color: 'bg-indigo-500' },
23-
{ id: 'data-display', name: 'Data Display', color: 'bg-pink-500' },
24-
{ id: 'overlay', name: 'Overlay', color: 'bg-yellow-500' }
25-
];
15+
// Removed category filter section per user request
2616

2717
export function ComponentSelector() {
28-
const { searchQuery, selectedCategory, selectedComponent, setSearchQuery, setSelectedCategory, setSelectedComponent } = useComponentStore();
18+
const { searchQuery, selectedComponent, setSearchQuery, setSelectedComponent } = useComponentStore();
2919
const [components, setComponents] = useState<ShadcnComponent[]>([]);
3020
const [filteredComponents, setFilteredComponents] = useState<ShadcnComponent[]>([]);
3121

@@ -49,13 +39,10 @@ export function ComponentSelector() {
4939
);
5040
}
5141

52-
// Filter by category
53-
if (selectedCategory !== 'all') {
54-
filtered = filtered.filter(comp => comp.category === selectedCategory);
55-
}
42+
// Category filtering removed
5643

5744
setFilteredComponents(filtered);
58-
}, [components, debouncedSearchQuery, selectedCategory]);
45+
}, [components, debouncedSearchQuery]);
5946

6047
const loadComponents = async () => {
6148
try {
@@ -77,17 +64,24 @@ export function ComponentSelector() {
7764
setSearchQuery(value);
7865
};
7966

80-
const handleCategoryChange = (categoryId: string) => {
81-
setSelectedCategory(categoryId);
82-
};
67+
// Category filter removed
8368

8469
const handleComponentSelect = (component: ShadcnComponent) => {
8570
setSelectedComponent(component);
8671
};
8772

8873
const getCategoryColor = (category: string) => {
89-
const cat = CATEGORIES.find(c => c.id === category);
90-
return cat?.color || 'bg-gray-500';
74+
// Keep badge color mapping simple without filter buttons
75+
switch (category) {
76+
case 'ui': return 'bg-green-500';
77+
case 'form': return 'bg-purple-500';
78+
case 'layout': return 'bg-orange-500';
79+
case 'feedback': return 'bg-red-500';
80+
case 'navigation': return 'bg-indigo-500';
81+
case 'data-display': return 'bg-pink-500';
82+
case 'overlay': return 'bg-yellow-500';
83+
default: return 'bg-gray-500';
84+
}
9185
};
9286

9387
if (isLoading) {
@@ -112,21 +106,7 @@ export function ComponentSelector() {
112106
/>
113107
</div>
114108

115-
{/* Category Filter */}
116-
<div className="flex flex-wrap gap-2">
117-
{CATEGORIES.map((category) => (
118-
<Button
119-
key={category.id}
120-
variant={selectedCategory === category.id ? 'default' : 'outline'}
121-
size="sm"
122-
onClick={() => handleCategoryChange(category.id)}
123-
className="text-xs"
124-
>
125-
<div className={`w-2 h-2 rounded-full ${category.color} mr-2`} />
126-
{category.name}
127-
</Button>
128-
))}
129-
</div>
109+
{/* Category Filter removed */}
130110

131111
{/* Components Grid */}
132112
<ScrollArea className="h-[600px]">
@@ -139,7 +119,7 @@ export function ComponentSelector() {
139119
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
140120
<AlertCircle className="h-12 w-12 mb-4 opacity-50" />
141121
<p className="text-center">
142-
{searchQuery || selectedCategory !== 'all'
122+
{searchQuery
143123
? 'No components found matching your criteria'
144124
: 'No components available'
145125
}

src/components/features/property-customizer.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
1515
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';
1616

1717
export function PropertyCustomizer() {
18-
const { selectedComponent, customProps, updateCustomProp, resetCustomProps } = useComponentStore();
18+
const { selectedComponent, customProps, stylePresetClassName, updateCustomProp, resetCustomProps } = useComponentStore();
1919

2020
if (!selectedComponent) {
2121
return (
@@ -129,7 +129,11 @@ export function PropertyCustomizer() {
129129
}
130130
};
131131

132-
const currentCode = generateComponentCode(selectedComponent, customProps);
132+
const combinedProps = {
133+
...customProps,
134+
className: `${stylePresetClassName ?? ''} ${typeof customProps.className === 'string' ? customProps.className : ''}`.trim()
135+
};
136+
const currentCode = generateComponentCode(selectedComponent, combinedProps);
133137

134138
return (
135139
<div className="space-y-4">

0 commit comments

Comments
 (0)