From d646bbd95e7c8f20a2c992bf8ffde81861a42c12 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 00:14:21 +0000
Subject: [PATCH] Create theme style guide (theme.show)
This commit implements a comprehensive, interactive style guide for themes.
- Added `themes.show` route and corresponding `show` method in `ThemesController`.
- Created `resources/js/pages/themes/show.tsx` which includes:
- Dynamic color palette listing with HEX, RGB, and HSL values.
- One-click copy controls for all color values with toast confirmations.
- WCAG contrast ratio calculations for key color pairs (Text, Primary).
- Live typography samples for Sans, Serif, and Mono fonts with copy controls.
- Realistic component previews (Buttons, Inputs, Badges, Cards, Alerts).
- Responsive light/dark mode preview sandbox.
- Export section with CSS variables and JSON representation.
- Integrated with existing `useCSSVars`, `useClipboard`, and `useAppearance` hooks.
- Used `culori` for color processing and accessibility checks.
Co-authored-by: claudemyburgh <6057076+claudemyburgh@users.noreply.github.com>
---
app/Http/Controllers/ThemesController.php | 13 +-
resources/js/pages/themes/show.tsx | 382 ++++++++++++++++++++++
routes/web.php | 1 +
3 files changed, 393 insertions(+), 3 deletions(-)
create mode 100644 resources/js/pages/themes/show.tsx
diff --git a/app/Http/Controllers/ThemesController.php b/app/Http/Controllers/ThemesController.php
index 9ddf9bc..e656395 100644
--- a/app/Http/Controllers/ThemesController.php
+++ b/app/Http/Controllers/ThemesController.php
@@ -10,7 +10,7 @@ class ThemesController extends Controller
{
public function index()
{
- $availableCategories = Cache::remember('themes:available_categories', 3600, fn() => Theme::query()
+ $availableCategories = Cache::remember('themes:available_categories', 3600, fn () => Theme::query()
->select('categories')
->get()
->pluck('categories')
@@ -24,7 +24,7 @@ public function index()
'themes' => Inertia::scroll(Theme::paginate(12)->withQueryString()),
'filters' => request()->only(['search', 'category']),
'availableCategories' => $availableCategories,
- 'totalThemesCount' => Cache::remember('themes:total_count', 3600, fn() => Theme::count()),
+ 'totalThemesCount' => Cache::remember('themes:total_count', 3600, fn () => Theme::count()),
]);
}
@@ -33,7 +33,7 @@ public function apiIndex()
$query = Theme::query();
if ($search = request('search')) {
- $query->where(fn($q) => $q
+ $query->where(fn ($q) => $q
->where('name', 'like', "%{$search}%")
->orWhere('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%")
@@ -43,6 +43,13 @@ public function apiIndex()
return $query->paginate(50)->withQueryString();
}
+ public function show(Theme $theme)
+ {
+ return Inertia::render('themes/show', [
+ 'theme' => $theme,
+ ]);
+ }
+
public function css(string $name)
{
return response(
diff --git a/resources/js/pages/themes/show.tsx b/resources/js/pages/themes/show.tsx
new file mode 100644
index 0000000..0ba76e2
--- /dev/null
+++ b/resources/js/pages/themes/show.tsx
@@ -0,0 +1,382 @@
+import { Head } from '@inertiajs/react';
+import { Clipboard, Download, Moon, Sun } from 'lucide-react';
+import { useState, useMemo } from 'react';
+import { toast } from 'sonner';
+import { wcagContrast } from 'culori';
+
+import Heading from '@/components/heading';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Separator } from '@/components/ui/separator';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
+import MainLayout from '@/layouts/main-layout';
+import MainWrapper from '@/layouts/main/main-wrapper';
+import { useCSSVars } from '@/hooks/use-css-vars';
+import { useClipboard } from '@/hooks/use-clipboard';
+import { convertColor } from '@/lib/color-utils';
+import type { Registry } from '@/types/registry';
+
+interface ThemesShowProps {
+ theme: Registry;
+}
+
+function ColorSwatch({ name, value }: { name: string; value: string }) {
+ const [, copy] = useClipboard();
+ const [format, setFormat] = useState<'hex' | 'rgb' | 'hsl'>('hex');
+
+ const displayValue = useMemo(() => {
+ return convertColor(value, format) || value;
+ }, [value, format]);
+
+ const handleCopy = () => {
+ copy(displayValue);
+ toast.success(`Copied ${name} to clipboard`);
+ };
+
+ return (
+
The quick brown fox jumps over the lazy dog.
+The quick brown fox jumps over the lazy dog.
+
+ {label === 'Monospace' ? (
+
+{`function resolveTheme(name: string) {
+ const theme = themes.find(t => t.name === name);
+ return theme ?? defaultTheme;
+}`}
+
+ ) : (
+ "Design is not just what it looks like and feels like. Design is how it works. Typography is the craft of endowing human language with a durable visual form."
+ )}
+
The foundational color palette of the theme.
+Font families and scale used in this theme.
+How the theme looks applied to standard interface elements.
++ Cards are used to group related information and provide a clear hierarchy. +
+Copy these into your main CSS file.
+
+{`:root {
+${Object.entries(theme.vars_light || {}).map(([k, v]) => ` --${k}: ${v};`).join('\n')}
+}
+
+.dark {
+${Object.entries(theme.vars_dark || {}).map(([k, v]) => ` --${k}: ${v};`).join('\n')}
+}`}
+
+
+ The registry representation of the theme.
+
+ {JSON.stringify(theme, null, 2)}
+
+
+