Skip to content

Report Layout

Resumo

O LxReportLayout é um layout padrão para telas de relatório. Ele fornece uma estrutura consistente com cabeçalho, barra de ações e área de tabela, integrando opcionalmente sidebar de filtros, personalização de colunas e gestão de preferências salvas via LxReportPresets.

Todos os blocos estruturais possuem slots de override total: ao fornecer o slot correspondente (header, content, toolbar, extra), o conteúdo padrão é completamente substituído pelo fornecido.

Relatório de Produtos

Consulte e filtre os produtos cadastrados no sistema.

Clique para visualizar o código
vue
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import { LxReportLayout, LxDataTable, LxDataTableColumn, LxDateRangePicker, LxMultiSelect, LxInputText, LxButton, LxToast } from '@lde/lxcomponents';
import { PresetTypeEnum } from '@/components/LxReportPresets';
import { useToast } from '@/components/LxToast/useToast.js';
import type { HostPresetPayload, ReportPreset, SavedFilter, ViewLists } from '@/components/LxReportPresets/types';
import type { Row, Totalizadores, CategoryOption, StatusOption, PortalUser } from './types';

const formatCurrency = (value: number): string => new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);

const toast = useToast();

const rows = ref<Row[]>([
	{ id: 1, produto: 'Teclado mecânico', categoria: 'Periféricos', quantidade: 12, valor: 349.9 },
	{ id: 2, produto: 'Monitor 27"', categoria: 'Monitores', quantidade: 5, valor: 1899.0 },
	{ id: 3, produto: 'Headset gamer', categoria: 'Periféricos', quantidade: 20, valor: 279.9 },
	{ id: 4, produto: 'Cadeira ergonômica', categoria: 'Mobiliário', quantidade: 3, valor: 2100.0 },
	{ id: 5, produto: 'Webcam HD', categoria: 'Periféricos', quantidade: 8, valor: 199.9 }
]);

const onExport = (): void => {
	toast?.add({ status: 'success', title: 'Exportando dados...', time: 2500 });
};

const onGenerate = (): void => {
	toast?.add({ status: 'success', title: 'Gerando relatório...', time: 2500 });
};

const mockUsers = ref<PortalUser[]>([
	{ Id: 1, Nome: 'Ana Souza' },
	{ Id: 2, Nome: 'Bruno Lima' },
	{ Id: 3, Nome: 'Carlos Pereira' }
]);

const DEFAULT_COLUMNS = ['id', 'produto', 'categoria', 'quantidade', 'valor'] as const;

const DEFAULT_FILTERS = {
	periodo: { firstDate: null as null, lastDate: null as null, presetDate: 3 /* CurrentMonth */ },
	categoria: [] as string[],
	produto: '',
	status: [] as string[]
};

const FILTER_IDS = {
	PERIODO: 1,
	CATEGORIA: 2,
	PRODUTO: 3,
	STATUS: 4
} as const;

const toSelectValues = (value: unknown): string[] => (Array.isArray(value) ? value.map((item) => String(item)) : []);

const periodo = ref({ ...DEFAULT_FILTERS.periodo });
const filterPeriodo = ref({ ...DEFAULT_FILTERS.periodo });
const filterCategoria = ref<string[]>([...DEFAULT_FILTERS.categoria]);
const filterProduto = ref(DEFAULT_FILTERS.produto);
const filterStatus = ref<string[]>([...DEFAULT_FILTERS.status]);

const currentColumns = computed(() => [...DEFAULT_COLUMNS]);

const currentFilters = computed<SavedFilter[]>(() => {
	const activeFilters: SavedFilter[] = [];

	if (filterPeriodo.value.firstDate || filterPeriodo.value.lastDate || filterPeriodo.value.presetDate) {
		activeFilters.push({
			FiltroId: FILTER_IDS.PERIODO,
			Valor: {
				firstDate: filterPeriodo.value.firstDate,
				lastDate: filterPeriodo.value.lastDate,
				presetDate: filterPeriodo.value.presetDate
			}
		});
	}

	if (filterCategoria.value.length > 0) {
		activeFilters.push({ FiltroId: FILTER_IDS.CATEGORIA, Valor: [...filterCategoria.value] });
	}

	if (filterProduto.value.trim()) {
		activeFilters.push({ FiltroId: FILTER_IDS.PRODUTO, Valor: filterProduto.value.trim() });
	}

	if (filterStatus.value.length > 0) {
		activeFilters.push({ FiltroId: FILTER_IDS.STATUS, Valor: [...filterStatus.value] });
	}

	return activeFilters;
});

const createPreset = (preset: Partial<ReportPreset> & { Id: number; Nome: string; Tipo: ReportPreset['Tipo'] }): ReportPreset => ({
	Id: preset.Id,
	Nome: preset.Nome,
	Tipo: preset.Tipo,
	Padrao: preset.Padrao ?? false,
	Aplicada: preset.Aplicada ?? false,
	UsuarioCriadorId: preset.UsuarioCriadorId ?? 1,
	PodeEditar: preset.PodeEditar ?? true,
	Colunas: preset.Colunas ?? [...DEFAULT_COLUMNS],
	Filtros: preset.Filtros ?? [],
	Administradores: preset.Administradores ?? [],
	IgnorarDatas: preset.IgnorarDatas ?? true,
	IgnorarEmpresas: preset.IgnorarEmpresas ?? true
});

const viewLists = reactive<ViewLists>({
	privadas: [
		createPreset({
			Id: 1,
			Nome: 'Somente periféricos',
			Tipo: PresetTypeEnum.PRIVATE,
			Padrao: true,
			Aplicada: true,
			Filtros: [
				{ FiltroId: FILTER_IDS.CATEGORIA, Valor: ['perifericos'] },
				{ FiltroId: FILTER_IDS.STATUS, Valor: ['ativo'] }
			]
		})
	],
	publicas: [
		createPreset({
			Id: 2,
			Nome: 'Dashboard da equipe',
			Tipo: PresetTypeEnum.PUBLIC,
			Padrao: false,
			PodeEditar: true,
			UsuarioCriadorId: 2,
			Administradores: [1, 2],
			Filtros: [{ FiltroId: FILTER_IDS.STATUS, Valor: ['ativo', 'pendente'] }]
		}),
		createPreset({
			Id: 3,
			Nome: 'Somente leitura',
			Tipo: PresetTypeEnum.PUBLIC,
			Padrao: false,
			PodeEditar: false,
			UsuarioCriadorId: 2,
			Filtros: [{ FiltroId: FILTER_IDS.PRODUTO, Valor: 'monitor' }]
		})
	],
	modelos: []
});

const appliedView = ref<ReportPreset | null>(viewLists.privadas?.[0] ?? null);
const presetsLoading = ref(false);

let nextPresetId = 10;

const allViews = computed<ReportPreset[]>(() => [...(viewLists.privadas ?? []), ...(viewLists.publicas ?? []), ...(viewLists.modelos ?? [])]);

const syncAppliedFlags = (viewId: number | null): void => {
	allViews.value.forEach((view) => {
		(view as { Aplicada: boolean }).Aplicada = view.Id === viewId;
	});
};

const findListByType = (type: ReportPreset['Tipo']): ReportPreset[] => {
	if (type === PresetTypeEnum.PUBLIC) {
		return (viewLists.publicas ??= []);
	}
	if (type === PresetTypeEnum.TEMPLATE) {
		return (viewLists.modelos ??= []);
	}
	return (viewLists.privadas ??= []);
};

const applyFiltersFromView = (view: ReportPreset): void => {
	const periodoFilter = view.Filtros.find((item) => item.FiltroId === FILTER_IDS.PERIODO);
	const categoriaFilter = view.Filtros.find((item) => item.FiltroId === FILTER_IDS.CATEGORIA);
	const produtoFilter = view.Filtros.find((item) => item.FiltroId === FILTER_IDS.PRODUTO);
	const statusFilter = view.Filtros.find((item) => item.FiltroId === FILTER_IDS.STATUS);

	if (periodoFilter && typeof periodoFilter.Valor === 'object' && periodoFilter.Valor !== null) {
		const periodoValue = periodoFilter.Valor as { firstDate?: null; lastDate?: null; presetDate?: number | null };
		filterPeriodo.value = {
			firstDate: periodoValue.firstDate ?? null,
			lastDate: periodoValue.lastDate ?? null,
			presetDate: periodoValue.presetDate ?? null
		};
	} else {
		filterPeriodo.value = { ...DEFAULT_FILTERS.periodo };
	}

	filterCategoria.value = toSelectValues(categoriaFilter?.Valor);
	filterProduto.value = typeof produtoFilter?.Valor === 'string' ? produtoFilter.Valor : '';
	filterStatus.value = toSelectValues(statusFilter?.Valor);
	periodo.value = { ...filterPeriodo.value };
};

const configPresets = computed(() => ({
	appliedView: appliedView.value,
	viewLists,
	currentColumns: currentColumns.value,
	currentFilters: currentFilters.value,
	viewPageId: 1,
	showTabPrivates: true,
	showTabPublics: true,
	showTabModels: false,
	showCheckboxPublic: true,
	showCheckboxDates: true,
	showCheckboxCompanies: true,
	canAddPublicView: true,
	usersList: mockUsers.value
}));

const onApplyView = (view: ReportPreset): void => {
	appliedView.value = view;
	syncAppliedFlags(view.Id);
	applyFiltersFromView(view);
	toast?.add({ status: 'success', title: `Preferência aplicada: ${view.Nome}`, time: 3000 });
};

const onDeactivateView = (view: ReportPreset): void => {
	appliedView.value = null;
	syncAppliedFlags(null);
	toast?.add({ status: 'success', title: `Preferência desativada: ${view.Nome}`, time: 3000 });
};

const onSavePreset = (payload: HostPresetPayload): void => {
	const targetList = findListByType(payload.Tipo);
	if (payload.Padrao) {
		targetList.forEach((item) => ((item as { Padrao: boolean }).Padrao = false));
	}

	const createdView = createPreset({
		Id: ++nextPresetId,
		Nome: payload.Descricao,
		Tipo: payload.Tipo,
		Padrao: payload.Padrao,
		Aplicada: true,
		Colunas: payload.ColunasConfiguracao.map((coluna) => coluna.Id),
		Filtros: payload.Filtros.map((filtro) => ({ FiltroId: Number(filtro.Id), Valor: filtro.Valor })),
		Administradores: payload.IdsUsuarios,
		IgnorarDatas: payload.IgnorarDatas,
		IgnorarEmpresas: payload.IgnorarEmpresas
	});

	targetList.push(createdView);
	appliedView.value = createdView;
	syncAppliedFlags(createdView.Id);
	applyFiltersFromView(createdView);
	toast?.add({ status: 'success', title: `Preferência criada: ${createdView.Nome}`, time: 3000 });
};

const onUpdatePreset = (payload: { id: number; data: HostPresetPayload }): void => {
	const currentView = allViews.value.find((view) => view.Id === payload.id);
	if (!currentView) return;

	const targetList = findListByType(currentView.Tipo);
	const targetIndex = targetList.findIndex((item) => item.Id === payload.id);
	if (targetIndex === -1) return;

	if (payload.data.Padrao) {
		targetList.forEach((item) => ((item as { Padrao: boolean }).Padrao = false));
	}

	targetList[targetIndex] = createPreset({
		...currentView,
		Id: payload.id,
		Nome: payload.data.Descricao,
		Tipo: payload.data.Tipo,
		Padrao: payload.data.Padrao,
		Aplicada: appliedView.value?.Id === payload.id,
		Colunas: payload.data.ColunasConfiguracao.map((coluna) => coluna.Id),
		Filtros: payload.data.Filtros.map((filtro) => ({ FiltroId: Number(filtro.Id), Valor: filtro.Valor })),
		Administradores: payload.data.IdsUsuarios,
		IgnorarDatas: payload.data.IgnorarDatas,
		IgnorarEmpresas: payload.data.IgnorarEmpresas
	});

	if (appliedView.value?.Id === payload.id) {
		appliedView.value = targetList[targetIndex];
		applyFiltersFromView(targetList[targetIndex]);
	}

	toast?.add({ status: 'success', title: `Preferência atualizada: ${payload.data.Descricao}`, time: 3000 });
};

const onDeletePreset = (payload: { id: number }): void => {
	const privateIndex = (viewLists.privadas ?? []).findIndex((item) => item.Id === payload.id);
	if (privateIndex !== -1) {
		viewLists.privadas?.splice(privateIndex, 1);
	}

	const publicIndex = (viewLists.publicas ?? []).findIndex((item) => item.Id === payload.id);
	if (publicIndex !== -1) {
		viewLists.publicas?.splice(publicIndex, 1);
	}

	if (appliedView.value?.Id === payload.id) {
		appliedView.value = null;
		syncAppliedFlags(null);
	}

	toast?.add({ status: 'success', title: `Preferência removida: ID ${payload.id}`, time: 3000 });
};

const totais = ref<Totalizadores>({ valor: 4828.6 });

const categoriaOptions = ref<CategoryOption[]>([
	{ id: 'perifericos', label: 'Periféricos' },
	{ id: 'monitores', label: 'Monitores' },
	{ id: 'mobiliario', label: 'Mobiliário' }
]);

const statusOptions = ref<StatusOption[]>([
	{ id: 'ativo', label: 'Ativo' },
	{ id: 'inativo', label: 'Inativo' },
	{ id: 'pendente', label: 'Pendente' }
]);

const tableStyle = ref('small');

const toolbarConfig = computed(() => ({
	showFilter: true,
	showColumns: false,
	showExport: true,
	showGenerate: true,
	scrollable: true,
	enableStyleSelector: true,
	enableTableExpand: true
}));
</script>

<template>
	<LxToast />

	<LxReportLayout
		id="example-report"
		title="Relatório de Produtos"
		description="Consulte e filtre os produtos cadastrados no sistema."
		docs-url="https://github.com"
		:toolbar-config="toolbarConfig"
		:config-presets="configPresets"
		@export="onExport"
		@generate="onGenerate"
		@update:tableStyle="tableStyle = $event"
		@apply:view="onApplyView"
		@deactivate:view="onDeactivateView"
		@update:loading="presetsLoading = $event"
		@insert:preset="onSavePreset"
		@update:preset="onUpdatePreset"
		@delete:preset="onDeletePreset"
	>
		<template #filters>
			<div class="example-filter-group">
				<LxDateRangePicker id="example-periodo" label="Período" v-model="periodo" size="sm" />
			</div>
		</template>

		<template #filters-body>
			<div class="example-advanced-filters">
				<div class="example-filter-group">
					<LxDateRangePicker id="adv-periodo" v-model="filterPeriodo" label="Período" />
				</div>
				<div class="example-filter-group">
					<LxMultiSelect
						id="adv-categoria"
						v-model="filterCategoria"
						:options="categoriaOptions"
						track-by="id"
						options-label="label"
						label="Categoria"
						placeholder="Selecione categorias..."
					/>
				</div>
				<div class="example-filter-group">
					<LxMultiSelect
						id="adv-status"
						v-model="filterStatus"
						:options="statusOptions"
						track-by="id"
						options-label="label"
						label="Status"
						placeholder="Selecione status..."
					/>
				</div>
				<div class="example-filter-group">
					<LxInputText id="adv-produto" v-model="filterProduto" label="Produto" placeholder="Buscar produto..." />
				</div>
			</div>
		</template>

		<template #filters-footer="{ close }">
			<LxButton id="adv-filter-apply-btn" severity="primary" @click="close">Filtrar relatório</LxButton>
		</template>

		<template #table="{ loading, tableStyle }">
			<LxDataTable :value="rows" :loading="loading" :show-footer="true" :visualization-config="{ size: tableStyle }">
				<LxDataTableColumn field="id" header="ID" />
				<LxDataTableColumn field="produto" header="Produto" />
				<LxDataTableColumn field="categoria" header="Categoria" />
				<LxDataTableColumn field="quantidade" header="Quantidade" />
				<LxDataTableColumn field="valor" header="Valor (R$)" header-style="text-align:right" body-style="text-align:right" footer-style="text-align:right">
					<template #body="{ data }">
						{{ formatCurrency(data.valor) }}
					</template>
					<template #footer>
						<strong>{{ formatCurrency(totais.valor) }}</strong>
					</template>
				</LxDataTableColumn>
			</LxDataTable>
		</template>
	</LxReportLayout>
</template>

<style scoped>
.example-filter-group {
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
}

.example-filter-label {
	font-size: 0.75rem;
	font-weight: 500;
	color: var(--lx-text-secondary, #6c757d);
}

.example-filter-actions {
	display: flex;
	gap: 0.5rem;
}

.example-advanced-filters {
	display: flex;
	flex-direction: column;
	gap: 1rem;
	margin: 1rem;
}
</style>

Propriedades

NomeDescriçãoTipoPadrão
idIdentificador base do layoutString'lx-report-layout'
title⚠️ obr Nome do relatório exibido no cabeçalhoString
descriptionDescrição breve exibida abaixo do títuloString
docsUrlURL da documentação. Quando informada, exibe o botão de ajuda no cabeçalhoString''
loadingRepassa o estado de carregamento para o slot tableBooleanfalse
textConfigPermite sobrescrever os textos padrão dos botões e rótulos auxiliares. Ver ReportLayoutTextConfigReportLayoutTextConfig{}
toolbarConfigControla a visibilidade dos botões da toolbar. Ver ToolbarConfigToolbarConfig{}
filterSidebarConfigConfiguração do sidebar de filtros avançados. Ver FilterSidebarConfigFilterSidebarConfig{}
columnsSidebarConfigConfiguração do sidebar de personalização de colunas. Ver ColumnsSidebarConfigColumnsSidebarConfig{}
configPresetsQuando informado, exibe o botão de preferências na barra de ações. Ver ReportPresetsTriggerConfigReportPresetsTriggerConfig

ReportLayoutTextConfig

NomeDescriçãoTipoPadrão
docsTexto do botão de documentaçãoString'Abrir documentação'
filterTexto do botão de filtros avançadosString'Filtros avançados'
columnsTexto do botão de colunasString'Colunas'
exportTexto do botão de exportaçãoString'Exportar'
generateTexto do botão de geraçãoString'Gerar relatório'
expandSectionTexto usado no botão para expandir a tabelaString'Expandir tabela'
collapseSectionTexto usado no botão para recolher a tabelaString'Recolher tabela'
closeSectionTexto usado no botão interno de fechar da área expandidaString'Recolher tabela'
filterSidebarTitleTítulo padrão do sidebar de filtros quando filterSidebarConfig.title não é informadoString'Filtros avançados'
columnsSidebarTitleTítulo padrão do sidebar de colunas quando columnsSidebarConfig.title não é informadoString'Personalizar colunas'

ToolbarConfig

NomeDescriçãoTipoPadrão
showFilterExibe o botão "Filtros avançados"Booleantrue
showColumnsExibe o botão "Colunas"Booleanfalse
showExportExibe o botão "Exportar"Booleantrue
showGenerateExibe o botão "Gerar relatório"Booleantrue
scrollableControla se a área de filtros na toolbar usa scroll horizontal quando houver overflowBooleantrue
sizeDefine o tamanho dos controles padrão da toolbar'sm' | 'md' | 'lg''sm'
enableStyleSelectorHabilita renderização do LxDataTableStyleSelector no final da lista de açõesBooleanfalse
enableTableExpandHabilita o botão de expandir tabela e o uso interno do LxExpandableContainer na área de conteúdoBooleanfalse
expandTeleportToDefine o seletor CSS do alvo de Teleport usado quando a tabela é expandidaString'body'

Quando toolbarConfig.enableTableExpand está ativo, o LxReportLayout usa internamente o LxExpandableContainer para focar a área de tabela (${id}-content). Durante a expansão, a toolbar é ocultada e o recolhimento é feito pelo controle interno do próprio container. Os textos de ação podem ser sobrescritos por textConfig e o título do container expandido reutiliza o title do relatório. Por padrão, o conteúdo expandido é teleportado para body (full page), mas você pode alterar o destino com toolbarConfig.expandTeleportTo.

FilterSidebarConfig

NomeDescriçãoTipoPadrão
titleTítulo exibido no cabeçalho do sidebar de filtrosString'Filtros avançados'

ColumnsSidebarConfig

NomeDescriçãoTipoPadrão
titleTítulo exibido no cabeçalho do sidebar de colunasString'Personalizar colunas'
itemsLista de colunas disponíveis para personalizaçãoLxDataTableColumnDef[][]

ReportPresetsTriggerConfig

configPresets usa a mesma interface do LxReportPresetsTrigger, e o LxReportLayout monta internamente um settingsPresets com defaults para campos opcionais.

NomeDescriçãoTipoPadrão
appliedViewPreferência atualmente aplicada; controla ícone e título do botãoReportPreset | null
viewListsListas de preferências privadas, públicas e modelosViewLists
currentColumnsColunas ativas no momento (para salvar na preferência)ColumnsConfig[]
currentFiltersFiltros ativos no momento (para salvar na preferência)SavedFilter[][]
viewPageIdID da página de preferências no sistema de preferênciasNumber0
showTabPrivatesExibe a aba de preferências privadasBooleantrue
showTabPublicsExibe a aba de preferências públicasBooleantrue
showTabModelsExibe a aba de modelosBooleanfalse
showCheckboxPublicExibe a opção de tornar a preferência públicaBooleanfalse
showCheckboxDatesExibe a opção de ignorar datas ao aplicarBooleanfalse
showCheckboxCompaniesExibe a opção de ignorar empresas ao aplicarBooleanfalse
canAddPublicViewPermite criar preferências públicas na aba de públicasBooleanfalse
usersListLista de usuários exibida para seleção de administradoresPortalUser[]
sizeDefine o tamanho do botão gatilho de preferências'sm' | 'md' | 'lg''sm'

Eventos

NomeDescriçãoPayload
exportEmitido ao clicar em "Exportar"
generateEmitido ao clicar em "Gerar relatório"
update:tableStyleEmitido ao alterar o estilo no LxDataTableStyleSelector'small' | 'large'
update:expandSectionEmitido ao alterar o estado expandido do botão de seçãoboolean
expand:sectionEmitido ao expandir região (mantendo alvo + trigger visíveis){ targetId: string | null }
collapse:sectionEmitido ao recolher seção e restaurar visibilidade{ targetId: string | null }
save:columnsEmitido ao salvar a configuração de colunas no sidebarunknown[]
apply:viewEmitido ao aplicar uma preferência salvaReportPreset
deactivate:viewEmitido ao desativar a preferência aplicadaReportPreset
update:loadingEmitido durante operações assíncronas do painel de preferênciasboolean
insert:presetEmitido ao criar uma nova preferênciaHostPresetPayload
update:presetEmitido ao atualizar uma preferência existente{ id: number; data: HostPresetPayload }
delete:presetEmitido ao excluir uma preferência{ id: number }

Slots

Slots de override total

Quando fornecidos, substituem completamente o bloco padrão correspondente do LxBaseLayout.

NomeEscopoDescrição
headerSubstitui todo o cabeçalho padrão (título + ações)
contentSubstitui toda a área de conteúdo (toolbar + tabela)
footerSubstitui o rodapé padrão. Só é renderizado quando o slot é fornecido
extraSubstitui a área de extras (sidebars de filtros e colunas)

Slots de composição interna

Funcionam quando os slots de override acima não são fornecidos, permitindo customização pontual dentro do layout padrão.

NomeEscopoDescrição
header-actionsAções extras no cabeçalho, à esquerda do botão de ajuda
toolbarOverride total da toolbar; substitui todo o bloco de filtros + ações quando fornecido
filtersInputs de filtros básicos na toolbar (ex: período, empresa)
filters-bodyConteúdo do sidebar de filtros avançados
filters-footer{ close }Rodapé do sidebar de filtros; close() fecha o sidebar
table{ loading, tableStyle }Conteúdo da área de tabela; loading repassa o estado da prop homônima e tableStyle expõe o estilo atual (small/large)

Dependência do slot table

Quando você usa o slot table, o LxReportLayout deixa de renderizar uma tabela interna e passa a responsabilidade para o componente consumidor.

Por isso, o slot recebe dados de escopo que devem ser usados explicitamente na tabela renderizada:

  • loading: sincroniza o estado de carregamento do layout com a tabela.
  • tableStyle: sincroniza o estilo selecionado no LxDataTableStyleSelector (small ou large).

Se tableStyle não for aplicado no componente de tabela, mudar o seletor visual não terá efeito prático na renderização.

Exemplo de uso recomendado:

vue
<template #table="{ loading, tableStyle }">
	<LxDataTable
		:value="rows"
		:loading="loading"
		:visualization-config="{ size: tableStyle }"
	>
		...
	</LxDataTable>
</template>

Observações:

  • O valor inicial de tableStyle no LxReportLayout é small.
  • O evento update:tableStyle é emitido com small ou large para quem precisar persistir esse estado fora do layout.

Desenvolvido pelo time Linx Microvix