Files
agenciapsilmno/src/layout/AppThemeBar.vue
T

366 lines
13 KiB
Vue

<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/AppThemeBar.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted } from 'vue';
import { useLayout } from '@/layout/composables/layout';
import { useConfiguratorBar } from '@/layout/composables/useConfiguratorBar';
import { primaryColors, surfaces, presetOptions, applyThemeEngine } from '@/theme/theme.options';
import { useUserSettingsPersistence } from '@/composables/useUserSettingsPersistence';
const { layoutConfig, isDarkTheme, changeMenuMode, setVariant, setRailOpenMode, layoutState, isMobile, effectiveVariant, effectiveMenuMode, railPanelPushesLayout } = useLayout();
const { close } = useConfiguratorBar();
const { init: initSettings, queuePatch } = useUserSettingsPersistence();
onMounted(() => initSettings());
/* ── Offset esquerdo dinâmico ──────────────────────────────
Rail desktop : 60px (rail) + 260px (painel, se aberto)
Clássico static ativo: 20rem (sidebar)
Demais casos : 0px
───────────────────────────────────────────────────────────── */
const leftOffset = computed(() => {
if (isMobile.value) return '0px';
if (effectiveVariant.value === 'rail') {
const panelW = railPanelPushesLayout.value ? 260 : 0;
return `${60 + panelW}px`;
}
// Clássico
if (effectiveMenuMode.value === 'overlay' || layoutState.staticMenuInactive) return '0px';
return '20rem';
});
const barStyle = computed(() => ({ left: leftOffset.value }));
/* ── Cores e presets ────────────────────────────────────── */
const menuModeOptions = [
{ label: 'Static', value: 'static' },
{ label: 'Overlay', value: 'overlay' }
];
const presetModel = computed({
get: () => layoutConfig.preset,
set: (v) => {
layoutConfig.preset = v;
applyThemeEngine(layoutConfig);
queuePatch?.({ preset: v });
saveThemeToStorage();
}
});
const menuModeModel = computed({
get: () => layoutConfig.menuMode,
set: (v) => {
changeMenuMode(v);
queuePatch?.({ menu_mode: v });
}
});
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();
}
if (type === 'surface') {
layoutConfig.surface = item.name;
applyThemeEngine(layoutConfig);
queuePatch?.({ surface_color: item.name });
saveThemeToStorage();
}
}
function handleSetVariant(v) {
setVariant(v);
queuePatch?.({ layout_variant: v });
}
</script>
<template>
<div class="theme-bar" :style="barStyle">
<div class="theme-bar__inner">
<!-- Cor principal -->
<div class="theme-bar__section">
<span class="theme-bar__label">Cor principal</span>
<div class="theme-bar__swatches">
<button
v-for="c of primaryColors"
:key="c.name"
type="button"
:title="c.name"
@click="updateColors('primary', c)"
:class="['swatch', { 'swatch--active': layoutConfig.primary === c.name }]"
:style="{ backgroundColor: c.name === 'noir' ? 'var(--text-color)' : c.palette['500'] }"
/>
</div>
</div>
<div class="theme-bar__divider" />
<!-- Surface -->
<div class="theme-bar__section">
<span class="theme-bar__label">Surface</span>
<div class="theme-bar__swatches">
<button
v-for="s of surfaces"
:key="s.name"
type="button"
:title="s.name"
@click="updateColors('surface', s)"
:class="[
'swatch',
{
'swatch--active': layoutConfig.surface
? layoutConfig.surface === s.name
: isDarkTheme
? s.name === 'zinc'
: s.name === 'slate'
}
]"
:style="{ backgroundColor: s.palette['500'] }"
/>
</div>
</div>
<div class="theme-bar__divider" />
<!-- Preset -->
<div class="theme-bar__section">
<span class="theme-bar__label">Preset</span>
<SelectButton v-model="presetModel" :options="presetOptions" :allowEmpty="false" size="small" />
</div>
<!-- Menu mode (somente layout clássico) -->
<template v-if="effectiveVariant === 'classic'">
<div class="theme-bar__divider" />
<div class="theme-bar__section">
<span class="theme-bar__label">Menu</span>
<SelectButton v-model="menuModeModel" :options="menuModeOptions" :allowEmpty="false" optionLabel="label" optionValue="value" size="small" />
</div>
</template>
<!-- Abrir Menu (somente layout rail) -->
<template v-if="effectiveVariant === 'rail'">
<div class="theme-bar__divider" />
<div class="theme-bar__section">
<span class="theme-bar__label">Abrir Menu</span>
<div class="flex gap-2">
<button
type="button"
class="variant-btn"
:class="{ 'variant-btn--active': layoutConfig.railOpenMode === 'hover' }"
@click="setRailOpenMode('hover')"
>
<i :class="layoutConfig.railOpenMode === 'hover' ? 'pi pi-check-circle' : 'pi pi-circle'" />
Hover
</button>
<button
type="button"
class="variant-btn"
:class="{ 'variant-btn--active': layoutConfig.railOpenMode === 'click' }"
@click="setRailOpenMode('click')"
>
<i :class="layoutConfig.railOpenMode === 'click' ? 'pi pi-check-circle' : 'pi pi-circle'" />
Clique
</button>
</div>
</div>
</template>
<div class="theme-bar__divider" />
<!-- Layout variant -->
<div class="theme-bar__section">
<span class="theme-bar__label">Layout</span>
<div class="flex gap-2">
<button
type="button"
class="variant-btn"
:class="{ 'variant-btn--active': layoutConfig.variant === 'rail' }"
@click="handleSetVariant('rail')"
>
<i :class="layoutConfig.variant === 'rail' ? 'pi pi-check-circle' : 'pi pi-circle'" />
Rail
</button>
<button
type="button"
class="variant-btn"
:class="{ 'variant-btn--active': layoutConfig.variant === 'classic' }"
@click="handleSetVariant('classic')"
>
<i :class="layoutConfig.variant === 'classic' ? 'pi pi-check-circle' : 'pi pi-circle'" />
Clássico
</button>
</div>
</div>
<!-- Fechar -->
<button type="button" class="theme-bar__close" title="Fechar painel de tema" @click="close">
<i class="pi pi-times" />
</button>
</div>
</div>
</template>
<style scoped>
.theme-bar {
position: fixed;
bottom: 0;
right: 0;
background: var(--surface-card);
border-top: 1px solid var(--surface-border);
box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.08);
z-index: 200;
padding: 0.6rem 1rem;
/* left acompanha o painel rail; transform é o slide-up/down de abrir/fechar */
transition:
left 0.22s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.theme-bar__inner {
display: flex;
align-items: center;
gap: 0;
flex-wrap: wrap;
row-gap: 0.5rem;
min-height: 52px;
}
.theme-bar__section {
display: flex;
flex-direction: column;
gap: 0.3rem;
padding: 0 0.85rem;
}
.theme-bar__label {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-color-secondary);
white-space: nowrap;
}
.theme-bar__swatches {
display: flex;
gap: 0.28rem;
flex-wrap: wrap;
max-width: 220px;
}
.theme-bar__divider {
width: 1px;
height: 44px;
background: var(--surface-border);
flex-shrink: 0;
align-self: center;
}
/* ── Swatches ─────────────────────────────────────────── */
.swatch {
width: 1.05rem;
height: 1.05rem;
border-radius: 50%;
border: none;
padding: 0;
cursor: pointer;
outline: none;
outline-offset: 2px;
transition: transform 0.12s;
flex-shrink: 0;
}
.swatch:hover {
transform: scale(1.2);
}
.swatch--active {
outline: 2px solid var(--primary-color);
}
/* ── Variant buttons ──────────────────────────────────── */
.variant-btn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.28rem 0.6rem;
border-radius: 6px;
border: 1px solid var(--surface-border);
background: var(--surface-ground);
color: var(--text-color-secondary);
font-size: 0.78rem;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.variant-btn:hover {
color: var(--text-color);
border-color: color-mix(in srgb, var(--primary-color) 60%, transparent);
}
.variant-btn--active {
background: color-mix(in srgb, var(--primary-color) 10%, transparent);
color: var(--primary-color);
border-color: var(--primary-color);
}
/* ── Fechar ───────────────────────────────────────────── */
.theme-bar__close {
margin-left: auto;
padding-left: 0.85rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
border: none;
background: transparent;
color: var(--text-color-secondary);
display: grid;
place-items: center;
cursor: pointer;
transition: all 0.15s;
flex-shrink: 0;
}
.theme-bar__close:hover {
background: var(--surface-ground);
color: var(--text-color);
}
/* ── Mobile: wrap, sem dividers verticais ─────────────── */
@media (max-width: 768px) {
.theme-bar__divider {
display: none;
}
.theme-bar__section {
padding: 0 0.5rem;
}
.theme-bar__swatches {
max-width: 100%;
}
}
</style>