Refactored AppDesigner

pull/6454/merge
Cagatay Civici 2024-12-26 17:41:51 +03:00
parent 96abb53576
commit b3f30c3ab7
27 changed files with 840 additions and 510 deletions

View File

@ -1,7 +1,6 @@
<template>
<Drawer
v-model:visible="$appState.designer.active"
header="Theme Designer"
position="right"
class="designer !w-screen md:!w-[48rem]"
:modal="false"
@ -14,142 +13,19 @@
footer: '!p-5'
}"
>
<Tabs v-model:value="$appState.designer.activeTab" :lazy="deferredTabs">
<TabList>
<Tab value="0">Base</Tab>
<Tab value="1">Primitive</Tab>
<Tab value="2">Semantic</Tab>
<Tab value="3">Component</Tab>
<Tab value="4">Custom</Tab>
</TabList>
<TabPanels class="!px-0">
<TabPanel value="0">
<div class="text-lg font-semibold mb-2">Choose a Theme to Get Started</div>
<span class="block text-muted-color leading-6 mb-4">Begin by selecting a built-in theme as a foundation, continue editing your current theme, or import a Figma tokens file.</span>
<div class="flex flex-col">
<div class="flex flex-col gap-4 border border-surface-200 dark:border-surface-700 rounded-md p-4">
<span class="font-semibold">Base Theme</span>
<span class="text-muted-color">Variety of built-in themes with distinct characteristics.</span>
<SelectButton v-model="$appState.preset" @update:modelValue="onPresetChange" :options="presetOptions" optionLabel="label" optionValue="value" :allowEmpty="false" />
</div>
<Divider>OR</Divider>
<div class="flex flex-col gap-4 border border-surface-200 dark:border-surface-700 rounded-md p-4 items-start">
<span class="font-semibold">Load Theme</span>
<span class="text-muted-color">Continue editing the theme files stored locally.</span>
<Button label="Restore from local storage" class="!px-3 !py-2" severity="secondary" @click="loadFromLocalStorage" />
</div>
<Divider>OR</Divider>
<div class="flex flex-col gap-4 border border-surface-200 dark:border-surface-700 rounded-md p-4">
<div class="flex items-center gap-4">
<span class="font-semibold">Import Figma Tokens </span>
<Tag value="PRO" severity="contrast"></Tag>
</div>
<span class="text-muted-color leading-6">Export the token studio json file and import to the Visual Editor. This feature is currently under development.</span>
<FileUpload mode="basic" disabled pt:root:class="!justify-start" />
</div>
</div>
</TabPanel>
<TabPanel value="1">
<div>
<form @keydown="onKeyDown" class="flex flex-col gap-3">
<DesignBorderRadius />
<DesignColors />
</form>
</div>
</TabPanel>
<TabPanel value="2">
<Accordion :value="['0', '1']" multiple>
<AccordionPanel value="0">
<AccordionHeader>Common</AccordionHeader>
<AccordionContent>
<div>
<form @keydown="onKeyDown" class="flex flex-col gap-3">
<DesignGeneral />
<DesignFormField />
<DesignList />
<DesignNavigation />
<DesignOverlay />
</form>
</div>
</AccordionContent>
</AccordionPanel>
<template #header>
<div class="flex items-center gap-2">
<Button v-if="$appState.designer.activeView !== 'dashboard'" variant="text" severity="secondary" rounded type="button" icon="pi pi-chevron-left" @click="openDashboard" />
<span class="font-bold text-xl">{{ viewTitle }}</span>
</div>
</template>
<AccordionPanel value="1">
<AccordionHeader>Color Scheme</AccordionHeader>
<AccordionContent>
<Tabs value="cs-0">
<TabList>
<Tab value="cs-0">Light</Tab>
<Tab value="cs-1">Dark</Tab>
</TabList>
<TabPanels class="!px-0">
<TabPanel value="cs-0">
<form @keydown="onKeyDown">
<DesignCS :value="$appState.designer.preset.semantic.colorScheme.light" />
</form>
</TabPanel>
<TabPanel value="cs-1">
<form @keydown="onKeyDown">
<DesignCS :value="$appState.designer.preset.semantic.colorScheme.dark" />
</form>
</TabPanel>
</TabPanels>
</Tabs>
</AccordionContent>
</AccordionPanel>
</Accordion>
</TabPanel>
<TabPanel value="3">
<form @keydown="onKeyDown">
<DesignComponent />
</form>
</TabPanel>
<TabPanel value="4">
<span class="leading-6 text-muted-color">Extend the theming system with your own design tokens e.g. <span class="font-medium">accent.color</span>. Do not use curly braces in the name field.</span>
<ul class="flex flex-col gap-4 list-none p-0 mx-0 my-4">
<li v-for="(token, index) of $appState.designer.customTokens" :key="index" class="first:border-t border-b border-surface-200 dark:border-surface-300 py-2">
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 flex-auto">
<span class="text-sm">Name</span>
<input v-model="token['name']" type="text" class="border border-surface-300 dark:border-surface-600 rounded-lg py-2 px-2 w-full" />
</label>
<label class="flex items-center gap-2 flex-auto">
<span class="text-sm">Value</span>
<input v-model="token['value']" type="text" class="border border-surface-300 dark:border-surface-600 rounded-lg py-2 px-2 w-full" />
</label>
<button
type="button"
@click="removeToken(index)"
class="cursor-pointer inline-flex items-center justify-center ms-auto w-8 h-8 rounded-full bg-red-50 hover:bg-red-100 text-red-600 dark:bg-red-400/10 dark:hover:bg-red-400/20 dark:text-red-400 transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-red-600 focus:dark:outline-red-400"
>
<i class="pi pi-times" />
</button>
</div>
</li>
</ul>
<div class="flex justify-between">
<button
type="button"
@click="addToken"
class="px-3 py-2 bg-zinc-950 hover:bg-zinc-800 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black rounded-md font-medium cursor-pointer transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-zinc-950 focus:dark:outline-white"
>
Add New
</button>
<button
v-if="$appState.designer.customTokens.length"
type="button"
@click="saveTokens"
class="px-3 py-2 bg-zinc-950 hover:bg-zinc-800 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black rounded-md font-medium cursor-pointer transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-zinc-950 focus:dark:outline-white"
>
Save
</button>
</div>
</TabPanel>
</TabPanels>
</Tabs>
<DesignDashboard v-if="$appState.designer.activeView === 'dashboard'" />
<DesignCreateTheme v-else-if="$appState.designer.activeView === 'create_theme'" />
<DesignEditor v-else-if="$appState.designer.activeView === 'editor'" :deferred="deferredTabs" />
<template #footer>
<div class="flex justify-between gap-2">
<div v-if="$appState.designer.active.view === 'editor'" class="flex justify-between gap-2">
<button
type="button"
@click="download"
@ -159,9 +35,9 @@
Download
</button>
<button
v-if="$appState.designer.activeTab !== '0'"
type="button"
@click="apply"
icon="pi pi-download"
class="px-3 py-2 bg-zinc-950 hover:bg-zinc-800 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black rounded-md font-medium cursor-pointer transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-zinc-950 focus:dark:outline-white"
>
Apply
@ -172,172 +48,40 @@
</template>
<script>
import EventBus from '@/app/AppEventBus';
import { $dt, updatePreset, usePreset } from '@primevue/themes';
import Aura from '@primevue/themes/aura';
import Lara from '@primevue/themes/lara';
import Material from '@primevue/themes/material';
import Nora from '@primevue/themes/nora';
import { computed } from 'vue';
const presets = {
Aura,
Material,
Lara,
Nora
};
import { $dt } from '@primevue/themes';
export default {
provide() {
return {
$preset: computed(() => this.$appState.designer.preset)
designerUtils: {
refreshACTokens: this.refreshACTokens,
saveTheme: this.saveTheme
}
};
},
data() {
return {
deferredTabs: true,
presetOptions: [
{ label: 'Aura', value: 'Aura' },
{ label: 'Material', value: 'Material' },
{ label: 'Lara', value: 'Lara' },
{ label: 'Nora', value: 'Nora' }
]
deferredTabs: true
};
},
mounted() {
if (!this.$appState.designer.preset) {
this.$appState.designer.preset = presets.Aura;
this.replaceColorPalette();
this.generateACTokens(null, this.$appState.designer.preset);
}
},
methods: {
apply() {
this.saveTheme();
updatePreset(this.$appState.designer.preset);
EventBus.emit('theme-palette-change');
onShow() {
this.deferredTabs = false;
},
onHide() {
this.deferredTabs = true;
},
download() {
const basePreset = this.$appState.preset;
const theme = JSON.stringify(this.$appState.designer.preset, null, 4).replace(/"([^"]+)":/g, '$1:');
const textContent = `import { createApp } from "vue";
import PrimeVue from "primevue/config";
import ${basePreset} from "@primevue/themes/${basePreset.toLowerCase()}";
import { definePreset } from "@primevue/themes";
const app = createApp(App);
const MyPreset = definePreset(${basePreset}, ${theme});
app.use(PrimeVue, {
theme: {
preset: MyPreset
},
});
app.mount("#app");
`;
const blob = new Blob([textContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mytheme.js';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// TODO: Fetch from endpoint
},
onPresetChange(value) {
this.$appState.preset = value;
const newPreset = presets[value];
if (value === 'Material') {
document.body.classList.add('material');
this.$primevue.config.ripple = true;
} else {
document.body.classList.remove('material');
}
this.$appState.designer.preset = newPreset;
this.replaceColorPalette();
usePreset(this.$appState.designer.preset);
apply() {
saveTheme();
updatePreset(this.$appState.designer.theme.preset);
EventBus.emit('theme-palette-change');
},
saveTheme() {
const localState = {
themes: {
defaultTheme: {
preset: this.$appState.designer.preset,
customTokens: this.$appState.designer.customTokens
}
}
};
localStorage.setItem(this.$appState.designer.key, JSON.stringify(localState));
},
loadFromLocalStorage() {
const localState = localStorage.getItem(this.$appState.designer.key);
if (localState) {
const parsedLocalState = JSON.parse(localState);
if (parsedLocalState?.themes?.defaultTheme) {
const defaultTheme = parsedLocalState.themes.defaultTheme;
if (defaultTheme.preset) {
this.$appState.designer.preset = defaultTheme.preset;
usePreset(this.$appState.designer.preset);
}
if (defaultTheme.customTokens) {
this.$appState.designer.customTokens = defaultTheme.customTokens;
this.refreshACTokens();
}
this.$toast.add({ severity: 'success', summary: 'Success', detail: 'Theme loaded to Designer.', life: 3000 });
}
} else {
this.$toast.add({ severity: 'warn', summary: 'Error', detail: 'No saved theme found.', life: 3000 });
}
},
addToken() {
this.$appState.designer.customTokens = [...this.$appState.designer.customTokens, ...[{}]];
},
removeToken(index) {
this.$appState.designer.customTokens.splice(index, 1);
},
saveTokens() {
this.$appState.designer.preset.extend = {};
this.$appState.designer.customTokens.forEach((token) => {
this.$appState.designer.preset.extend[this.transformTokenName(token.name)] = token.value;
});
this.refreshACTokens();
this.saveTheme();
this.$toast.add({ severity: 'success', summary: 'Success', detail: 'Tokens saved', life: 3000 });
},
replaceColorPalette() {
this.$appState.designer.preset.semantic.primary = this.$appState.designer.preset.primitive.emerald;
this.$appState.designer.preset.semantic.colorScheme.light.surface = { ...{ 0: '#ffffff' }, ...this.$appState.designer.preset.primitive.slate };
this.$appState.designer.preset.semantic.colorScheme.dark.surface = { ...{ 0: '#ffffff' }, ...this.$appState.designer.preset.primitive.zinc };
},
transformTokenName(name) {
if (name && name.trim().length) {
let tokenNameSections = name.split('.');
let transformedName = '';
tokenNameSections.forEach((section, index) => {
transformedName += index === 0 ? section : section.charAt(0).toUpperCase() + section.substring(1);
});
return transformedName;
} else {
return name;
}
// TODO: Save to DB or Local Storage
console.log('theme saved');
},
camelCaseToDotCase(name) {
return name.replace(/([a-z])([A-Z])/g, '$1.$2').toLowerCase();
@ -359,26 +103,33 @@ app.mount("#app");
const tokenValue = $dt(tokenName).value;
const isColor = tokenName.includes('color') || tokenName.includes('background') || regex.test(tokenName);
this.$appState.designer.acTokens.push({ token: tokenName, label: '{' + tokenName + '}', variable: $dt(tokenName).variable, value: tokenValue, isColor: isColor });
this.$appState.designer.theme.acTokens.push({ token: tokenName, label: '{' + tokenName + '}', variable: $dt(tokenName).variable, value: tokenValue, isColor: isColor });
}
}
}
},
refreshACTokens() {
this.$appState.designer.acTokens = [];
this.generateACTokens(null, this.$appState.designer.preset);
this.$appState.designer.theme.acTokens = [];
this.generateACTokens(null, this.$appState.designer.theme.preset);
console.log('refresh tokens');
},
onKeyDown(event) {
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
this.apply();
event.preventDefault();
openDashboard() {
this.$appState.designer.activeView = 'dashboard';
}
},
computed: {
viewTitle() {
const view = this.$appState.designer.activeView;
if (view === 'dashboard') {
return 'Theme Designer';
} else if (view === 'create_theme') {
return 'Create Theme';
} else if (view === 'editor') {
return this.$appState.designer.theme.name;
}
},
onShow() {
this.deferredTabs = false;
},
onHide() {
this.deferredTabs = true;
return null;
}
}
};

View File

@ -0,0 +1,169 @@
<template>
<section class="mb-6">
<span class="block text-lg font-semibold mb-2">Theme Name</span>
<input v-model="themeName" type="text" autocomplete="off" class="px-3 py-2 rounded-md border border-surface-300 dark:border-surface-700 flex-1" />
</section>
<section class="mb-6">
<div class="text-lg font-semibold mb-2">Foundation</div>
<span class="block text-muted-color leading-6 mb-4">Start by choosing a built-in theme as a foundation, or import your own design by uploading a tokens.json file created with the Prime UI Kit and Tokens Studio.</span>
<div class="flex flex-col">
<div class="flex flex-col gap-4 border border-surface-200 dark:border-surface-700 rounded-md p-4">
<span class="font-semibold">Base Theme</span>
<span class="text-muted-color">Variety of built-in themes with distinct characteristics.</span>
<div class="flex justify-between">
<SelectButton v-model="basePreset" :options="presetOptions" optionLabel="label" optionValue="value" :allowEmpty="false" />
<button
type="button"
@click="createThemeFromPreset"
icon="pi pi-download"
class="px-3 py-2 bg-zinc-950 hover:bg-zinc-800 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black rounded-md font-medium cursor-pointer transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-zinc-950 focus:dark:outline-white"
>
Create
</button>
</div>
</div>
<Divider>OR</Divider>
<div class="flex flex-col gap-4 border border-surface-200 dark:border-surface-700 rounded-md p-4">
<span class="font-semibold">Import Figma Tokens </span>
<span class="text-muted-color leading-6">Export the token.json file from Figma Token Studio and import to the Visual Editor.</span>
<div class="flex justify-between">
<FileUpload mode="basic" pt:root:class="!justify-start" @select="onFileSelect($event)" />
<button
type="button"
@click="createThemeFromFigma"
icon="pi pi-download"
class="px-3 py-2 bg-zinc-950 hover:bg-zinc-800 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black rounded-md font-medium cursor-pointer transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-zinc-950 focus:dark:outline-white"
>
Create
</button>
</div>
</div>
</div>
</section>
</template>
<script>
import { usePreset } from '@primevue/themes';
import Aura from '@primevue/themes/aura';
import Lara from '@primevue/themes/lara';
import Material from '@primevue/themes/material';
import Nora from '@primevue/themes/nora';
const presets = {
Aura,
Material,
Lara,
Nora
};
export default {
setup() {
const runtimeConfig = useRuntimeConfig();
return {
designerApiBase: runtimeConfig.public.designerApiBase
};
},
data() {
return {
themeName: null,
basePreset: 'Aura',
figmaData: null,
presetOptions: [
{ label: 'Aura', value: 'Aura' },
{ label: 'Material', value: 'Material' },
{ label: 'Lara', value: 'Lara' },
{ label: 'Nora', value: 'Nora' }
]
};
},
methods: {
replaceColorPalette() {
this.$appState.designer.theme.preset.semantic.primary = this.$appState.designer.theme.preset.primitive.emerald;
this.$appState.designer.theme.preset.semantic.colorScheme.light.surface = { ...{ 0: '#ffffff' }, ...this.$appState.designer.theme.preset.primitive.slate };
this.$appState.designer.theme.preset.semantic.colorScheme.dark.surface = { ...{ 0: '#ffffff' }, ...this.$appState.designer.theme.preset.primitive.zinc };
},
async createThemeFromPreset() {
const newPreset = presets[this.basePreset];
if (this.basePreset === 'Material') {
document.body.classList.add('material');
this.$primevue.config.ripple = true;
} else {
document.body.classList.remove('material');
}
const { error } = await $fetch(this.designerApiBase + '/theme/create', {
method: 'POST',
body: {
name: this.themeName,
preset: newPreset,
license_key: this.$appState.designer.licenseKey
}
});
if (error) {
this.$toast.add({ severity: 'error', summary: 'An error occured', detail: error.message, life: 3000 });
} else {
this.$appState.designer.theme = {
name: this.themeName,
preset: newPreset,
customTokens: [],
acTokens: []
};
this.replaceColorPalette();
usePreset(newPreset);
this.$appState.designer.activeView = 'editor';
}
},
onFileSelect(event) {
const file = event.files[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = (e) => {
this.figmaData = e.target.result;
};
reader.onerror = (e) => {
this.$toast.add({ severity: 'error', summary: 'An error occured', detail: 'Unable to read file', life: 3000 });
};
reader.readAsText(file);
},
async createThemeFromFigma() {
const { data: newPreset, error } = await $fetch(this.designerApiBase + '/theme/figma', {
method: 'POST',
body: {
name: this.themeName,
figma_tokens: this.figmaData,
license_key: this.$appState.designer.licenseKey
}
});
if (error) {
this.$toast.add({ severity: 'error', summary: 'An error occured', detail: error.message, life: 3000 });
} else {
this.$appState.designer.theme = {
name: this.themeName,
preset: newPreset,
customTokens: [],
acTokens: []
};
this.replaceColorPalette();
usePreset(newPreset);
this.$appState.designer.activeView = 'editor';
}
}
}
};
</script>

View File

@ -0,0 +1,236 @@
<template>
<div class="text-lg font-semibold mb-2">License Key</div>
<span class="block text-muted-color leading-6 mb-4"
>A license can be purchased from PrimeStore, if you do not have a license key, you are still able to use the Designer in free tier. <NuxtLink to="/designer" class="doc-link">Learn more</NuxtLink> about the Theme Designer.</span
>
<form @submit.prevent class="flex gap-4">
<input v-model="$appState.designer.licenseKey" type="password" autocomplete="off" class="px-3 py-2 rounded-md border border-surface-300 dark:border-surface-700 flex-1" />
<button
type="button"
@click="activate(false)"
icon="pi pi-download"
:disabled="!$appState.designer.licenseKey"
class="px-3 py-2 bg-zinc-950 hover:bg-zinc-800 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black rounded-md font-medium cursor-pointer transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-zinc-950 focus:dark:outline-white disabled:opacity-60"
>
Activate
</button>
</form>
<div class="text-lg font-semibold mb-2 mt-6">My Themes</div>
<span class="block text-muted-color leading-6 mb-4">Continue editing your existing themes or build a new one.</span>
<div class="flex flex-wrap gap-6">
<button type="button" class="rounded-xl h-36 w-36 bg-transparent border border-gray-200 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-500 text-black dark:text-white" @click="openNewTheme">
<i class="pi pi-plus !text-2xl"></i>
</button>
<template v-if="loading">
<Skeleton class="!rounded-xl !h-36 !w-36">-</Skeleton>
<Skeleton class="!rounded-xl !h-36 !w-36">-</Skeleton>
</template>
<template v-else>
<div v-for="theme of $appState.designer.themes" :key="theme.t_key" class="flex flex-col gap-2 relative">
<button
type="button"
class="rounded-xl h-36 w-36 px-4 overflow-hidden text-ellipsis bg-transparent border border-gray-200 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-500 text-black dark:text-white"
@click="loadTheme(theme)"
>
<span class="text-2xl uppercase">{{ abbrThemeName(theme) }}</span>
</button>
<div class="flex flex-col items-center gap-2">
<div class="group flex items-center gap-2 relative">
<input v-model="theme.t_name" type="text" class="w-24 text-sm px-2 py-1" maxlength="100" @blur="renameTheme(theme)" />
<i class="hidden group-hover:block pi pi-pencil !text-sm absolute top-50 right-0 text-muted-color"></i>
</div>
<span class="text-muted-color text-xs">{{ formatTimestamp(theme.last_updated) }}</span>
</div>
<button
type="button"
@click="toggleMenuOptions($event, theme)"
class="bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700 text-black dark:text-white flex absolute top-2 right-2 w-8 h-8 rounded-full items-center justify-center"
>
<i class="pi pi-ellipsis-v" />
</button>
</div>
</template>
<Menu ref="themeMenu" :model="themeOptions" :popup="true" />
<ConfirmDialog group="designer"></ConfirmDialog>
</div>
</template>
<script>
import { usePreset } from '@primevue/themes';
export default {
setup() {
const runtimeConfig = useRuntimeConfig();
return {
designerApiBase: runtimeConfig.public.designerApiBase
};
},
inject: ['designerUtils'],
data() {
return {
loading: false,
currentTheme: null,
confirmDialogVisible: false,
themeOptions: [
{
label: 'Delete',
icon: 'pi pi-times',
command: () => {
this.$confirm.require({
group: 'designer',
message: 'Are you sure you want to delete this theme?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
rejectProps: {
severity: 'secondary'
},
accept: () => {
this.deleteTheme(this.currentTheme);
}
});
}
},
{
label: 'Duplicate',
icon: 'pi pi-copy',
command: () => {
this.duplicateTheme(this.currentTheme);
}
},
{ label: 'Download', icon: 'pi pi-download' }
]
};
},
created() {
if (!this.$appState.designer.licenseKey) {
const keyValue = localStorage.getItem(this.$appState.designer.localStoreKey);
if (keyValue) {
this.$appState.designer.licenseKey = keyValue;
this.activate(true);
}
} else {
this.loadThemes();
}
},
methods: {
async activate(silent) {
this.loading = true;
const { data, error } = await $fetch(this.designerApiBase + '/license/' + this.$appState.designer.licenseKey);
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
} else {
if (data) {
if (!silent) {
this.$toast.add({ severity: 'success', summary: 'Success', detail: 'License is activated.', life: 3000 });
}
this.loadThemes();
localStorage.setItem(this.$appState.designer.localStoreKey, this.$appState.designer.licenseKey);
} else {
this.$toast.add({ severity: 'warn', summary: 'Unable to Activate', detail: 'Invalid key', life: 3000 });
this.$appState.designer.themes = [];
}
}
this.loading = false;
},
openNewTheme() {
this.$appState.designer.activeView = 'create_theme';
},
async loadThemes() {
const { data, error } = await $fetch(this.designerApiBase + '/theme/list/' + this.$appState.designer.licenseKey);
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
} else {
this.$appState.designer.themes = data;
}
},
async loadTheme(theme) {
const { data, error } = await $fetch(this.designerApiBase + '/theme/' + theme.t_key);
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: 'Code: ' + error.code, life: 3000 });
} else {
this.$appState.designer.theme = {
name: data.t_name,
preset: JSON.parse(data.preset),
customTokens: [],
acTokens: []
};
usePreset(this.$appState.designer.theme.preset);
this.designerUtils.refreshACTokens();
this.$appState.designer.activeView = 'editor';
}
},
async renameTheme(theme) {
const { error } = await $fetch(this.designerApiBase + '/theme/rename/' + theme.t_key, {
method: 'PATCH',
body: {
name: theme.t_name,
license_key: this.$appState.designer.licenseKey
}
});
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
}
},
async deleteTheme(theme) {
const { error } = await $fetch(this.designerApiBase + '/theme/delete/' + theme.t_key, {
method: 'DELETE',
body: {
license_key: this.$appState.designer.licenseKey
}
});
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
} else {
this.loadThemes();
}
},
async duplicateTheme(theme) {
const { error } = await $fetch(this.designerApiBase + '/theme/duplicate/' + theme.t_key, {
method: 'POST',
body: {
license_key: this.$appState.designer.licenseKey
}
});
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
} else {
this.loadThemes();
}
},
formatTimestamp(timestamp) {
const date = new Date(timestamp);
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
};
return date.toLocaleString('en-US', options);
},
toggleMenuOptions(event, theme) {
this.currentTheme = theme;
this.$refs.themeMenu.toggle(event);
},
abbrThemeName(theme) {
return theme.t_name ? theme.t_name.substring(0, 2) : 'U';
}
}
};
</script>

View File

@ -0,0 +1,96 @@
<template>
<Tabs v-model:value="$appState.designer.activeTab" :lazy="deferred">
<TabList>
<Tab value="0">Primitive</Tab>
<Tab value="1">Semantic</Tab>
<Tab value="2" :disabled="!isComponentRoute">Component</Tab>
<Tab value="3">Custom</Tab>
</TabList>
<TabPanels class="!px-0">
<TabPanel value="0">
<div>
<form @keydown="onKeyDown" class="flex flex-col gap-3">
<DesignBorderRadius />
<DesignColors />
</form>
</div>
</TabPanel>
<TabPanel value="1">
<Accordion :value="['0', '1']" multiple>
<AccordionPanel value="0">
<AccordionHeader>Common</AccordionHeader>
<AccordionContent>
<div>
<form @keydown="onKeyDown" class="flex flex-col gap-3">
<DesignGeneral />
<DesignFormField />
<DesignList />
<DesignNavigation />
<DesignOverlay />
</form>
</div>
</AccordionContent>
</AccordionPanel>
<AccordionPanel value="1">
<AccordionHeader>Color Scheme</AccordionHeader>
<AccordionContent>
<Tabs value="cs-0">
<TabList>
<Tab value="cs-0">Light</Tab>
<Tab value="cs-1">Dark</Tab>
</TabList>
<TabPanels class="!px-0">
<TabPanel value="cs-0">
<form @keydown="onKeyDown">
<DesignCS :value="$appState.designer.theme.preset.semantic.colorScheme.light" />
</form>
</TabPanel>
<TabPanel value="cs-1">
<form @keydown="onKeyDown">
<DesignCS :value="$appState.designer.theme.preset.semantic.colorScheme.dark" />
</form>
</TabPanel>
</TabPanels>
</Tabs>
</AccordionContent>
</AccordionPanel>
</Accordion>
</TabPanel>
<TabPanel value="2">
<form @keydown="onKeyDown">
<DesignComponent />
</form>
</TabPanel>
<TabPanel value="3">
<DesignCustomTokens />
</TabPanel>
</TabPanels>
</Tabs>
</template>
<script>
export default {
props: {
deferred: {
type: Boolean,
default: true
}
},
methods: {
onKeyDown(event) {
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
this.apply();
event.preventDefault();
}
}
},
computed: {
isComponentRoute() {
const components = this.$appState.designer.theme?.preset?.components;
return components ? components[this.$route.name] != null : false;
}
}
};
</script>

View File

@ -1,6 +1,6 @@
<template>
<div>
<label :for="inputId" class="text-sm text-zinc-700 dark:text-white capitalize">{{ label }}</label>
<label :for="inputId" class="text-sm text-zinc-700 dark:text-white block capitalize text-ellipsis overflow-hidden w-full whitespace-nowrap" :title="label">{{ label }}</label>
<div :id="id" class="relative">
<AutoComplete
:modelValue="modelValue"

View File

@ -1,5 +1,5 @@
<template>
<section class="flex flex-col gap-3">
<section v-if="isComponentRoute" class="flex flex-col gap-3">
<div class="text-lg font-semibold capitalize">{{ componentKey }}</div>
<template v-for="(value, name) in tokens" :key="name">
<DesignComponentSection v-if="name !== 'colorScheme'" :componentKey="componentKey" :path="name" />
@ -27,19 +27,23 @@
<script>
export default {
inject: ['$preset'],
computed: {
componentKey() {
return this.$route.name;
},
isComponentRoute() {
const components = this.$appState.designer.theme?.preset?.components;
return components ? components[this.componentKey] != null : false;
},
tokens() {
return this.$preset.components[this.componentKey];
return this.$appState.designer.theme?.preset?.components[this.componentKey];
},
lightTokens() {
return this.$preset.components[this.componentKey].colorScheme.light;
return this.$appState.designer.theme?.preset?.components[this.componentKey].colorScheme.light;
},
darkTokens() {
return this.$preset.components[this.componentKey].colorScheme.dark;
return this.$appState.designer.theme?.preset?.components[this.componentKey].colorScheme.dark;
},
hasColorScheme() {
return this.tokens.colorScheme != undefined;

View File

@ -24,7 +24,6 @@ export default {
default: null
}
},
inject: ['$preset'],
methods: {
camelCaseToSpaces(val) {
return val.replace(/([a-z])([A-Z])/g, '$1 $2');
@ -67,7 +66,7 @@ export default {
.join(' ');
},
tokens() {
return this.getObjectProperty(this.$preset.components[this.componentKey], this.path);
return this.getObjectProperty(this.$appState.designer.theme.preset.components[this.componentKey], this.path);
},
nestedTokens() {
const groups = {};

View File

@ -0,0 +1,80 @@
<template>
<span class="leading-6 text-muted-color">Extend the theming system with your own design tokens e.g. <span class="font-medium">accent.color</span>. Do not use curly braces in the name field.</span>
<ul class="flex flex-col gap-4 list-none p-0 mx-0 my-4">
<li v-for="(token, index) of $appState.designer.customTokens" :key="index" class="first:border-t border-b border-surface-200 dark:border-surface-300 py-2">
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 flex-auto">
<span class="text-sm">Name</span>
<input v-model="token['name']" type="text" class="border border-surface-300 dark:border-surface-600 rounded-lg py-2 px-2 w-full" />
</label>
<label class="flex items-center gap-2 flex-auto">
<span class="text-sm">Value</span>
<input v-model="token['value']" type="text" class="border border-surface-300 dark:border-surface-600 rounded-lg py-2 px-2 w-full" />
</label>
<button
type="button"
@click="removeToken(index)"
class="cursor-pointer inline-flex items-center justify-center ms-auto w-8 h-8 rounded-full bg-red-50 hover:bg-red-100 text-red-600 dark:bg-red-400/10 dark:hover:bg-red-400/20 dark:text-red-400 transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-red-600 focus:dark:outline-red-400"
>
<i class="pi pi-times" />
</button>
</div>
</li>
</ul>
<div class="flex justify-between">
<button
type="button"
@click="addToken"
class="px-3 py-2 bg-zinc-950 hover:bg-zinc-800 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black rounded-md font-medium cursor-pointer transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-zinc-950 focus:dark:outline-white"
>
Add New
</button>
<button
v-if="$appState.designer.theme.customTokens.length"
type="button"
@click="saveTokens"
class="px-3 py-2 bg-zinc-950 hover:bg-zinc-800 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black rounded-md font-medium cursor-pointer transition-colors duration-200 focus:outline focus:outline-offset-2 focus:outline-zinc-950 focus:dark:outline-white"
>
Save
</button>
</div>
</template>
<script>
export default {
inject: ['designer'],
methods: {
addToken() {
this.$appState.designer.theme.customTokens = [...this.$appState.designer.theme.customTokens, ...[{}]];
},
removeToken(index) {
this.$appState.designer.theme.customTokens.splice(index, 1);
},
saveTokens() {
this.$appState.designer.theme.preset.extend = {};
this.$appState.designer.theme.customTokens.forEach((token) => {
this.$appState.designer.theme.preset.extend[this.transformTokenName(token.name)] = token.value;
});
designer.refreshACTokens();
designer.saveTheme();
this.$toast.add({ severity: 'success', summary: 'Success', detail: 'Tokens saved', life: 3000 });
},
transformTokenName(name) {
if (name && name.trim().length) {
let tokenNameSections = name.split('.');
let transformedName = '';
tokenNameSections.forEach((section, index) => {
transformedName += index === 0 ? section : section.charAt(0).toUpperCase() + section.substring(1);
});
return transformedName;
} else {
return name;
}
}
}
};
</script>

View File

@ -0,0 +1,28 @@
<template>
<Fieldset legend="Rounded" :toggleable="true">
<section class="grid grid-cols-4 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.primitive.borderRadius.none" label="None" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.primitive.borderRadius.xs" label="Extra Small" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.primitive.borderRadius.sm" label="Small" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.primitive.borderRadius.md" label="Medium" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.primitive.borderRadius.lg" label="Large" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.primitive.borderRadius.xl" label="Extra Large" />
</div>
</section>
</Fieldset>
</template>
<script>
export default {};
</script>

View File

@ -1,12 +1,12 @@
<template>
<Fieldset legend="Colors" :toggleable="true">
<template v-for="(value, key) of $preset.primitive" :key="key">
<template v-for="(value, key) of $appState.designer.theme.preset.primitive" :key="key">
<section v-if="key !== 'borderRadius'" class="flex justify-between items-center mb-4">
<div class="flex gap-2 items-center">
<span class="text-sm capitalize block w-20">{{ key }}</span>
<input :value="$preset.primitive[key]['500']" @input="onColorChange($event, key)" type="color" />
<input :value="$appState.designer.theme.preset.primitive[key]['500']" @input="onColorChange($event, key)" type="color" />
</div>
<DesignColorPalette :value="$preset.primitive[key]" />
<DesignColorPalette :value="$appState.designer.theme.preset.primitive[key]" />
</section>
</template>
</Fieldset>
@ -16,10 +16,9 @@
import { palette } from '@primevue/themes';
export default {
inject: ['$preset'],
methods: {
onColorChange(event, color) {
this.$preset.primitive[color] = palette(event.target.value);
this.$appState.designer.theme.preset.primitive[color] = palette(event.target.value);
}
}
};

View File

@ -0,0 +1,67 @@
<template>
<Fieldset legend="Form Field" :toggleable="true">
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Base</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.paddingX" label="Padding X" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.paddingY" label="Padding Y" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.borderRadius" label="Border Radius" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.transitionDuration" label="Transition Duration" />
</div>
</section>
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Small</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.sm.paddingX" label="Padding X" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.sm.paddingY" label="Padding Y" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.sm.fontSize" label="Font Size" />
</div>
<div></div>
</section>
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Large</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.lg.paddingX" label="Padding X" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.lg.paddingY" label="Padding Y" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.lg.fontSize" label="Font Size" />
</div>
<div></div>
</section>
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Focus Ring</div>
<section class="grid grid-cols-4 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.focusRing.width" label="Width" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.focusRing.style" label="Style" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.focusRing.color" label="Color" type="color" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.formField.focusRing.offset" label="Offset" />
</div>
</section>
</Fieldset>
</template>
<script>
export default {};
</script>

View File

@ -0,0 +1,61 @@
<template>
<Fieldset legend="General" :toggleable="true">
<section class="flex justify-between items-center mb-4">
<div class="flex gap-2 items-center">
<span class="text-sm">Primary</span>
<input :value="$appState.designer.theme.preset.semantic.primary['500']" @input="onPrimaryColorChange($event)" type="color" />
</div>
<DesignColorPalette :value="$appState.designer.theme.preset.semantic.primary" />
</section>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.transitionDuration" label="Transition Duration" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.disabledOpacity" label="Disabled Opacity" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.iconSize" label="Icon Size" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.anchorGutter" label="Anchor Gutter" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.content.borderRadius" label="Border Radius" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.mask.transitionDuration" label="Mask Transition Dur." />
</div>
<div class="flex flex-col gap-1"></div>
<div class="flex flex-col gap-1"></div>
</section>
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Focus Ring</div>
<section class="grid grid-cols-4 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.focusRing.width" label="Width" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.focusRing.style" label="Style" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.focusRing.color" label="Color" type="color" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.focusRing.offset" label="Offset" />
</div>
</section>
</Fieldset>
</template>
<script>
import { palette } from '@primevue/themes';
export default {
methods: {
onPrimaryColorChange(event) {
this.$appState.designer.theme.preset.semantic.primary = palette(event.target.value);
}
}
};
</script>

View File

@ -3,13 +3,13 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Container</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.list.padding" label="Padding" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.list.padding" label="Padding" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.list.gap" label="Gap" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.list.gap" label="Gap" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.list.header.padding" label="Header Padding" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.list.header.padding" label="Header Padding" />
</div>
<div class="flex flex-col gap-1"></div>
</section>
@ -17,27 +17,25 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Option</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.list.option.padding" label="Padding" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.list.option.padding" label="Padding" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.list.option.borderRadius" label="Border Radius" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.list.option.borderRadius" label="Border Radius" />
</div>
</section>
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Option Group</div>
<section class="grid grid-cols-4 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.list.optionGroup.padding" label="Padding" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.list.optionGroup.padding" label="Padding" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.list.optionGroup.fontWeight" label="Font Weight" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.list.optionGroup.fontWeight" label="Font Weight" />
</div>
</section>
</Fieldset>
</template>
<script>
export default {
inject: ['$preset']
};
export default {};
</script>

View File

@ -3,10 +3,10 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">List</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.navigation.list.padding" label="Padding" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.navigation.list.padding" label="Padding" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.navigation.list.gap" label="Gap" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.navigation.list.gap" label="Gap" />
</div>
<div class="flex flex-col gap-1"></div>
<div class="flex flex-col gap-1"></div>
@ -15,13 +15,13 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Item</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.navigation.item.padding" label="Padding" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.navigation.item.padding" label="Padding" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.navigation.item.borderRadius" label="Border Radius" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.navigation.item.borderRadius" label="Border Radius" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.navigation.item.gap" label="Gap" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.navigation.item.gap" label="Gap" />
</div>
<div class="flex flex-col gap-1"></div>
</section>
@ -29,10 +29,10 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Submenu Label</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.navigation.submenuLabel.padding" label="Padding" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.navigation.submenuLabel.padding" label="Padding" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.navigation.submenuLabel.fontWeight" label="Font Weight" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.navigation.submenuLabel.fontWeight" label="Font Weight" />
</div>
<div class="flex flex-col gap-1"></div>
<div class="flex flex-col gap-1"></div>
@ -41,7 +41,7 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Submenu Icon</div>
<section class="grid grid-cols-4 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.navigation.submenuIcon.size" label="Size" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.navigation.submenuIcon.size" label="Size" />
</div>
<div class="flex flex-col gap-1"></div>
<div class="flex flex-col gap-1"></div>
@ -51,7 +51,5 @@
</template>
<script>
export default {
inject: ['$preset']
};
export default {};
</script>

View File

@ -3,10 +3,10 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Select</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.overlay.select.borderRadius" label="Border Radius" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.overlay.select.borderRadius" label="Border Radius" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.overlay.select.shadow" label="Shadow" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.overlay.select.shadow" label="Shadow" />
</div>
<div class="flex flex-col gap-1"></div>
<div class="flex flex-col gap-1"></div>
@ -15,13 +15,13 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Popover</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.overlay.popover.borderRadius" label="Border Radius" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.overlay.popover.borderRadius" label="Border Radius" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.overlay.popover.padding" label="Padding" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.overlay.popover.padding" label="Padding" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.overlay.popover.shadow" label="Shadow" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.overlay.popover.shadow" label="Shadow" />
</div>
<div class="flex flex-col gap-1"></div>
</section>
@ -29,13 +29,13 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Modal</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.overlay.modal.borderRadius" label="Border Radius" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.overlay.modal.borderRadius" label="Border Radius" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.overlay.modal.padding" label="Padding" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.overlay.modal.padding" label="Padding" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.overlay.modal.shadow" label="Shadow" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.overlay.modal.shadow" label="Shadow" />
</div>
<div class="flex flex-col gap-1"></div>
</section>
@ -43,7 +43,7 @@
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Navigation</div>
<section class="grid grid-cols-4 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.overlay.navigation.shadow" label="Shadow" />
<DesignTokenField v-model="$appState.designer.theme.preset.semantic.overlay.navigation.shadow" label="Shadow" />
</div>
<div class="flex flex-col gap-1"></div>
<div class="flex flex-col gap-1"></div>
@ -53,7 +53,5 @@
</template>
<script>
export default {
inject: ['$preset']
};
export default {};
</script>

View File

@ -1,30 +0,0 @@
<template>
<Fieldset legend="Rounded" :toggleable="true">
<section class="grid grid-cols-4 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.primitive.borderRadius.none" label="None" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.primitive.borderRadius.xs" label="Extra Small" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.primitive.borderRadius.sm" label="Small" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.primitive.borderRadius.md" label="Medium" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.primitive.borderRadius.lg" label="Large" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.primitive.borderRadius.xl" label="Extra Large" />
</div>
</section>
</Fieldset>
</template>
<script>
export default {
inject: ['$preset']
};
</script>

View File

@ -1,69 +0,0 @@
<template>
<Fieldset legend="Form Field" :toggleable="true">
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Base</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.paddingX" label="Padding X" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.paddingY" label="Padding Y" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.borderRadius" label="Border Radius" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.transitionDuration" label="Transition Duration" />
</div>
</section>
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Small</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.sm.paddingX" label="Padding X" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.sm.paddingY" label="Padding Y" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.sm.fontSize" label="Font Size" />
</div>
<div></div>
</section>
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Large</div>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.lg.paddingX" label="Padding X" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.lg.paddingY" label="Padding Y" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.lg.fontSize" label="Font Size" />
</div>
<div></div>
</section>
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Focus Ring</div>
<section class="grid grid-cols-4 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.focusRing.width" label="Width" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.focusRing.style" label="Style" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.focusRing.color" label="Color" type="color" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.formField.focusRing.offset" label="Offset" />
</div>
</section>
</Fieldset>
</template>
<script>
export default {
inject: ['$preset']
};
</script>

View File

@ -1,62 +0,0 @@
<template>
<Fieldset legend="General" :toggleable="true">
<section class="flex justify-between items-center mb-4">
<div class="flex gap-2 items-center">
<span class="text-sm">Primary</span>
<input :value="$preset.semantic.primary['500']" @input="onPrimaryColorChange($event)" type="color" />
</div>
<DesignColorPalette :value="$preset.semantic.primary" />
</section>
<section class="grid grid-cols-4 mb-3 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.transitionDuration" label="Transition Duration" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.disabledOpacity" label="Disabled Opacity" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.iconSize" label="Icon Size" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.anchorGutter" label="Anchor Gutter" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.content.borderRadius" label="Border Radius" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.mask.transitionDuration" label="Mask Transition Dur." />
</div>
<div class="flex flex-col gap-1"></div>
<div class="flex flex-col gap-1"></div>
</section>
<div class="text-sm mb-1 font-semibold text-surface-950 dark:text-surface-0">Focus Ring</div>
<section class="grid grid-cols-4 gap-2">
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.focusRing.width" label="Width" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.focusRing.style" label="Style" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.focusRing.color" label="Color" type="color" />
</div>
<div class="flex flex-col gap-1">
<DesignTokenField v-model="$preset.semantic.focusRing.offset" label="Offset" />
</div>
</section>
</Fieldset>
</template>
<script>
import { palette } from '@primevue/themes';
export default {
inject: ['$preset'],
methods: {
onPrimaryColorChange(event) {
this.$preset.semantic.primary = palette(event.target.value);
}
}
};
</script>

View File

@ -97,7 +97,8 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
contextPath: baseUrl,
DEV_ENV: PROCESS_ENV.DEV_ENV
DEV_ENV: PROCESS_ENV.DEV_ENV,
designerApiBase: ''
}
},
css: ['primeicons/primeicons.css', '@/assets/styles/flags.css', '@docsearch/css/dist/style.css', '@/assets/styles/tailwind/main.css', '@/assets/styles/layout/layout.scss']

View File

@ -15,12 +15,18 @@ const $appState = {
announcement: null,
storageKey: 'primevue',
designer: {
key: 'primevue-designer-theme',
localStoreKey: 'primevue-designer-licensekey',
licenseKey: null,
active: false,
activeView: 'dashboard',
activeTab: '0',
preset: null,
customTokens: [],
acTokens: []
theme: {
name: null,
preset: null,
customTokens: [],
acTokens: []
},
themes: []
}
});
}