Layout 100%, Notificações, SetupWizard

This commit is contained in:
Leonardo
2026-03-17 21:08:14 -03:00
parent 84d65e49c0
commit 66f67cd40f
77 changed files with 35823 additions and 15023 deletions
+116 -184
View File
@@ -233,56 +233,53 @@ onBeforeUnmount(() => {
<Toast />
<ConfirmDialog />
<div class="features-root">
<!-- Sentinel -->
<div ref="heroSentinelRef" class="h-px" />
<!-- Info decorativa (scrolls away naturalmente) -->
<div class="flex items-start gap-4 px-4 pb-3">
<div class="features-hero__icon-wrap">
<i class="pi pi-bolt features-hero__icon" />
</div>
<div class="features-hero__sub">
Cadastre os recursos (features) que os planos podem habilitar.
A <b>key</b> é o identificador técnico; o <b>nome</b> é exibido para o usuário.
</div>
<!-- Hero sticky -->
<div
ref="heroEl"
class="sticky mx-3 md:mx-4 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5"
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
>
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-20 bg-fuchsia-400/10" />
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-10 -left-24 bg-indigo-400/10" />
</div>
<!-- HERO -->
<div ref="heroSentinelRef" class="features-hero-sentinel" />
<div ref="heroEl" class="features-hero mb-4" :class="{ 'features-hero--stuck': heroStuck }">
<div class="features-hero__blobs" aria-hidden="true">
<div class="features-hero__blob features-hero__blob--1" />
<div class="features-hero__blob features-hero__blob--2" />
<div class="relative z-10 flex items-center justify-between gap-3 flex-wrap">
<div class="min-w-0">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Recursos do Sistema</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Cadastre os recursos (features) que os planos podem habilitar.</div>
</div>
<div class="features-hero__inner">
<div class="features-hero__info min-w-0">
<div class="features-hero__title">Recursos do Sistema</div>
</div>
<!-- Ações desktop ( 1200px) -->
<div class="hidden xl:flex items-center gap-2 flex-wrap">
<Button label="Atualizar" icon="pi pi-refresh" severity="secondary" outlined size="small" :loading="loading" :disabled="saving" @click="fetchAll" />
<Button label="Adicionar recurso" icon="pi pi-plus" size="small" :disabled="saving" @click="openCreate" />
</div>
<!-- Ações desktop ( 1200px) -->
<div class="features-hero__actions features-hero__actions--desktop">
<Button label="Atualizar" icon="pi pi-refresh" severity="secondary" outlined size="small" :loading="loading" :disabled="saving" @click="fetchAll" />
<Button label="Adicionar recurso" icon="pi pi-plus" size="small" :disabled="saving" @click="openCreate" />
</div>
<!-- Ações mobile (< 1200px) -->
<div class="features-hero__actions--mobile">
<Button
label="Ações"
icon="pi pi-ellipsis-v"
severity="warn"
size="small"
aria-haspopup="true"
aria-controls="features_hero_menu"
@click="(e) => heroMenuRef.toggle(e)"
/>
<Menu ref="heroMenuRef" id="features_hero_menu" :model="heroMenuItems" :popup="true" />
</div>
<!-- Ações mobile (< 1200px) -->
<div class="flex xl:hidden">
<Button
label="Ações"
icon="pi pi-ellipsis-v"
severity="warn"
size="small"
aria-haspopup="true"
aria-controls="features_hero_menu"
@click="(e) => heroMenuRef.toggle(e)"
/>
<Menu ref="heroMenuRef" id="features_hero_menu" :model="heroMenuItems" :popup="true" />
</div>
</div>
</div>
<!-- Search sempre visível, fora do hero sticky -->
<div class="px-4 mb-4">
<!-- content -->
<div class="px-3 md:px-4 pb-8 flex flex-col gap-4">
<!-- Search -->
<div>
<FloatLabel variant="on" class="w-full md:w-[380px]">
<IconField class="w-full">
<InputIcon class="pi pi-search" />
@@ -292,7 +289,6 @@ onBeforeUnmount(() => {
</FloatLabel>
</div>
<div class="px-4 pb-4">
<DataTable :value="filteredRows" dataKey="id" :loading="loading" stripedRows responsiveLayout="scroll">
<Column header="Domínio" style="width: 9rem">
<template #body="{ data }">
@@ -303,8 +299,8 @@ onBeforeUnmount(() => {
<Column field="key" header="Key" sortable style="min-width: 18rem">
<template #body="{ data }">
<div class="flex flex-col">
<span class="font-medium font-mono text-sm">{{ data.key }}</span>
<small class="text-color-secondary">ID: {{ data.id }}</small>
<span class="font-medium font-mono text-[1rem]">{{ data.key }}</span>
<div class="text-[1rem] text-[var(--text-color-secondary)]">ID: {{ data.id }}</div>
</div>
</template>
</Column>
@@ -317,7 +313,7 @@ onBeforeUnmount(() => {
<Column field="descricao" header="Descrição" sortable style="min-width: 22rem">
<template #body="{ data }">
<div class="max-w-[600px] whitespace-nowrap overflow-hidden text-ellipsis text-color-secondary" :title="data.descricao || ''">
<div class="max-w-[600px] whitespace-nowrap overflow-hidden text-ellipsis text-[var(--text-color-secondary)]" :title="data.descricao || ''">
{{ data.descricao || '—' }}
</div>
</template>
@@ -334,151 +330,87 @@ onBeforeUnmount(() => {
</template>
</Column>
</DataTable>
</div>
</div>
<Dialog
v-model:visible="showDlg"
modal
:header="isEdit ? 'Editar recurso' : 'Novo recurso'"
:style="{ width: '640px' }"
:closable="!saving"
:dismissableMask="!saving"
:draggable="false"
>
<div class="flex flex-col gap-4">
<!-- Key -->
<div>
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-tag" />
<InputText
id="cr-key"
v-model.trim="form.key"
class="w-full"
variant="filled"
:disabled="saving"
autocomplete="off"
autofocus
@blur="form.key = slugifyKey(form.key)"
@keydown.enter.prevent="save"
/>
</IconField>
<label for="cr-key">Key *</label>
</FloatLabel>
<small class="text-color-secondary block mt-1">
Ex.: <span class="font-mono">agenda.view</span> ou <span class="font-mono">online_scheduling.manage</span>.
Espaços e acentos são normalizados automaticamente.
</small>
</div>
<!-- Nome -->
<div>
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-bookmark" />
<InputText
id="cr-name"
v-model.trim="form.name"
class="w-full"
variant="filled"
:disabled="saving"
autocomplete="off"
@keydown.enter.prevent="save"
/>
</IconField>
<label for="cr-name">Nome *</label>
</FloatLabel>
<small class="text-color-secondary block mt-1">
Nome exibido para o usuário na página de upgrade e nas listagens.
</small>
</div>
<!-- Descrição PT-BR -->
<div>
<FloatLabel variant="on">
<Textarea
id="cr-desc-pt"
v-model.trim="form.descricao"
<Dialog
v-model:visible="showDlg"
modal
:header="isEdit ? 'Editar recurso' : 'Novo recurso'"
:style="{ width: '640px' }"
:closable="!saving"
:dismissableMask="!saving"
:draggable="false"
>
<div class="flex flex-col gap-4">
<!-- Key -->
<div>
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-tag" />
<InputText
id="cr-key"
v-model.trim="form.key"
class="w-full"
rows="3"
autoResize
variant="filled"
:disabled="saving"
autocomplete="off"
autofocus
@blur="form.key = slugifyKey(form.key)"
@keydown.enter.prevent="save"
/>
<label for="cr-desc-pt">Descrição</label>
</FloatLabel>
<small class="text-color-secondary block mt-1">
Explique o que o recurso habilita e para quem se aplica.
</small>
</IconField>
<label for="cr-key">Key *</label>
</FloatLabel>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">
Ex.: <span class="font-mono">agenda.view</span> ou <span class="font-mono">online_scheduling.manage</span>.
Espaços e acentos são normalizados automaticamente.
</div>
</div>
<template #footer>
<Button label="Cancelar" severity="secondary" outlined @click="showDlg = false" :disabled="saving" />
<Button :label="isEdit ? 'Salvar' : 'Criar'" icon="pi pi-check" :loading="saving" @click="save" />
</template>
</Dialog>
</div>
</template>
<!-- Nome -->
<div>
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-bookmark" />
<InputText
id="cr-name"
v-model.trim="form.name"
class="w-full"
variant="filled"
:disabled="saving"
autocomplete="off"
@keydown.enter.prevent="save"
/>
</IconField>
<label for="cr-name">Nome *</label>
</FloatLabel>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">
Nome exibido para o usuário na página de upgrade e nas listagens.
</div>
</div>
<style scoped>
.features-root { padding: 1rem; }
@media (min-width: 768px) { .features-root { padding: 1.5rem; } }
<!-- Descrição PT-BR -->
<div>
<FloatLabel variant="on">
<Textarea
id="cr-desc-pt"
v-model.trim="form.descricao"
class="w-full"
rows="3"
autoResize
:disabled="saving"
/>
<label for="cr-desc-pt">Descrição</label>
</FloatLabel>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">
Explique o que o recurso habilita e para quem se aplica.
</div>
</div>
</div>
.features-hero-sentinel { height: 1px; }
.features-hero {
position: sticky;
top: var(--layout-sticky-top, 56px);
z-index: 20;
overflow: hidden;
border-radius: 1.75rem;
border: 1px solid var(--surface-border);
background: var(--surface-card);
padding: 1.5rem;
}
.features-hero--stuck {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.features-hero__blobs {
position: absolute; inset: 0; pointer-events: none; overflow: hidden;
}
.features-hero__blob {
position: absolute; border-radius: 50%; filter: blur(70px);
}
.features-hero__blob--1 { width: 20rem; height: 20rem; top: -5rem; right: -4rem; background: rgba(217,70,239,0.10); }
.features-hero__blob--2 { width: 18rem; height: 18rem; top: 1rem; left: -5rem; background: rgba(99,102,241,0.10); }
.features-hero__inner {
position: relative; z-index: 1;
display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap;
}
.features-hero__icon-wrap {
flex-shrink: 0;
width: 4rem; height: 4rem; border-radius: 1.125rem;
border: 2px solid var(--surface-border);
background: var(--surface-ground);
display: grid; place-items: center;
}
.features-hero__icon { font-size: 1.5rem; color: var(--text-color); }
.features-hero__info { flex: 1; min-width: 0; }
.features-hero__title {
font-size: 1.25rem; font-weight: 700; letter-spacing: -0.025em;
color: var(--text-color); line-height: 1.2;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.features-hero__sub {
font-size: 0.78rem; color: var(--text-color-secondary); margin-top: 4px; line-height: 1.5;
}
.features-hero__actions--desktop {
display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
}
.features-hero__actions--mobile { display: none; }
@media (max-width: 1199px) {
.features-hero__actions--desktop { display: none; }
.features-hero__actions--mobile { display: flex; }
}
</style>
<template #footer>
<Button label="Cancelar" severity="secondary" outlined @click="showDlg = false" :disabled="saving" />
<Button :label="isEdit ? 'Salvar' : 'Criar'" icon="pi pi-check" :loading="saving" @click="save" />
</template>
</Dialog>
</template>