Skip to content

Commit 52a5781

Browse files
committed
feat: export / import theme
1 parent c6cd9d1 commit 52a5781

File tree

1 file changed

+113
-13
lines changed

1 file changed

+113
-13
lines changed

frontend/app/components/admin/project/AdminProjectThemeEditor.vue

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<script setup lang="ts">
22
const modelValue = defineModel<Colors>({ required: true })
33
4+
const toast = useToast()
5+
46
const colorFields = [
57
{ key: 'accent', label: 'Accent' },
68
{ key: 'accentContrast', label: 'Accent Contrast' },
@@ -18,6 +20,98 @@ const colorFields = [
1820
1921
type ColorKey = (typeof colorFields)[number]['key']
2022
23+
// Convert camelCase to kebab-case
24+
function toKebabCase(str: string): string {
25+
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
26+
}
27+
28+
// Convert kebab-case to camelCase
29+
function toCamelCase(str: string): string {
30+
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
31+
}
32+
33+
// Export theme as JSON file
34+
function exportTheme() {
35+
const themeJson = {
36+
light: Object.fromEntries(
37+
Object.entries(modelValue.value.light)
38+
.filter(([key]) => key !== '__typename')
39+
.map(([key, value]) => [toKebabCase(key), value]),
40+
),
41+
dark: Object.fromEntries(
42+
Object.entries(modelValue.value.dark)
43+
.filter(([key]) => key !== '__typename')
44+
.map(([key, value]) => [toKebabCase(key), value]),
45+
),
46+
}
47+
console.log(themeJson)
48+
49+
const blob = new Blob([JSON.stringify(themeJson, null, 2)], {
50+
type: 'application/json',
51+
})
52+
const url = URL.createObjectURL(blob)
53+
const a = document.createElement('a')
54+
a.href = url
55+
a.download = `theme.json`
56+
a.click()
57+
URL.revokeObjectURL(url)
58+
}
59+
60+
// Import theme from JSON file
61+
function importTheme() {
62+
const input = document.createElement('input')
63+
input.type = 'file'
64+
input.accept = '.json'
65+
input.onchange = async (e) => {
66+
const file = (e.target as HTMLInputElement).files?.[0]
67+
if (!file) return
68+
69+
try {
70+
const text = await file.text()
71+
const json = JSON.parse(text)
72+
73+
if (!json.light || !json.dark) {
74+
throw new Error('Invalid theme format: missing light or dark keys')
75+
}
76+
77+
const convertColorSet = (
78+
colorSet: Record<string, string>,
79+
): Record<string, string> => {
80+
return Object.fromEntries(
81+
Object.entries(colorSet).map(([key, value]) => [
82+
toCamelCase(key),
83+
value,
84+
]),
85+
)
86+
}
87+
88+
modelValue.value = {
89+
...modelValue.value,
90+
light: {
91+
...modelValue.value.light,
92+
...convertColorSet(json.light),
93+
},
94+
dark: {
95+
...modelValue.value.dark,
96+
...convertColorSet(json.dark),
97+
},
98+
}
99+
100+
toast.add({
101+
title: 'Theme imported',
102+
color: 'success',
103+
})
104+
} catch (err) {
105+
toast.add({
106+
title: 'Failed to import theme',
107+
description: err instanceof Error ? err.message : 'Invalid JSON file',
108+
color: 'error',
109+
})
110+
}
111+
}
112+
input.click()
113+
}
114+
21115
const lightStyles = computed(() => {
22116
return {
23117
'--color-accent': modelValue.value.light.accent,
@@ -78,10 +172,9 @@ function updateDarkColor(key: ColorKey, value: string) {
78172
<UButton variant="soft" block>Open theme editor</UButton>
79173

80174
<template #body>
81-
<div class="flex gap-8">
82-
<div class="grid flex-1 grid-cols-2 gap-6">
175+
<div class="flex flex-col gap-6">
176+
<div class="mx-auto flex gap-8">
83177
<div>
84-
<h3 class="mb-4 text-lg font-semibold">Light Mode</h3>
85178
<div class="space-y-3">
86179
<UFormField
87180
v-for="field in colorFields"
@@ -95,8 +188,17 @@ function updateDarkColor(key: ColorKey, value: string) {
95188
</UFormField>
96189
</div>
97190
</div>
191+
<div class="flex shrink-0 gap-4">
192+
<div class="text-center">
193+
<p class="text-muted mb-2 text-sm">Light</p>
194+
<AdminProjectThemePreview :style="lightStyles" />
195+
</div>
196+
<div class="text-center">
197+
<p class="text-muted mb-2 text-sm">Dark</p>
198+
<AdminProjectThemePreview :style="darkStyles" />
199+
</div>
200+
</div>
98201
<div>
99-
<h3 class="mb-4 text-lg font-semibold">Dark Mode</h3>
100202
<div class="space-y-3">
101203
<UFormField
102204
v-for="field in colorFields"
@@ -111,15 +213,13 @@ function updateDarkColor(key: ColorKey, value: string) {
111213
</div>
112214
</div>
113215
</div>
114-
<div class="flex shrink-0 gap-4">
115-
<div class="text-center">
116-
<p class="text-muted mb-2 text-sm">Light</p>
117-
<AdminProjectThemePreview :style="lightStyles" />
118-
</div>
119-
<div class="text-center">
120-
<p class="text-muted mb-2 text-sm">Dark</p>
121-
<AdminProjectThemePreview :style="darkStyles" />
122-
</div>
216+
<div class="flex justify-center gap-2">
217+
<UButton variant="soft" icon="i-lucide-upload" @click="importTheme">
218+
Import
219+
</UButton>
220+
<UButton variant="soft" icon="i-lucide-download" @click="exportTheme">
221+
Export
222+
</UButton>
123223
</div>
124224
</div>
125225
</template>

0 commit comments

Comments
 (0)