From 66e1b15bf1ad53696073c311eebda72e8205e2a8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:38:36 +0000 Subject: [PATCH 1/2] 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> --- .../js/components/themes/controls-panel.tsx | 16 ++- .../js/components/themes/theme-preview.tsx | 104 ++++-------------- resources/js/pages/themes/create.tsx | 53 +++++++-- resources/js/store/theme.ts | 22 ++++ 4 files changed, 104 insertions(+), 91 deletions(-) diff --git a/resources/js/components/themes/controls-panel.tsx b/resources/js/components/themes/controls-panel.tsx index d6be42f..0c4f4fe 100644 --- a/resources/js/components/themes/controls-panel.tsx +++ b/resources/js/components/themes/controls-panel.tsx @@ -1,15 +1,22 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { useThemeStore } from '@/lib/theme/store'; +import { usePage } from '@inertiajs/react'; import { ColorControls } from './color-controls'; import { RadiusControls } from './raduis-controles'; +import ThemeInfo from './theme-info'; import { TypographyControls } from './typography-controls'; export function ControlsPanel() { + const { title, name, description, tags, setInfo } = useThemeStore(); + const { availableTags = [] } = usePage<{ availableTags: string[] }>().props; + return ( - + Colors Radius Type + Info
@@ -21,6 +28,13 @@ export function ControlsPanel() { + + +
); diff --git a/resources/js/components/themes/theme-preview.tsx b/resources/js/components/themes/theme-preview.tsx index 22d86b3..6973199 100644 --- a/resources/js/components/themes/theme-preview.tsx +++ b/resources/js/components/themes/theme-preview.tsx @@ -1,12 +1,9 @@ import { useThemeStore } from '@/lib/theme/store'; import { tokenValueToCss } from '@/lib/theme/color'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; import { Sun, Moon } from 'lucide-react'; import { useMemo, useState } from 'react'; +import CardsPreview from '@/components/preview/cards-preview'; const COLOR_TOKENS = [ 'background', 'foreground', @@ -68,93 +65,38 @@ export function ThemePreview() { return (
-
- Preview +
+ + Preview +
-
-

- Theme Preview -

-

- Sample UI components using your current palette. -

- -
- - - - - -
- - - - Example Card - - -

- This card demonstrates how your theme appears on standard - shadcn/ui components. -

-
- - - - -
- - - - Form Example - - -
- - -
-
- - -
-
-
- -
- Default - Secondary - Outline - Destructive -
- -
-

- SYSTEM — font-mono ({fonts.mono}) -

-

- The quick brown fox jumps over the lazy dog — serif -

-

- The quick brown fox jumps over the lazy dog — sans -

-
+
+
diff --git a/resources/js/pages/themes/create.tsx b/resources/js/pages/themes/create.tsx index c86f2de..78b0fff 100644 --- a/resources/js/pages/themes/create.tsx +++ b/resources/js/pages/themes/create.tsx @@ -1,14 +1,17 @@ -import { Head, Link } from '@inertiajs/react'; -import { ArrowLeft, Pencil } from 'lucide-react'; +import { Head, Link, router } from '@inertiajs/react'; +import { ArrowLeft, Check, Pencil, Save } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { ControlsPanel } from '@/components/themes/controls-panel'; import { ExportDialog } from '@/components/themes/export-dialog'; import { ThemePreview } from '@/components/themes/theme-preview'; +import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; import ThemeCreatorLayout from '@/layouts/theme-creator-layout'; import { cn } from '@/lib/utils'; -import { index } from '@/routes/themes'; +import { index, store } from '@/routes/themes'; import { useThemeCreatorStore } from '@/store/theme-creator'; +import { useThemeStore } from '@/lib/theme/store'; +import { toast } from 'sonner'; const MOBILE_TABS = ['Editor', 'Preview'] as const; const MIN_SIDEBAR = 330; @@ -16,7 +19,7 @@ const MAX_SIDEBAR_PCT = 0.5; export default function ThemeCreate() { const [mobileTab, setMobileTab] = useState('Editor'); - const [themeName, setThemeName] = useState('My New Theme'); + const { title, setTitle, name, description, tags, light, dark, radius, fonts } = useThemeStore(); const [isEditing, setIsEditing] = useState(false); const inputRef = useRef(null); @@ -60,9 +63,38 @@ export default function ThemeCreate() { }; }, []); + const handleSave = () => { + const themeData = { + name, + title, + description, + tags, + cssVars: { + light, + dark, + }, + font: { + family: fonts.sans, + serif: fonts.serif, + mono: fonts.mono, + }, + radius, + }; + + router.post(store().url, { theme_data: themeData }, { + onSuccess: () => { + toast.success('Theme saved successfully!'); + }, + onError: (errors) => { + const message = Object.values(errors).flat().join(' '); + toast.error(message || 'Failed to save theme.'); + } + }); + }; + return ( <> - +
-
+
+
diff --git a/resources/js/store/theme.ts b/resources/js/store/theme.ts index 028d76b..00443e5 100644 --- a/resources/js/store/theme.ts +++ b/resources/js/store/theme.ts @@ -5,6 +5,11 @@ import { DEFAULT_LIGHT, DEFAULT_DARK, type TokenMap } from '@/lib/theme/defaults export type ThemeFonts = { sans: string; serif: string; mono: string }; export interface ThemeState { + title: string; + name: string; + description: string; + tags: string[]; + light: TokenMap; dark: TokenMap; radius: number; @@ -14,6 +19,12 @@ export interface ThemeState { lineHeight: number; letterSpacing: number; + setTitle: (v: string) => void; + setName: (v: string) => void; + setDescription: (v: string) => void; + setTags: (v: string[]) => void; + setInfo: (info: Partial>) => void; + setToken: (mode: 'light' | 'dark', token: string, value: string) => void; setManyTokens: (mode: 'light' | 'dark', map: Record) => void; setRadius: (v: number) => void; @@ -27,6 +38,11 @@ export interface ThemeState { export const useThemeStore = create()( persist( (set) => ({ + title: 'My New Theme', + name: 'my-new-theme', + description: '', + tags: [], + light: { ...DEFAULT_LIGHT }, dark: { ...DEFAULT_DARK }, radius: 0.625, @@ -36,6 +52,12 @@ export const useThemeStore = create()( lineHeight: 1.5, letterSpacing: 0, + setTitle: (v) => set({ title: v }), + setName: (v) => set({ name: v }), + setDescription: (v) => set({ description: v }), + setTags: (v) => set({ tags: v }), + setInfo: (info) => set((s) => ({ ...s, ...info })), + setToken: (mode, token, value) => set((s) => ({ [mode]: { ...s[mode], [token]: value }, From d79911af534eb271c224e8a18a17fe6744fd12ad Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:55:01 +0000 Subject: [PATCH 2/2] fix: resolve linting issues in theme creator and previews - Removed unused imports (`Check`, `Head`, `useCallback`) and variables (`onTooltipOpenChange`, `kind`). - Added missing dependency (`setSidebarWidth`) to `useEffect` in `create.tsx`. - Applied automated linting and formatting fixes across multiple files to satisfy CI checks. Co-authored-by: claudemyburgh <6057076+claudemyburgh@users.noreply.github.com> --- .../js/components/app/newsletter-dialog.tsx | 4 +-- resources/js/components/delete-user.tsx | 2 +- .../js/components/preview/cards-preview.tsx | 3 +- .../js/components/themes/color-controls.tsx | 4 +-- .../js/components/themes/controls-panel.tsx | 2 +- .../js/components/themes/export-dialog.tsx | 14 ++++---- .../js/components/themes/raduis-controles.tsx | 3 +- .../js/components/themes/theme-preview.tsx | 7 ++-- .../components/themes/typography-controls.tsx | 6 ++-- resources/js/layouts/main/main-footer.tsx | 2 +- .../js/layouts/main/theme/main-theme-card.tsx | 3 +- .../layouts/main/theme/main-theme-search.tsx | 4 ++- resources/js/lib/theme/color.ts | 29 ++++++++++++--- resources/js/lib/theme/css-export.ts | 35 ++++++++++++++++--- resources/js/pages/settings/profile.tsx | 2 +- resources/js/pages/settings/security.tsx | 2 +- resources/js/pages/themes/create.tsx | 8 ++--- resources/js/store/theme.ts | 3 +- 18 files changed, 91 insertions(+), 42 deletions(-) diff --git a/resources/js/components/app/newsletter-dialog.tsx b/resources/js/components/app/newsletter-dialog.tsx index bd07bcb..0d3dc6b 100644 --- a/resources/js/components/app/newsletter-dialog.tsx +++ b/resources/js/components/app/newsletter-dialog.tsx @@ -1,5 +1,6 @@ -import { Form, Head } from '@inertiajs/react'; +import { Form } from '@inertiajs/react'; import { Mail } from 'lucide-react'; +import InputError from '@/components/input-error'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -11,7 +12,6 @@ import { import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Spinner } from '@/components/ui/spinner'; -import InputError from '@/components/input-error'; import { subscribe } from '@/routes/newsletter'; interface NewsletterDialogProps { diff --git a/resources/js/components/delete-user.tsx b/resources/js/components/delete-user.tsx index 213e76f..6df1845 100644 --- a/resources/js/components/delete-user.tsx +++ b/resources/js/components/delete-user.tsx @@ -1,6 +1,5 @@ import { Form } from '@inertiajs/react'; import { useRef } from 'react'; -import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; import Heading from '@/components/heading'; import InputError from '@/components/input-error'; import PasswordInput from '@/components/password-input'; @@ -15,6 +14,7 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; +import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; export default function DeleteUser() { const passwordInput = useRef(null); diff --git a/resources/js/components/preview/cards-preview.tsx b/resources/js/components/preview/cards-preview.tsx index b3f1705..e10c3fc 100644 --- a/resources/js/components/preview/cards-preview.tsx +++ b/resources/js/components/preview/cards-preview.tsx @@ -1,5 +1,5 @@ import { ChevronDown, Sparkles, Terminal } from 'lucide-react'; -import { useCallback, useState } from 'react'; +import { useState } from 'react'; import { Bar, BarChart, @@ -91,7 +91,6 @@ const chartData = [ export default function CardsPreview({ ...props }) { const [date, setDate] = useState(new Date()); - const onTooltipOpenChange = useCallback(() => {}, []); return (
diff --git a/resources/js/components/themes/theme-preview.tsx b/resources/js/components/themes/theme-preview.tsx index 6973199..e35cfda 100644 --- a/resources/js/components/themes/theme-preview.tsx +++ b/resources/js/components/themes/theme-preview.tsx @@ -1,9 +1,9 @@ -import { useThemeStore } from '@/lib/theme/store'; -import { tokenValueToCss } from '@/lib/theme/color'; -import { Button } from '@/components/ui/button'; import { Sun, Moon } from 'lucide-react'; import { useMemo, useState } from 'react'; import CardsPreview from '@/components/preview/cards-preview'; +import { Button } from '@/components/ui/button'; +import { tokenValueToCss } from '@/lib/theme/color'; +import { useThemeStore } from '@/lib/theme/store'; const COLOR_TOKENS = [ 'background', 'foreground', @@ -27,6 +27,7 @@ function buildVars( for (const token of COLOR_TOKENS) { const val = colors[token]; + if (val) { const css = tokenValueToCss(val); vars[`--${token}`] = css; diff --git a/resources/js/components/themes/typography-controls.tsx b/resources/js/components/themes/typography-controls.tsx index e6df266..861037e 100644 --- a/resources/js/components/themes/typography-controls.tsx +++ b/resources/js/components/themes/typography-controls.tsx @@ -7,14 +7,14 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Slider } from '@/components/ui/slider'; +import { useThemeStore } from '@/lib/theme/store'; import { loadGoogleFont, MONO_FONTS, SANS_FONTS, SERIF_FONTS, } from './font-list'; -import { Slider } from '@/components/ui/slider'; -import { useThemeStore } from '@/lib/theme/store'; const SCALES = [ { name: 'Minor Second', value: 1.067 }, @@ -129,13 +129,11 @@ export function TypographyControls() { } function FontSelect({ - kind, label, options, value, onChange, }: { - kind: string; label: string; options: string[]; value: string; diff --git a/resources/js/layouts/main/main-footer.tsx b/resources/js/layouts/main/main-footer.tsx index 3f1dbff..a1354f8 100644 --- a/resources/js/layouts/main/main-footer.tsx +++ b/resources/js/layouts/main/main-footer.tsx @@ -1,11 +1,11 @@ import { Link } from '@inertiajs/react'; import { GithubIcon } from 'lucide-react'; +import XIcon from '@/components/icons/x-icon'; import { Button } from '@/components/ui/button'; import { PlaceholderPattern } from '@/components/ui/placeholder-pattern'; import MainWrapper from '@/layouts/main/main-wrapper'; import { home } from '@/routes'; -import XIcon from '@/components/icons/x-icon'; const navLinks = [ { href: '#', label: 'Features' }, diff --git a/resources/js/layouts/main/theme/main-theme-card.tsx b/resources/js/layouts/main/theme/main-theme-card.tsx index 029b2a7..f82a94c 100644 --- a/resources/js/layouts/main/theme/main-theme-card.tsx +++ b/resources/js/layouts/main/theme/main-theme-card.tsx @@ -1,4 +1,5 @@ -import { motion, type Variants } from 'motion/react'; +import { motion } from 'motion/react'; +import type {Variants} from 'motion/react'; import { Card, CardContent, diff --git a/resources/js/layouts/main/theme/main-theme-search.tsx b/resources/js/layouts/main/theme/main-theme-search.tsx index 918f35e..d436588 100644 --- a/resources/js/layouts/main/theme/main-theme-search.tsx +++ b/resources/js/layouts/main/theme/main-theme-search.tsx @@ -48,7 +48,9 @@ function MainThemeSearch({ }, [debouncedSearch, selectedTags]); useEffect(() => { - if (!showFilters) return; + if (!showFilters) { +return; +} const handler = (e: MouseEvent) => { if ( diff --git a/resources/js/lib/theme/color.ts b/resources/js/lib/theme/color.ts index 8552a17..367f9b3 100644 --- a/resources/js/lib/theme/color.ts +++ b/resources/js/lib/theme/color.ts @@ -5,12 +5,21 @@ const toOklch = converter('oklch'); // Convert hex (#rrggbb) -> "L C H" string used as token value. export function hexToTokenValue(hex: string): string { const parsed = parse(hex); - if (!parsed) return '0 0 0'; + + if (!parsed) { +return '0 0 0'; +} + const o = toOklch(parsed); - if (!o) return '0 0 0'; + + if (!o) { +return '0 0 0'; +} + const L = round(o.l ?? 0, 3); const C = round(o.c ?? 0, 3); const H = round(o.h ?? 0, 3); + return `${L} ${C} ${H}`; } @@ -19,6 +28,7 @@ export function tokenValueToHex(value: string): string { const clean = value.split('/')[0].trim(); const [l, c, h] = clean.split(/\s+/).map(Number); const hex = formatHex({ mode: 'oklch', l: l || 0, c: c || 0, h: h || 0 }); + return hex ?? '#000000'; } @@ -27,11 +37,13 @@ export function tokenValueToCss(value: string): string { // pass through alpha syntax return `oklch(${value})`; } + return `oklch(${value})`; } function round(n: number, d: number) { const f = Math.pow(10, d); + return Math.round(n * f) / f; } @@ -41,9 +53,17 @@ export function derivePaletteFromPrimary( dark: boolean, ): Record { const parsed = parse(hex); - if (!parsed) return {}; + + if (!parsed) { +return {}; +} + const o = toOklch(parsed); - if (!o) return {}; + + if (!o) { +return {}; +} + const h = o.h ?? 250; const c = o.c ?? 0.05; @@ -68,6 +88,7 @@ export function derivePaletteFromPrimary( ring: `${o.l} ${c} ${h}`, }; } + return { background: `1 0 0`, foreground: `0.16 0.02 ${h}`, diff --git a/resources/js/lib/theme/css-export.ts b/resources/js/lib/theme/css-export.ts index b8c4623..254c10c 100644 --- a/resources/js/lib/theme/css-export.ts +++ b/resources/js/lib/theme/css-export.ts @@ -4,12 +4,21 @@ const toOklch = converter('oklch'); export function hexToTokenValue(hex: string): string { const parsed = parse(hex); - if (!parsed) return '0 0 0'; + + if (!parsed) { +return '0 0 0'; +} + const o = toOklch(parsed); - if (!o) return '0 0 0'; + + if (!o) { +return '0 0 0'; +} + const L = round(o.l ?? 0, 3); const C = round(o.c ?? 0, 3); const H = round(o.h ?? 0, 3); + return `${L} ${C} ${H}`; } @@ -17,6 +26,7 @@ export function tokenValueToHex(value: string): string { const clean = value.split('/')[0].trim(); const [l, c, h] = clean.split(/\s+/).map(Number); const hex = formatHex({ mode: 'oklch', l: l || 0, c: c || 0, h: h || 0 }); + return hex ?? '#000000'; } @@ -24,11 +34,13 @@ export function tokenValueToCss(value: string): string { if (value.includes('/')) { return `oklch(${value})`; } + return `oklch(${value})`; } function round(n: number, d: number) { const f = Math.pow(10, d); + return Math.round(n * f) / f; } @@ -37,9 +49,17 @@ export function derivePaletteFromPrimary( dark: boolean, ): Record { const parsed = parse(hex); - if (!parsed) return {}; + + if (!parsed) { +return {}; +} + const o = toOklch(parsed); - if (!o) return {}; + + if (!o) { +return {}; +} + const h = o.h ?? 250; const c = o.c ?? 0.05; @@ -64,6 +84,7 @@ export function derivePaletteFromPrimary( ring: `${o.l} ${c} ${h}`, }; } + return { background: `1 0 0`, foreground: `0.16 0.02 ${h}`, @@ -110,7 +131,11 @@ export function generateIndexCss(state: { const lines = entries.map( ([k, v]) => ` ${toVar(k)}: ${toColor(v)};`, ); - if (extra) lines.push(extra); + + if (extra) { +lines.push(extra); +} + return `${selector} {\n${lines.join('\n')}\n}`; }; diff --git a/resources/js/pages/settings/profile.tsx b/resources/js/pages/settings/profile.tsx index ff878f2..5204b64 100644 --- a/resources/js/pages/settings/profile.tsx +++ b/resources/js/pages/settings/profile.tsx @@ -1,5 +1,4 @@ import { Form, Head, Link, usePage } from '@inertiajs/react'; -import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; import DeleteUser from '@/components/delete-user'; import Heading from '@/components/heading'; import InputError from '@/components/input-error'; @@ -8,6 +7,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { edit } from '@/routes/profile'; import { send } from '@/routes/verification'; +import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; export default function Profile({ mustVerifyEmail, diff --git a/resources/js/pages/settings/security.tsx b/resources/js/pages/settings/security.tsx index 78d4aa2..f81e6ae 100644 --- a/resources/js/pages/settings/security.tsx +++ b/resources/js/pages/settings/security.tsx @@ -1,7 +1,6 @@ import { Form, Head } from '@inertiajs/react'; import { ShieldCheck } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; -import SecurityController from '@/actions/App/Http/Controllers/Settings/SecurityController'; import Heading from '@/components/heading'; import InputError from '@/components/input-error'; import PasswordInput from '@/components/password-input'; @@ -12,6 +11,7 @@ import { Label } from '@/components/ui/label'; import { useTwoFactorAuth } from '@/hooks/use-two-factor-auth'; import { edit } from '@/routes/security'; import { disable, enable } from '@/routes/two-factor'; +import SecurityController from '@/actions/App/Http/Controllers/Settings/SecurityController'; type Props = { canManageTwoFactor?: boolean; diff --git a/resources/js/pages/themes/create.tsx b/resources/js/pages/themes/create.tsx index 78b0fff..3697192 100644 --- a/resources/js/pages/themes/create.tsx +++ b/resources/js/pages/themes/create.tsx @@ -1,17 +1,17 @@ import { Head, Link, router } from '@inertiajs/react'; -import { ArrowLeft, Check, Pencil, Save } from 'lucide-react'; +import { ArrowLeft, Pencil, Save } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { toast } from 'sonner'; import { ControlsPanel } from '@/components/themes/controls-panel'; import { ExportDialog } from '@/components/themes/export-dialog'; import { ThemePreview } from '@/components/themes/theme-preview'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; import ThemeCreatorLayout from '@/layouts/theme-creator-layout'; +import { useThemeStore } from '@/lib/theme/store'; import { cn } from '@/lib/utils'; import { index, store } from '@/routes/themes'; import { useThemeCreatorStore } from '@/store/theme-creator'; -import { useThemeStore } from '@/lib/theme/store'; -import { toast } from 'sonner'; const MOBILE_TABS = ['Editor', 'Preview'] as const; const MIN_SIDEBAR = 330; @@ -61,7 +61,7 @@ export default function ThemeCreate() { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, []); + }, [setSidebarWidth]); const handleSave = () => { const themeData = { diff --git a/resources/js/store/theme.ts b/resources/js/store/theme.ts index 00443e5..cca9c15 100644 --- a/resources/js/store/theme.ts +++ b/resources/js/store/theme.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import { DEFAULT_LIGHT, DEFAULT_DARK, type TokenMap } from '@/lib/theme/defaults'; +import { DEFAULT_LIGHT, DEFAULT_DARK } from '@/lib/theme/defaults'; +import type {TokenMap} from '@/lib/theme/defaults'; export type ThemeFonts = { sans: string; serif: string; mono: string };