Skip to content

Commit c214fc2

Browse files
committed
feat(theme): add comprehensive theme customization with preview and presets
- Replace simple WidgetTheme with advanced AppTheme supporting colors, typography, radius, and shadows - Add interactive theme preview with example components and wallpaper background - Implement font family picker with system font detection using queryLocalFonts API - Create theme preset system with local storage persistence and creation dialog - Add new UI components (Command, Dialog) and update translation files for new features - Remove old widget-theme-form and migrate to tab-based app-theme-form with detailed controls
1 parent fca5c2b commit c214fc2

11 files changed

Lines changed: 1128 additions & 195 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@
2020
"@tailwindcss/vite": "^4.2.0",
2121
"@uiw/react-color": "^2.9.5",
2222
"@widget-js/core": "latest",
23-
"@widget-js/react": "workspace:^",
23+
"@widget-js/react": "latest",
2424
"@widget-js/web-api": "24.1.1-beta.70",
2525
"axios": "^1.13.5",
2626
"baseline-browser-mapping": "^2.10.0",
2727
"class-variance-authority": "^0.7.1",
2828
"clsx": "^2.1.1",
29+
"cmdk": "^1.1.1",
2930
"consola": "^3.4.2",
3031
"driver.js": "^1.4.0",
3132
"framer-motion": "^12.34.3",

src/assets/images/wallpaper.jpg

362 KB
Loading

src/components/ui/command.tsx

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import { Command as CommandPrimitive } from "cmdk"
5+
import { SearchIcon } from "lucide-react"
6+
7+
import { cn } from "@/lib/utils"
8+
import {
9+
Dialog,
10+
DialogContent,
11+
DialogDescription,
12+
DialogHeader,
13+
DialogTitle,
14+
} from "@/components/ui/dialog"
15+
16+
function Command({
17+
className,
18+
...props
19+
}: React.ComponentProps<typeof CommandPrimitive>) {
20+
return (
21+
<CommandPrimitive
22+
data-slot="command"
23+
className={cn(
24+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
25+
className
26+
)}
27+
{...props}
28+
/>
29+
)
30+
}
31+
32+
function CommandDialog({
33+
title = "Command Palette",
34+
description = "Search for a command to run...",
35+
children,
36+
className,
37+
showCloseButton = true,
38+
...props
39+
}: React.ComponentProps<typeof Dialog> & {
40+
title?: string
41+
description?: string
42+
className?: string
43+
showCloseButton?: boolean
44+
}) {
45+
return (
46+
<Dialog {...props}>
47+
<DialogHeader className="sr-only">
48+
<DialogTitle>{title}</DialogTitle>
49+
<DialogDescription>{description}</DialogDescription>
50+
</DialogHeader>
51+
<DialogContent
52+
className={cn("overflow-hidden p-0", className)}
53+
showCloseButton={showCloseButton}
54+
>
55+
<Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
56+
{children}
57+
</Command>
58+
</DialogContent>
59+
</Dialog>
60+
)
61+
}
62+
63+
function CommandInput({
64+
className,
65+
...props
66+
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
67+
return (
68+
<div
69+
data-slot="command-input-wrapper"
70+
className="flex h-9 items-center gap-2 border-b px-3"
71+
>
72+
<SearchIcon className="size-4 shrink-0 opacity-50" />
73+
<CommandPrimitive.Input
74+
data-slot="command-input"
75+
className={cn(
76+
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
77+
className
78+
)}
79+
{...props}
80+
/>
81+
</div>
82+
)
83+
}
84+
85+
function CommandList({
86+
className,
87+
...props
88+
}: React.ComponentProps<typeof CommandPrimitive.List>) {
89+
return (
90+
<CommandPrimitive.List
91+
data-slot="command-list"
92+
className={cn(
93+
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
94+
className
95+
)}
96+
{...props}
97+
/>
98+
)
99+
}
100+
101+
function CommandEmpty({
102+
...props
103+
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
104+
return (
105+
<CommandPrimitive.Empty
106+
data-slot="command-empty"
107+
className="py-6 text-center text-sm"
108+
{...props}
109+
/>
110+
)
111+
}
112+
113+
function CommandGroup({
114+
className,
115+
...props
116+
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
117+
return (
118+
<CommandPrimitive.Group
119+
data-slot="command-group"
120+
className={cn(
121+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
122+
className
123+
)}
124+
{...props}
125+
/>
126+
)
127+
}
128+
129+
function CommandSeparator({
130+
className,
131+
...props
132+
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
133+
return (
134+
<CommandPrimitive.Separator
135+
data-slot="command-separator"
136+
className={cn("-mx-1 h-px bg-border", className)}
137+
{...props}
138+
/>
139+
)
140+
}
141+
142+
function CommandItem({
143+
className,
144+
...props
145+
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
146+
return (
147+
<CommandPrimitive.Item
148+
data-slot="command-item"
149+
className={cn(
150+
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
151+
className
152+
)}
153+
{...props}
154+
/>
155+
)
156+
}
157+
158+
function CommandShortcut({
159+
className,
160+
...props
161+
}: React.ComponentProps<"span">) {
162+
return (
163+
<span
164+
data-slot="command-shortcut"
165+
className={cn(
166+
"ml-auto text-xs tracking-widest text-muted-foreground",
167+
className
168+
)}
169+
{...props}
170+
/>
171+
)
172+
}
173+
174+
export {
175+
Command,
176+
CommandDialog,
177+
CommandInput,
178+
CommandList,
179+
CommandEmpty,
180+
CommandGroup,
181+
CommandItem,
182+
CommandShortcut,
183+
CommandSeparator,
184+
}

src/locales/en/translation.json

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@
6565
"theme": {
6666
"title": "Global Theme",
6767
"presets": "Presets",
68+
"createPreset": {
69+
"button": "Create preset",
70+
"title": "Create theme preset",
71+
"description": "Save the current theme as a reusable preset.",
72+
"namePlaceholder": "Enter a theme name",
73+
"cancel": "Cancel",
74+
"confirm": "Create",
75+
"emptyName": "Please enter a theme name",
76+
"duplicateName": "A preset with the same name already exists",
77+
"success": "Theme preset created"
78+
},
6879
"customization": "Customization",
6980
"fontSize": "Font Size",
7081
"borderRadius": "Border Radius",
@@ -78,7 +89,72 @@
7889
"dividerColor": "Divider Color",
7990
"translucent": "Translucent",
8091
"dark": "Dark",
81-
"light": "Light"
92+
"light": "Light",
93+
"searchFont": "Search font...",
94+
"fontNotFound": "No font found",
95+
"colors": {
96+
"title": "Colors",
97+
"base": "Base",
98+
"primary": "Primary",
99+
"secondary": "Secondary",
100+
"muted": "Muted",
101+
"accent": "Accent",
102+
"destructive": "Destructive",
103+
"cardPopover": "Card & Popover",
104+
"bordersInputs": "Borders & Inputs",
105+
"background": "Background",
106+
"foreground": "Foreground",
107+
"card": "Card",
108+
"cardForeground": "Card Foreground",
109+
"popover": "Popover",
110+
"popoverForeground": "Popover Foreground",
111+
"primaryForeground": "Primary Foreground",
112+
"secondaryForeground": "Secondary Foreground",
113+
"mutedForeground": "Muted Foreground",
114+
"accentForeground": "Accent Foreground",
115+
"destructiveForeground": "Destructive Foreground",
116+
"border": "Border",
117+
"input": "Input",
118+
"ring": "Ring",
119+
"shadow": "Shadow",
120+
"innerShadow": "Inner Shadow"
121+
},
122+
"typography": {
123+
"title": "Typography"
124+
},
125+
"radius": {
126+
"title": "Radius",
127+
"sm": "Small Radius (sm)",
128+
"md": "Medium Radius (md)",
129+
"lg": "Large Radius (lg/Widget Usage)",
130+
"full": "Full Radius"
131+
},
132+
"shadow": {
133+
"title": "Shadow",
134+
"sm": "Small Shadow (sm)",
135+
"md": "Medium Shadow (md)",
136+
"lg": "Large Shadow (lg)"
137+
},
138+
"preview": {
139+
"title": "Theme Preview",
140+
"description": "This is how your widgets and app components will look with the current theme settings.",
141+
"cardTitle": "Example Component",
142+
"cardDesc": "A descriptive subtitle for the card.",
143+
"email": "Email Address",
144+
"emailPlaceholder": "name@example.com",
145+
"enableNotifications": "Enable notifications",
146+
"cancel": "Cancel",
147+
"save": "Save Changes",
148+
"buttons": "Buttons",
149+
"primary": "Primary",
150+
"secondary": "Secondary",
151+
"destructive": "Destructive",
152+
"outline": "Outline",
153+
"ghost": "Ghost",
154+
"inputRing": "Input & Focus Ring",
155+
"inputPlaceholder": "Focus me to see the ring color...",
156+
"slider": "Slider"
157+
}
82158
},
83159
"dashboard": {
84160
"home": "Home",

src/locales/zh/translation.json

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,21 @@
6565
"theme": {
6666
"title": "全局主题",
6767
"presets": "预设",
68+
"createPreset": {
69+
"button": "创建预设",
70+
"title": "创建主题预设",
71+
"description": "为当前主题保存一个可重复使用的预设。",
72+
"namePlaceholder": "请输入主题名称",
73+
"cancel": "取消",
74+
"confirm": "创建",
75+
"emptyName": "请输入主题名称",
76+
"duplicateName": "已存在同名主题预设",
77+
"success": "主题预设已创建"
78+
},
6879
"customization": "自定义",
6980
"fontSize": "字体大小",
7081
"borderRadius": "圆角大小",
71-
"fontFamily": "字体系列",
82+
"fontFamily": "字体",
7283
"systemDefault": "系统默认",
7384
"primaryColor": "主色调",
7485
"textColor": "文本颜色",
@@ -78,7 +89,72 @@
7889
"dividerColor": "分割线颜色",
7990
"translucent": "半透明",
8091
"dark": "暗色",
81-
"light": "亮色"
92+
"light": "亮色",
93+
"searchFont": "搜索字体...",
94+
"fontNotFound": "未找到字体",
95+
"colors": {
96+
"title": "颜色体系",
97+
"base": "基础色调",
98+
"primary": "主色调",
99+
"secondary": "次色调",
100+
"muted": "弱化色调",
101+
"accent": "强调色调",
102+
"destructive": "危险色调",
103+
"cardPopover": "卡片与浮层",
104+
"bordersInputs": "边框与输入",
105+
"background": "背景色",
106+
"foreground": "前景色",
107+
"card": "卡片背景",
108+
"cardForeground": "卡片文本",
109+
"popover": "浮层背景",
110+
"popoverForeground": "浮层文本",
111+
"primaryForeground": "主文本色",
112+
"secondaryForeground": "次文本色",
113+
"mutedForeground": "弱化文本色",
114+
"accentForeground": "强调文本色",
115+
"destructiveForeground": "危险文本色",
116+
"border": "边框色",
117+
"input": "输入框背景",
118+
"ring": "焦点环",
119+
"shadow": "阴影色",
120+
"innerShadow": "内阴影色"
121+
},
122+
"typography": {
123+
"title": "排版"
124+
},
125+
"radius": {
126+
"title": "圆角",
127+
"sm": "小圆角 (sm)",
128+
"md": "中圆角 (md)",
129+
"lg": "大圆角 (lg/组件使用)",
130+
"full": "全圆角 (full)"
131+
},
132+
"shadow": {
133+
"title": "阴影",
134+
"sm": "小阴影 (sm)",
135+
"md": "中阴影 (md)",
136+
"lg": "大阴影 (lg)"
137+
},
138+
"preview": {
139+
"title": "主题预览",
140+
"description": "您的组件和应用将以此主题设置呈现。",
141+
"cardTitle": "示例组件",
142+
"cardDesc": "卡片的描述性副标题。",
143+
"email": "电子邮箱",
144+
"emailPlaceholder": "name@example.com",
145+
"enableNotifications": "启用通知",
146+
"cancel": "取消",
147+
"save": "保存更改",
148+
"buttons": "按钮",
149+
"primary": "主要",
150+
"secondary": "次要",
151+
"destructive": "危险",
152+
"outline": "轮廓",
153+
"ghost": "幽灵",
154+
"inputRing": "输入框与焦点环",
155+
"inputPlaceholder": "点击此处以查看焦点环颜色...",
156+
"slider": "滑块"
157+
}
82158
},
83159
"dashboard": {
84160
"home": "首页",

0 commit comments

Comments
 (0)