Files
agenciapsilmno/src/layout/AppConfigurator.vue
T

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>