249 lines
9.6 KiB
Vue
249 lines
9.6 KiB
Vue
<!--
|
|
|--------------------------------------------------------------------------
|
|
| Agência PSI
|
|
|--------------------------------------------------------------------------
|
|
| Criado e desenvolvido por Leonardo Nohama
|
|
|
|
|
| Tecnologia aplicada à escuta.
|
|
| Estrutura para o cuidado.
|
|
|
|
|
| Arquivo: src/layout/AppConfigurator.vue
|
|
| Data: 2026
|
|
| Local: São Carlos/SP — Brasil
|
|
|--------------------------------------------------------------------------
|
|
| © 2026 — Todos os direitos reservados
|
|
|--------------------------------------------------------------------------
|
|
-->
|
|
<script setup>
|
|
import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue';
|
|
import { useLayout } from '@/layout/composables/layout';
|
|
import { useConfiguratorBar } from '@/layout/composables/useConfiguratorBar';
|
|
import { primaryColors, surfaces, presetOptions, applyThemeEngine } from '@/theme/theme.options';
|
|
|
|
const { layoutConfig, isDarkTheme, changeMenuMode, setVariant, setRailOpenMode } = useLayout();
|
|
const { open, close } = useConfiguratorBar();
|
|
|
|
// ── Fechar ao clicar fora ────────────────────────────────────
|
|
const panelEl = ref(null);
|
|
function onDocClick(e) {
|
|
if (!open.value) return;
|
|
if (!panelEl.value?.contains(e.target)) close();
|
|
}
|
|
onMounted(() => document.addEventListener('mousedown', onDocClick, true));
|
|
onBeforeUnmount(() => document.removeEventListener('mousedown', onDocClick, true));
|
|
|
|
// ✅ vem do AppTopbar (mesma instância)
|
|
const queuePatch = inject('queueUserSettingsPatch', null);
|
|
|
|
// menu mode options
|
|
const menuModeOptions = [
|
|
{ label: 'Static', value: 'static' },
|
|
{ label: 'Overlay', value: 'overlay' }
|
|
];
|
|
|
|
// ✅ v-model sincronizado (sem state local)
|
|
const presetModel = computed({
|
|
get: () => layoutConfig.preset,
|
|
set: (val) => {
|
|
if (!val || val === layoutConfig.preset) return;
|
|
layoutConfig.preset = val;
|
|
|
|
applyThemeEngine(layoutConfig);
|
|
queuePatch?.({ preset: val });
|
|
saveThemeToStorage();
|
|
}
|
|
});
|
|
|
|
const menuModeModel = computed({
|
|
get: () => layoutConfig.menuMode,
|
|
set: (val) => {
|
|
if (!val || val === layoutConfig.menuMode) return;
|
|
layoutConfig.menuMode = val;
|
|
|
|
// ✅ changeMenuMode espera event.value (seu composable usa event.value)
|
|
try {
|
|
changeMenuMode({ value: val });
|
|
} catch {}
|
|
|
|
queuePatch?.({ menu_mode: val });
|
|
}
|
|
});
|
|
|
|
function saveThemeToStorage() {
|
|
try {
|
|
localStorage.setItem('ui_theme_config', JSON.stringify({
|
|
preset: layoutConfig.preset,
|
|
primary: layoutConfig.primary,
|
|
surface: layoutConfig.surface,
|
|
menuMode: layoutConfig.menuMode
|
|
}));
|
|
} catch {}
|
|
}
|
|
|
|
function updateColors(type, item) {
|
|
if (type === 'primary') {
|
|
layoutConfig.primary = item.name;
|
|
applyThemeEngine(layoutConfig);
|
|
queuePatch?.({ primary_color: item.name });
|
|
saveThemeToStorage();
|
|
return;
|
|
}
|
|
|
|
if (type === 'surface') {
|
|
layoutConfig.surface = item.name;
|
|
applyThemeEngine(layoutConfig);
|
|
queuePatch?.({ surface_color: item.name });
|
|
saveThemeToStorage();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="open"
|
|
ref="panelEl"
|
|
class="config-panel fixed bottom-18 left-18 w-64 p-4 bg-surface-0 dark:bg-surface-900 border border-surface rounded-border origin-bottom-left shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)] z-2000"
|
|
>
|
|
<div class="flex flex-col gap-4">
|
|
<div>
|
|
<span class="text-sm text-muted-color font-semibold">Primary</span>
|
|
<div class="pt-2 flex gap-2 flex-wrap justify-between">
|
|
<button
|
|
v-for="c of primaryColors"
|
|
:key="c.name"
|
|
type="button"
|
|
:title="c.name"
|
|
@click="updateColors('primary', c)"
|
|
:class="['border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1', { 'outline-primary': layoutConfig.primary === c.name }]"
|
|
:style="{ backgroundColor: `${c.name === 'noir' ? 'var(--text-color)' : c.palette['500']}` }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-sm text-muted-color font-semibold">Surface</span>
|
|
<div class="pt-2 flex gap-2 flex-wrap justify-between">
|
|
<button
|
|
v-for="s of surfaces"
|
|
:key="s.name"
|
|
type="button"
|
|
:title="s.name"
|
|
@click="updateColors('surface', s)"
|
|
:class="[
|
|
'border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1',
|
|
{ 'outline-primary': layoutConfig.surface ? layoutConfig.surface === s.name : isDarkTheme ? s.name === 'zinc' : s.name === 'slate' }
|
|
]"
|
|
:style="{ backgroundColor: `${s.palette['500']}` }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-sm text-muted-color font-semibold">Presets</span>
|
|
<SelectButton v-model="presetModel" :options="presetOptions" :allowEmpty="false" />
|
|
</div>
|
|
|
|
<!-- Menu Mode: visível apenas no Layout Clássico -->
|
|
<div v-show="layoutConfig.variant === 'classic'" class="flex flex-col gap-2">
|
|
<span class="text-sm text-muted-color font-semibold">Menu Mode</span>
|
|
<SelectButton v-model="menuModeModel" :options="menuModeOptions" :allowEmpty="false" optionLabel="label" optionValue="value" />
|
|
</div>
|
|
|
|
<!-- Abrir Menu: visível apenas no Layout Rail -->
|
|
<div v-show="layoutConfig.variant === 'rail'" class="flex flex-col gap-1">
|
|
<span class="text-sm text-muted-color font-semibold">Abrir Menu</span>
|
|
<div class="flex flex-col gap-1">
|
|
<button type="button" class="layout-option" :class="{ 'layout-option--active': layoutConfig.railOpenMode === 'hover' }" @click="setRailOpenMode('hover')">
|
|
<i :class="layoutConfig.railOpenMode === 'hover' ? 'pi pi-check-circle' : 'pi pi-circle'" class="layout-option__icon" />
|
|
<span class="layout-option__label">Mouse Hover</span>
|
|
<span v-if="layoutConfig.railOpenMode === 'hover'" class="layout-option__badge layout-option__badge--default">Padrão</span>
|
|
</button>
|
|
<button type="button" class="layout-option" :class="{ 'layout-option--active': layoutConfig.railOpenMode === 'click' }" @click="setRailOpenMode('click')">
|
|
<i :class="layoutConfig.railOpenMode === 'click' ? 'pi pi-check-circle' : 'pi pi-circle'" class="layout-option__icon" />
|
|
<span class="layout-option__label">Com Clique</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-sm text-muted-color font-semibold">Layout</span>
|
|
<div class="flex flex-col gap-1">
|
|
<!-- Layout Rail -->
|
|
<button type="button" class="layout-option" :class="{ 'layout-option--active': layoutConfig.variant === 'rail' }" @click="setVariant('rail')">
|
|
<i :class="layoutConfig.variant === 'rail' ? 'pi pi-check-circle' : 'pi pi-circle'" class="layout-option__icon" />
|
|
<span class="layout-option__label">Layout Rail</span>
|
|
<span v-if="layoutConfig.variant === 'rail'" class="layout-option__badge layout-option__badge--default">Padrão</span>
|
|
</button>
|
|
|
|
<!-- Layout Clássico -->
|
|
<button type="button" class="layout-option" :class="{ 'layout-option--active': layoutConfig.variant === 'classic' }" @click="setVariant('classic')">
|
|
<i :class="layoutConfig.variant === 'classic' ? 'pi pi-check-circle' : 'pi pi-circle'" class="layout-option__icon" />
|
|
<span class="layout-option__label">Layout Clássico</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.layout-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.45rem 0.6rem;
|
|
border-radius: var(--border-radius, 6px);
|
|
border: 1px solid var(--surface-border);
|
|
background: var(--surface-ground);
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition:
|
|
border-color 0.15s,
|
|
background 0.15s;
|
|
width: 100%;
|
|
}
|
|
|
|
.layout-option:hover {
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.layout-option--active {
|
|
border-color: var(--primary-color);
|
|
background: color-mix(in srgb, var(--primary-color) 8%, transparent);
|
|
}
|
|
|
|
.layout-option__icon {
|
|
font-size: 0.85rem;
|
|
color: var(--text-color-secondary);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.layout-option--active .layout-option__icon {
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.layout-option__label {
|
|
flex: 1;
|
|
font-weight: 500;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.layout-option__badge {
|
|
font-size: 0.6rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.01em;
|
|
padding: 0.1rem 0.45rem;
|
|
border-radius: 999px;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.layout-option__badge--default {
|
|
background: var(--primary-color);
|
|
color: var(--primary-color-text, #fff);
|
|
}
|
|
</style>
|