+
{{ label }}
diff --git a/src/components/ui/index-table/IndexTable.mdx b/src/components/ui/index-table/IndexTable.mdx
new file mode 100644
index 00000000..aced0ffe
--- /dev/null
+++ b/src/components/ui/index-table/IndexTable.mdx
@@ -0,0 +1,278 @@
+import { Controls, Canvas, Meta, Markdown } from '@storybook/blocks';
+
+import * as IndexTableStories from './IndexTable.stories';
+
+
+
+# IndexTable
+
+> Este é um novo componente que substitui o antigo `TableList` e foi construído para ser mais simples, flexível e customizável. Internamente, o componente IndexTable não possui regras de negócio e funciona com base em propriedades passadas e eventos emitidos para o componente pai.
+
+O **IndexTable** é utilizado para exibir uma listagem de itens em forma de tabela. Ele é composto por abas que podem ser usadas para exibir os itens de um determinado contexto que atendem a um conjunto de filtros específicos. Além disso, o componente possui uma barra de ações com ações padrão para listagens.
+
+Uma das principais melhorias deste componente é na acessibilidade, pois ele inclui tratativas para garantir uma navegação completa por teclado, o que melhora a experiência de uso em geral.
+
+O componente também é totalmente customizável, permitindo que o usuário oculte as partes que não deseja exibir, desde que não se comprometa a funcionalidade essencial. Para isso, o IndexTable oferece slots que permitem a substituição ou adição de partes específicas da interface.
+
+
+
+
+## Slots
+
+- `actions` - slot para adicionar ações a tabela
+- `action-pagination` - slot para substituir o componente de paginação padrão da tabela caso seja necessário. Para isso o componente deve possuir a propriedade `pagination` com o valor `null`.
+- `bulk-actions` - slot para adicionar ações em massa, bloco visível quando há itens selecionados na listagem.
+- `footer-actions` - slot para adicionar ações no rodapé da tabela caso seja necessário.
+- `cell({key})` - slot para substituir o conteúdo de uma determinada célula, onde `{key}` é a chave do campo. O conteúdo disponível para o slot é `{ item: T, row: number }`, sendo `T` o mesmo tipo genérico atribuito para os items do componente.
+- `head({key})` - slot para substituir o conteúdo do cabeçalho de uma determinada coluna, onde `{key}` é a chave determinada para a coluna. O conteúdo disponível para o slot é `{ field: { key: string, label: string }, label: string }`.
+
+
+
+> Observação: a tabela acima não representa os slots disponíveis para o componente, para isso consulte a documentação acima no item [Slots](#slots). Além disso algumas das props disponíveis não estão sendo documentadas adequadamente na tabela acima, para isso consulte o complemento abaixo:
+
+### Complemento de Props
+
+- `ordination` - Define as opções de ordenação a serem exibidas no componente, sempre deve ser definida uma opção como ativa, se nenhuma for definida seleciona a primeira. Uma característica da lista de opções de ordenação é não permitir seleção múltipla, assim ao selecionar uma opção desmarca a anterior.
+- `pagination` - Quando o valor `null` é passado libera o slot `#pagination` para o uso do componente desejado, se houver. Interface para implementação da prop `pagination` é a `IndexTablePaginationProp`.
+- `bulkActions` - Define quais ações em massa serão exibidas ao selecionar itens da listagem no botão de `Ação em massa`. Interface para implementação é a `KeyLabelDefault[]`.
+- `activeFilterTags` - Define quais tags de filtros estão aplicados a tabela no momento. Interface para implementação é a `KeyLabelDefault[]`.
+- `loadingText` - Texto para o estado de carregamento interno do componente, se nada for passado assume um valor padrão.
+- `isInternalLoading` - Estado de carregamento interno do componente, deve ser usado para troca entre abas no componente IndexTable, ele permite a visualização do loading dentro do componente.
+
+## Exemplo de uso no código
+
+```vue
+
+
+
+
+
+ Alguma Ação a adicionar
+
+
+
+ [{{ props.label }}]
+
+
+
+ {{ props.item.image }} {{ props.row }}
+
+
+
+```
diff --git a/src/components/ui/index-table/IndexTable.scss b/src/components/ui/index-table/IndexTable.scss
new file mode 100644
index 00000000..159b2b97
--- /dev/null
+++ b/src/components/ui/index-table/IndexTable.scss
@@ -0,0 +1,33 @@
+@import '../../../scss/mixins.scss';
+
+.ui-index-table {
+ overflow-x: auto;
+ background-color: var(--s-color-fill-default);
+ border-top-left-radius: var(--s-border-radius-small);
+ border-top-right-radius: var(--s-border-radius-small);
+ border-bottom-left-radius: var(--s-border-radius-small);
+ border-bottom-right-radius: var(--s-border-radius-small);
+ @include scrollbarStyle;
+
+ &.-large {
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
+ }
+
+ &-wrapper {
+ min-width: fit-content;
+ }
+
+ &-footer {
+ background-color: var(--s-color-fill-default);
+ border-top: var(--s-border-light);
+ padding: var(--s-spacing-x-small) var(--s-spacing-nano);
+ order: 1;
+ border-bottom-left-radius: var(--s-border-radius-small);
+ border-bottom-right-radius: var(--s-border-radius-small);
+
+ &:empty {
+ display: none;
+ }
+ }
+}
diff --git a/src/components/ui/index-table/IndexTable.stories.ts b/src/components/ui/index-table/IndexTable.stories.ts
new file mode 100644
index 00000000..fb5633f8
--- /dev/null
+++ b/src/components/ui/index-table/IndexTable.stories.ts
@@ -0,0 +1,225 @@
+import type { Meta, StoryObj } from '@storybook/vue3';
+import Button from '../button/Button.vue';
+import {
+ completeIndexTableActions,
+ completeIndexTableProps,
+ type ItemInTable,
+} from './__mocks__/completeIndexTableArgs';
+import { customLineWidthAndHeightIndexTableProps } from './__mocks__/customLineWidthAndHeightIndexTableArgs';
+import { customSlotsIndexTableProps } from './__mocks__/customSlotsArgs';
+import './__mocks__/CustomStyleIndexTable.css';
+import { filterTabWithoutItemsIndexTableProps } from './__mocks__/filterTabWithoutItemsArgs';
+import {
+ flowForChangingTabsAndRemovingFiltersIndexTableProps,
+ wrapperToChangeTab,
+ wrapperToRemoveFilter,
+} from './__mocks__/flowForChangingTabsAndRemovingFiltersArgs';
+import { changeLoading, initializationLoadingDataTableProps } from './__mocks__/initializationLoadingDataArgs';
+import { minimumIndexTableProps } from './__mocks__/minimumIndexTableArgs';
+import { handlePageChange, paginationChangeIndexTableProps } from './__mocks__/paginationChangeIndexTableArgs';
+import { orderByName, sortItemsIndexTableProps, wrapperOrderBy } from './__mocks__/sortItemsIndexTableArgs';
+import IndexTable from './IndexTable.vue';
+import type { KeyLabelDefault } from './types';
+
+const templateIndexTable = /* html */ `
+
+
+ {{ content }}
+
+
+`;
+
+const meta: Meta
> = {
+ title: 'ui/IndexTable',
+ component: IndexTable as any,
+ render: (args) =>
+ ({
+ components: { IndexTable },
+ setup() {
+ return { args };
+ },
+ template: templateIndexTable,
+ }) as any,
+ argTypes: {
+ pagination: { control: { type: 'object' } },
+ ordination: { control: { type: 'object' } },
+ searchValue: { control: { type: 'text' }, if: { arg: 'show.search' } },
+ isLoading: { control: { type: 'boolean' } },
+ isInternalLoading: { control: { type: 'boolean' } },
+ emptyResultDisplay: { control: { type: 'object' } },
+ checkboxSelectAllValue: { control: { type: 'select' }, options: [true, false, null] },
+ },
+ parameters: {
+ controls: { expanded: true },
+ docs: {
+ controls: { exclude: '^on.*' },
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const complete: Story = {
+ args: {
+ ...completeIndexTableProps,
+ ...completeIndexTableActions,
+ },
+};
+
+export const filterTabWithoutItems: Story = {
+ args: {
+ ...filterTabWithoutItemsIndexTableProps,
+ ...completeIndexTableActions,
+ },
+};
+
+export const initializationLoadingData: Story = {
+ args: {
+ ...initializationLoadingDataTableProps,
+ ...completeIndexTableActions,
+ },
+ render: (args) =>
+ ({
+ components: { IndexTable },
+ setup() {
+ changeLoading(args as any);
+
+ return { args };
+ },
+ template: templateIndexTable,
+ }) as any,
+};
+
+export const flowForChangingTabsAndRemovingFilters: Story = {
+ args: {
+ ...flowForChangingTabsAndRemovingFiltersIndexTableProps,
+ ...completeIndexTableActions,
+ },
+ render: (args: any) =>
+ ({
+ components: { IndexTable },
+ setup() {
+ const handleOpenTab = (key: string) => {
+ wrapperToChangeTab(key, args);
+ };
+
+ args.onOpenTab = handleOpenTab;
+
+ const handleRemoveFilter = (tagFilter: KeyLabelDefault) => {
+ wrapperToRemoveFilter(tagFilter, args);
+ };
+
+ args.onRemoveFilter = handleRemoveFilter;
+
+ return { args };
+ },
+ template: templateIndexTable,
+ }) as any,
+};
+
+export const customSlots: Story = {
+ args: {
+ ...customSlotsIndexTableProps,
+ ...completeIndexTableActions,
+ },
+ render: (args) =>
+ ({
+ components: { IndexTable, Button },
+ setup() {
+ return { args };
+ },
+ template: /* html */ `
+
+
+ {{propsSlot.label}} (R$)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{propsSlot.item.name}}
- Modelo: {{propsSlot.item.model}}
+
+
+
+
+
+
+
+
+ `,
+ }) as any,
+};
+
+export const sortItems: Story = {
+ args: {
+ ...sortItemsIndexTableProps,
+ ...completeIndexTableActions,
+ },
+ render: (args: any) =>
+ ({
+ components: { IndexTable },
+ setup() {
+ orderByName(args);
+ const handleSortItems = (key: string) => {
+ wrapperOrderBy(key, args);
+ };
+
+ args.onOrderBy = handleSortItems;
+
+ return { args };
+ },
+ template: templateIndexTable,
+ }) as any,
+};
+
+export const paginationChange: Story = {
+ args: {
+ ...paginationChangeIndexTableProps,
+ ...completeIndexTableActions,
+ },
+ render: (args: any) =>
+ ({
+ components: { IndexTable },
+ setup() {
+ const handleNextPage = () => {
+ handlePageChange(args, 'next-page');
+ };
+
+ const handlePreviousPage = () => {
+ handlePageChange(args, 'previous-page');
+ };
+
+ args.onNextPage = handleNextPage;
+ args.onPreviousPage = handlePreviousPage;
+
+ return { args };
+ },
+ template: templateIndexTable,
+ }) as any,
+};
+
+export const customLineWidthAndHeight: Story = {
+ args: {
+ ...customLineWidthAndHeightIndexTableProps,
+ ...completeIndexTableActions,
+ },
+};
+
+export const minimum: Story = {
+ args: {
+ ...minimumIndexTableProps,
+ ...completeIndexTableActions,
+ },
+};
diff --git a/src/components/ui/index-table/IndexTable.vue b/src/components/ui/index-table/IndexTable.vue
new file mode 100644
index 00000000..f3285763
--- /dev/null
+++ b/src/components/ui/index-table/IndexTable.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/ui/index-table/__mocks__/CustomStyleIndexTable.css b/src/components/ui/index-table/__mocks__/CustomStyleIndexTable.css
new file mode 100644
index 00000000..d1f65eb0
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/CustomStyleIndexTable.css
@@ -0,0 +1,13 @@
+.custom-width.ui-index-table-list-head-price {
+ width: 20%;
+ min-width: 100px;
+}
+
+.custom-width.ui-index-table-list-head-id {
+ width: 5%;
+ max-width: 25px;
+}
+
+.custom-cell.ui-index-table-list-cell-name {
+ min-width: 230px;
+}
diff --git a/src/components/ui/index-table/__mocks__/completeIndexTableArgs.ts b/src/components/ui/index-table/__mocks__/completeIndexTableArgs.ts
new file mode 100644
index 00000000..a42775cc
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/completeIndexTableArgs.ts
@@ -0,0 +1,116 @@
+import { fn } from '@storybook/test';
+import type { IndexTableProps } from '../types';
+
+export type ItemInTable = {
+ id: number;
+ name: string;
+ price: string;
+};
+
+export const completeIndexTableProps: IndexTableProps = {
+ show: {
+ tabs: true,
+ select: true,
+ reload: true,
+ search: true,
+ filters: true,
+ bulkActionDelete: true,
+ },
+ tabs: [
+ {
+ label: 'Todos',
+ key: 'all',
+ active: false,
+ disabled: false,
+ },
+ {
+ label: 'Produtos ativos',
+ key: 'active_products',
+ active: true,
+ disabled: false,
+ },
+ {
+ label: 'Produtos inativos',
+ key: 'inactive_products',
+ active: false,
+ disabled: false,
+ },
+ ],
+ ordination: [
+ {
+ key: 'name',
+ label: 'Nome (A-z)',
+ active: false,
+ },
+ {
+ key: 'updated',
+ label: 'Atualizados',
+ active: false,
+ },
+ {
+ key: 'created_at',
+ label: 'Data de criação',
+ active: true,
+ },
+ ],
+ pagination: {
+ from: 1,
+ to: 3,
+ size: 25,
+ total: 3,
+ page: 1,
+ },
+ bulkActions: [
+ { label: 'Ativar registros', key: 'activate-records' },
+ { label: 'Inativar registros', key: 'inactivate-records' },
+ ],
+ activeFilterTags: [{ key: 'active', label: 'Ativo' }],
+ searchValue: '',
+ checkboxSelectAllValue: false,
+ emptyResultDisplay: {
+ show: false,
+ },
+ isLoading: false,
+ isInternalLoading: false,
+ items: [
+ { id: 1, name: 'Produto 1', price: 'R$ 10,00' },
+ { id: 2, name: 'Produto 2', price: 'R$ 20,00' },
+ { id: 3, name: 'Produto 3', price: 'R$ 30,00' },
+ ],
+ fields: [
+ {
+ key: 'id',
+ label: 'ID',
+ },
+ {
+ key: 'name',
+ label: 'Nome',
+ },
+ {
+ key: 'price',
+ label: 'Preço',
+ },
+ ],
+ loadingText: '',
+ headClass: {},
+ cellClass: {},
+};
+
+export const completeIndexTableActions: Record = {
+ onClearSearch: fn(),
+ onSearch: fn(),
+ onReload: fn(),
+ onOpenTab: fn(),
+ onFilters: fn(),
+ onSaveCustomFilters: fn(),
+ onSelectAll: fn(),
+ onDeleteSelectedItems: fn(),
+ onBulkAction: fn(),
+ onNextPage: fn(),
+ onPreviousPage: fn(),
+ onOrderBy: fn(),
+ onSelectedItems: fn(),
+ onRemoveFilter: fn(),
+ onResetFilters: fn(),
+ onOpenItem: fn(),
+};
diff --git a/src/components/ui/index-table/__mocks__/customLineWidthAndHeightIndexTableArgs.ts b/src/components/ui/index-table/__mocks__/customLineWidthAndHeightIndexTableArgs.ts
new file mode 100644
index 00000000..e4de57b7
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/customLineWidthAndHeightIndexTableArgs.ts
@@ -0,0 +1,56 @@
+import type { IndexTableProps } from '../types';
+import { completeIndexTableProps, type ItemInTable } from './completeIndexTableArgs';
+
+const items = [
+ { id: 1, name: 'Smartphone Samsung Galaxy A10', price: 'R$ 1.000,00' },
+ { id: 2, name: 'Iphone 11', price: 'R$ 5.000,00' },
+ { id: 3, name: 'Motorola Moto G', price: 'R$ 1.200,00' },
+ { id: 4, name: 'Samsung Galaxy S21', price: 'R$ 2.000,00' },
+ { id: 5, name: 'Iphone 12', price: 'R$ 6.000,00' },
+ { id: 6, name: 'Motorola Edge', price: 'R$ 3.000,00' },
+ { id: 7, name: 'Logitech MX Master 3', price: 'R$ 500,00' },
+ { id: 8, name: 'Logitech K380 Keyboard', price: 'R$ 250,00' },
+ { id: 9, name: 'Logitech G502 Mouse', price: 'R$ 350,00' },
+ { id: 10, name: 'Logitech G Pro Keyboard', price: 'R$ 600,00' },
+ { id: 11, name: 'Logitech C920 Webcam', price: 'R$ 400,00' },
+ { id: 12, name: 'Logitech Z906 Speakers', price: 'R$ 1.500,00' },
+ { id: 13, name: 'Logitech G733 Headset', price: 'R$ 800,00' },
+ { id: 14, name: 'Logitech G915 Keyboard', price: 'R$ 1.200,00' },
+ { id: 15, name: 'Logitech G305 Mouse', price: 'R$ 300,00' },
+ { id: 16, name: 'Logitech G Pro X Headset', price: 'R$ 900,00' },
+ { id: 17, name: 'Logitech G560 Speakers', price: 'R$ 1.000,00' },
+ { id: 18, name: 'Logitech G815 Keyboard', price: 'R$ 1.100,00' },
+ { id: 19, name: 'Logitech G903 Mouse', price: 'R$ 700,00' },
+ { id: 20, name: 'Logitech G432 Headset', price: 'R$ 500,00' },
+ { id: 21, name: 'Logitech G213 Keyboard', price: 'R$ 400,00' },
+ { id: 22, name: 'Logitech G600 Mouse', price: 'R$ 450,00' },
+ { id: 23, name: 'Logitech G935 Headset', price: 'R$ 1.000,00' },
+ { id: 24, name: 'Logitech G910 Keyboard', price: 'R$ 800,00' },
+ { id: 25, name: 'Logitech G203 Mouse', price: 'R$ 250,00' },
+];
+
+export const customLineWidthAndHeightIndexTableProps: IndexTableProps = {
+ ...completeIndexTableProps,
+ tabs: [
+ {
+ label: 'Todos',
+ key: 'all',
+ active: true,
+ },
+ ],
+ activeFilterTags: [],
+ items,
+ pagination: {
+ from: 1,
+ to: 25,
+ size: 25,
+ total: 25,
+ page: 1,
+ },
+ headClass: {
+ 'custom-width': true,
+ },
+ cellClass: {
+ 'custom-cell': true,
+ },
+};
diff --git a/src/components/ui/index-table/__mocks__/customSlotsArgs.ts b/src/components/ui/index-table/__mocks__/customSlotsArgs.ts
new file mode 100644
index 00000000..7a4e0722
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/customSlotsArgs.ts
@@ -0,0 +1,58 @@
+import type { IndexTableProps } from '../types';
+import { completeIndexTableProps, type ItemInTable } from './completeIndexTableArgs';
+
+export interface ItemInTableCustomSlots extends ItemInTable {
+ model?: string;
+ image?: string;
+}
+
+export const customSlotsIndexTableProps: IndexTableProps = {
+ ...completeIndexTableProps,
+ pagination: null,
+ items: [
+ {
+ id: 1,
+ price: 'R$ 4.400,00',
+ name: 'Smartphone Iphone 12',
+ model: 'Iphone',
+ image: 'https://via.placeholder.com/55',
+ },
+ {
+ id: 2,
+ name: 'Smartphone Samsung Galaxy S21',
+ price: 'R$ 3.200,00',
+ },
+ {
+ name: 'Smartphone Motorola Moto G9',
+ id: 3,
+ price: 'R$ 1.200,00',
+ },
+ ],
+ show: {
+ tabs: true,
+ select: true,
+ reload: false,
+ search: true,
+ filters: false,
+ bulkActionDelete: false,
+ },
+ ordination: null,
+ tabs: [
+ {
+ label: 'Todos',
+ key: 'all',
+ active: true,
+ },
+ {
+ label: 'Produtos ativos',
+ key: 'active_products',
+ active: false,
+ },
+ {
+ label: 'Produtos inativos',
+ key: 'inactive_products',
+ active: false,
+ },
+ ],
+ activeFilterTags: [],
+};
diff --git a/src/components/ui/index-table/__mocks__/filterTabWithoutItemsArgs.ts b/src/components/ui/index-table/__mocks__/filterTabWithoutItemsArgs.ts
new file mode 100644
index 00000000..2d1e43d3
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/filterTabWithoutItemsArgs.ts
@@ -0,0 +1,27 @@
+import type { IndexTableProps } from '../types';
+import { completeIndexTableProps, type ItemInTable } from './completeIndexTableArgs';
+
+export const filterTabWithoutItemsIndexTableProps: IndexTableProps = {
+ ...completeIndexTableProps,
+ items: [],
+ emptyResultDisplay: {
+ show: true,
+ },
+ tabs: [
+ {
+ label: 'Todos',
+ key: 'all',
+ active: false,
+ },
+ {
+ label: 'Produtos ativos',
+ key: 'active_products',
+ active: true,
+ },
+ {
+ label: 'Produtos inativos',
+ key: 'inactive_products',
+ active: false,
+ },
+ ],
+};
diff --git a/src/components/ui/index-table/__mocks__/flowForChangingTabsAndRemovingFiltersArgs.ts b/src/components/ui/index-table/__mocks__/flowForChangingTabsAndRemovingFiltersArgs.ts
new file mode 100644
index 00000000..b242898b
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/flowForChangingTabsAndRemovingFiltersArgs.ts
@@ -0,0 +1,135 @@
+import type { IndexTableProps, KeyLabelDefault } from '../types';
+import { completeIndexTableProps, type ItemInTable } from './completeIndexTableArgs';
+
+export const allItems: ItemInTable[] = [
+ {
+ id: 1,
+ name: 'Product 1',
+ price: 'R$ 100,00',
+ },
+ {
+ id: 2,
+ name: 'Product 2',
+ price: 'R$ 200,00',
+ },
+ {
+ id: 3,
+ name: 'Product 3',
+ price: 'R$ 300,00',
+ },
+ {
+ id: 4,
+ name: 'Product 4',
+ price: 'R$ 400,00',
+ },
+ {
+ id: 5,
+ name: 'Product 5',
+ price: 'R$ 500,00',
+ },
+ {
+ id: 6,
+ name: 'Product 6',
+ price: 'R$ 600,00',
+ },
+ {
+ id: 7,
+ name: 'Product 7',
+ price: 'R$ 700,00',
+ },
+ {
+ id: 8,
+ name: 'Product 8',
+ price: 'R$ 800,00',
+ },
+ {
+ id: 9,
+ name: 'Product 9',
+ price: 'R$ 900,00',
+ },
+ {
+ id: 10,
+ name: 'Product 10',
+ price: 'R$ 1000,00',
+ },
+];
+
+export const itemsActive = [allItems[1], allItems[2], allItems[5]];
+export const filterTagsActiveTab = [
+ {
+ key: 'status_active',
+ label: 'Status: Ativo',
+ },
+];
+
+export const itemsInactive = [
+ allItems[0],
+ allItems[3],
+ allItems[4],
+ allItems[6],
+ allItems[7],
+ allItems[8],
+ allItems[9],
+];
+export const filterTagsInactiveTab = [
+ {
+ key: 'status_inactive',
+ label: 'Status: Inativo',
+ },
+];
+
+export const flowForChangingTabsAndRemovingFiltersIndexTableProps: IndexTableProps = {
+ ...completeIndexTableProps,
+ items: allItems,
+ tabs: [
+ {
+ label: 'Todos',
+ key: 'all',
+ active: true,
+ },
+ {
+ label: 'Produtos ativos',
+ key: 'active_products',
+ active: false,
+ },
+ {
+ label: 'Produtos inativos',
+ key: 'inactive_products',
+ active: false,
+ },
+ ],
+ activeFilterTags: [],
+};
+
+export const wrapperToChangeTab = (key: string, args: IndexTableProps) => {
+ args.isInternalLoading = true;
+ console.info('Changing tab >>>>', key);
+ // TODO: adicionar troca de paginação
+
+ setTimeout(() => {
+ if (key === 'active_products') {
+ args.activeFilterTags = filterTagsActiveTab;
+ args.items = itemsActive;
+ }
+
+ if (key === 'inactive_products') {
+ args.activeFilterTags = filterTagsInactiveTab;
+ args.items = itemsInactive;
+ }
+
+ if (key === 'all') {
+ args.activeFilterTags = [];
+ args.items = allItems;
+ }
+
+ args.isInternalLoading = false;
+ }, 2000);
+};
+
+export const wrapperToRemoveFilter = (tagFilter: KeyLabelDefault, args: IndexTableProps) => {
+ console.info('Removing filter >>>>', tagFilter);
+
+ args.activeFilterTags = [];
+ args.items = allItems;
+ args.tabs = args.tabs.map((tab) => ({ ...tab, active: tab.key === 'all' }));
+};
diff --git a/src/components/ui/index-table/__mocks__/initializationLoadingDataArgs.ts b/src/components/ui/index-table/__mocks__/initializationLoadingDataArgs.ts
new file mode 100644
index 00000000..b9863f2c
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/initializationLoadingDataArgs.ts
@@ -0,0 +1,31 @@
+import type { IndexTableProps } from '../types';
+import { completeIndexTableProps, type ItemInTable } from './completeIndexTableArgs';
+
+export const initializationLoadingDataTableProps: IndexTableProps = {
+ ...completeIndexTableProps,
+ tabs: [
+ {
+ label: 'Todos',
+ key: 'all',
+ active: true,
+ },
+ {
+ label: 'Produtos ativos',
+ key: 'active_products',
+ active: false,
+ },
+ {
+ label: 'Produtos inativos',
+ key: 'inactive_products',
+ active: false,
+ },
+ ],
+ activeFilterTags: [],
+ isLoading: true,
+};
+
+export const changeLoading = (args: IndexTableProps) => {
+ setTimeout(() => {
+ args.isLoading = false;
+ }, 3000);
+};
diff --git a/src/components/ui/index-table/__mocks__/minimumIndexTableArgs.ts b/src/components/ui/index-table/__mocks__/minimumIndexTableArgs.ts
new file mode 100644
index 00000000..35abf8ce
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/minimumIndexTableArgs.ts
@@ -0,0 +1,66 @@
+import type { IndexTableProps } from '../types';
+import type { ItemInTable } from './completeIndexTableArgs';
+
+export const minimumIndexTableProps: IndexTableProps = {
+ show: {
+ tabs: false,
+ select: false,
+ reload: false,
+ search: false,
+ filters: false,
+ bulkActionDelete: false,
+ },
+ tabs: [
+ {
+ label: 'Todos',
+ key: 'all',
+ active: false,
+ },
+ {
+ label: 'Produtos ativos',
+ key: 'active_products',
+ active: true,
+ },
+ {
+ label: 'Produtos inativos',
+ key: 'inactive_products',
+ active: false,
+ },
+ ],
+ ordination: null,
+ pagination: {
+ from: 1,
+ to: 3,
+ size: 25,
+ total: 3,
+ page: 1,
+ },
+ bulkActions: [],
+ activeFilterTags: [],
+ searchValue: '',
+ checkboxSelectAllValue: false,
+ emptyResultDisplay: {
+ show: false,
+ },
+ isLoading: false,
+ isInternalLoading: false,
+ items: [
+ { id: 1, name: 'Produto 1', price: 'R$ 10,00' },
+ { id: 2, name: 'Produto 2', price: 'R$ 20,00' },
+ { id: 3, name: 'Produto 3', price: 'R$ 30,00' },
+ ],
+ fields: [
+ {
+ key: 'id',
+ label: 'ID',
+ },
+ {
+ key: 'name',
+ label: 'Nome',
+ },
+ {
+ key: 'price',
+ label: 'Preço',
+ },
+ ],
+};
diff --git a/src/components/ui/index-table/__mocks__/paginationChangeIndexTableArgs.ts b/src/components/ui/index-table/__mocks__/paginationChangeIndexTableArgs.ts
new file mode 100644
index 00000000..9ce1b77b
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/paginationChangeIndexTableArgs.ts
@@ -0,0 +1,123 @@
+import type { IndexTableProps } from '../types';
+import { completeIndexTableProps, type ItemInTable } from './completeIndexTableArgs';
+
+const itemsPage1 = [
+ { id: 1, name: 'Smartphone Samsung Galaxy A10', price: 'R$ 1.000,00' },
+ { id: 2, name: 'Iphone 11', price: 'R$ 5.000,00' },
+ { id: 3, name: 'Motorola Moto G', price: 'R$ 1.200,00' },
+ { id: 4, name: 'Samsung Galaxy S21', price: 'R$ 2.000,00' },
+ { id: 5, name: 'Iphone 12', price: 'R$ 6.000,00' },
+ { id: 6, name: 'Motorola Edge', price: 'R$ 3.000,00' },
+ { id: 7, name: 'Logitech MX Master 3', price: 'R$ 500,00' },
+ { id: 8, name: 'Logitech K380 Keyboard', price: 'R$ 250,00' },
+ { id: 9, name: 'Logitech G502 Mouse', price: 'R$ 350,00' },
+ { id: 10, name: 'Logitech G Pro Keyboard', price: 'R$ 600,00' },
+ { id: 11, name: 'Logitech C920 Webcam', price: 'R$ 400,00' },
+ { id: 12, name: 'Logitech Z906 Speakers', price: 'R$ 1.500,00' },
+ { id: 13, name: 'Logitech G733 Headset', price: 'R$ 800,00' },
+ { id: 14, name: 'Logitech G915 Keyboard', price: 'R$ 1.200,00' },
+ { id: 15, name: 'Logitech G305 Mouse', price: 'R$ 300,00' },
+ { id: 16, name: 'Logitech G Pro X Headset', price: 'R$ 900,00' },
+ { id: 17, name: 'Logitech G560 Speakers', price: 'R$ 1.000,00' },
+ { id: 18, name: 'Logitech G815 Keyboard', price: 'R$ 1.100,00' },
+ { id: 19, name: 'Logitech G903 Mouse', price: 'R$ 700,00' },
+ { id: 20, name: 'Logitech G432 Headset', price: 'R$ 500,00' },
+ { id: 21, name: 'Logitech G213 Keyboard', price: 'R$ 400,00' },
+ { id: 22, name: 'Logitech G600 Mouse', price: 'R$ 450,00' },
+ { id: 23, name: 'Logitech G935 Headset', price: 'R$ 1.000,00' },
+ { id: 24, name: 'Logitech G910 Keyboard', price: 'R$ 800,00' },
+ { id: 25, name: 'Logitech G203 Mouse', price: 'R$ 250,00' },
+];
+
+const itemsPage2 = [
+ { id: 26, name: 'LG UltraGear Monitor', price: 'R$ 2.500,00' },
+ { id: 27, name: 'LG Gram Laptop', price: 'R$ 8.000,00' },
+ { id: 28, name: 'LG 4K Monitor', price: 'R$ 3.000,00' },
+ { id: 29, name: 'LG UltraWide Monitor', price: 'R$ 2.800,00' },
+ { id: 30, name: 'LG 24MP88HV-S Monitor', price: 'R$ 1.200,00' },
+ { id: 31, name: 'LG 27GL850 Monitor', price: 'R$ 2.200,00' },
+ { id: 32, name: 'LG 32UL950-W Monitor', price: 'R$ 4.000,00' },
+ { id: 33, name: 'LG 34WK95U-W Monitor', price: 'R$ 5.000,00' },
+ { id: 34, name: 'LG 38WK95C-W Monitor', price: 'R$ 6.000,00' },
+ { id: 35, name: 'LG 49WL95C-W Monitor', price: 'R$ 7.000,00' },
+ { id: 36, name: 'Dell XPS 13 Laptop', price: 'R$ 10.000,00' },
+ { id: 37, name: 'Dell Inspiron 15 Laptop', price: 'R$ 4.500,00' },
+ { id: 38, name: 'Dell G5 15 Laptop', price: 'R$ 6.000,00' },
+ { id: 39, name: 'Dell Alienware m15 Laptop', price: 'R$ 12.000,00' },
+ { id: 40, name: 'Dell Latitude 7400 Laptop', price: 'R$ 9.000,00' },
+ { id: 41, name: 'Dell Precision 5540 Laptop', price: 'R$ 11.000,00' },
+ { id: 42, name: 'Dell UltraSharp Monitor', price: 'R$ 3.500,00' },
+ { id: 43, name: 'Dell P2419H Monitor', price: 'R$ 1.500,00' },
+ { id: 44, name: 'Dell S2719DGF Monitor', price: 'R$ 2.000,00' },
+ { id: 45, name: 'Dell U2718Q Monitor', price: 'R$ 3.000,00' },
+ { id: 46, name: 'Dell U3419W Monitor', price: 'R$ 4.500,00' },
+ { id: 47, name: 'Dell U4919DW Monitor', price: 'R$ 6.500,00' },
+ { id: 48, name: 'Dell Pro Stereo Soundbar', price: 'R$ 800,00' },
+ { id: 49, name: 'Dell Professional Soundbar', price: 'R$ 1.000,00' },
+ { id: 50, name: 'Dell AE515M Soundbar', price: 'R$ 600,00' },
+];
+
+const itemsPage3 = [
+ { id: 51, name: 'Sony WH-1000XM4 Headphones', price: 'R$ 1.500,00' },
+ { id: 52, name: 'Bose QuietComfort 35 II', price: 'R$ 1.200,00' },
+ { id: 53, name: 'Apple AirPods Pro', price: 'R$ 1.800,00' },
+ { id: 54, name: 'Samsung Galaxy Buds Pro', price: 'R$ 1.000,00' },
+ { id: 55, name: 'JBL Charge 4 Speaker', price: 'R$ 600,00' },
+ { id: 56, name: 'Amazon Echo Dot', price: 'R$ 300,00' },
+ { id: 57, name: 'Google Nest Hub', price: 'R$ 700,00' },
+ { id: 58, name: 'Fitbit Charge 4', price: 'R$ 800,00' },
+ { id: 59, name: 'Garmin Forerunner 245', price: 'R$ 1.200,00' },
+ { id: 60, name: 'Apple Watch Series 6', price: 'R$ 3.000,00' },
+];
+
+export const paginationChangeIndexTableProps: IndexTableProps = {
+ ...completeIndexTableProps,
+ tabs: [
+ {
+ label: 'Todos',
+ key: 'all',
+ active: true,
+ },
+ ],
+ activeFilterTags: [],
+ items: itemsPage1,
+ pagination: {
+ from: 1,
+ to: 25,
+ size: 25,
+ total: itemsPage1.length + itemsPage2.length + itemsPage3.length,
+ page: 1,
+ },
+};
+
+export const handlePageChange = (args: IndexTableProps, event: 'next-page' | 'previous-page') => {
+ console.info('Evento disparado:', event);
+ args.isInternalLoading = true;
+ const currentPage = args.pagination?.page || 1;
+ const isFirstPage = currentPage === 1;
+ const isLastPage = args.pagination?.to === args.pagination?.total;
+
+ setTimeout(() => {
+ if (event === 'next-page' && !isLastPage) {
+ args.pagination!.page = currentPage + 1;
+ } else if (event === 'previous-page' && !isFirstPage) {
+ args.pagination!.page = currentPage - 1;
+ }
+
+ if (args.pagination!.page === 1) {
+ args.items = itemsPage1;
+ args.pagination!.from = 1;
+ args.pagination!.to = 25;
+ } else if (args.pagination!.page === 2) {
+ args.items = itemsPage2;
+ args.pagination!.from = 26;
+ args.pagination!.to = 50;
+ } else if (args.pagination!.page === 3) {
+ args.items = itemsPage3;
+ args.pagination!.from = 51;
+ args.pagination!.to = 60;
+ }
+
+ args.isInternalLoading = false;
+ }, 1000);
+};
diff --git a/src/components/ui/index-table/__mocks__/sortItemsIndexTableArgs.ts b/src/components/ui/index-table/__mocks__/sortItemsIndexTableArgs.ts
new file mode 100644
index 00000000..af688ef1
--- /dev/null
+++ b/src/components/ui/index-table/__mocks__/sortItemsIndexTableArgs.ts
@@ -0,0 +1,103 @@
+import type { IndexTableProps } from '../types';
+import { completeIndexTableProps } from './completeIndexTableArgs';
+
+export interface ItemSortItemsInTable {
+ id: number;
+ name: string;
+ price: string;
+ created_at: string;
+ updated: string;
+}
+
+const ordination = [
+ {
+ key: 'order_by_name',
+ label: 'Nome (A-z)',
+ active: false,
+ },
+ {
+ key: 'order_by_updated',
+ label: 'Data de atualização',
+ active: false,
+ },
+ {
+ key: 'order_by_created_at',
+ label: 'Data de criação',
+ active: false,
+ },
+];
+
+export const sortItemsIndexTableProps: IndexTableProps = {
+ ...completeIndexTableProps,
+ tabs: [
+ {
+ label: 'Todos',
+ key: 'all',
+ active: true,
+ },
+ ],
+ ordination,
+ activeFilterTags: [{ key: ordination[0].key, label: ordination[0].label }],
+ items: [
+ {
+ id: 1,
+ name: 'Smartphone Samsung Galaxy A10',
+ price: 'R$ 1.000,00',
+ created_at: '2021-01-01',
+ updated: '2021-01-01',
+ },
+ { id: 2, name: 'Iphone 11', price: 'R$ 5.000,00', created_at: '2022-03-01', updated: '2022-04-01' },
+ { id: 3, name: 'Motorola Moto G', price: 'R$ 1.200,00', created_at: '2022-07-01', updated: '2021-01-01' },
+ { id: 4, name: 'Samsung Galaxy S21', price: 'R$ 2.000,00', created_at: '2023-09-29', updated: '2024-12-04' },
+ { id: 5, name: 'Iphone 12', price: 'R$ 6.000,00', created_at: '2024-01-01', updated: '2025-01-01' },
+ { id: 6, name: 'Motorola Edge', price: 'R$ 3.000,00', created_at: '2025-01-01', updated: '2024-07-08' },
+ ],
+};
+
+export const orderByName = (args: IndexTableProps) => {
+ const items = args.items!.sort((a, b) => a.name.localeCompare(b.name));
+ args.items = items;
+};
+
+const orderByUpdated = (args: IndexTableProps) => {
+ const items = args.items!.sort((a, b) => a.updated.localeCompare(b.updated));
+ args.items = items;
+};
+
+const orderByCreated = (args: IndexTableProps) => {
+ const items = args.items!.sort((a, b) => a.created_at.localeCompare(b.created_at));
+ args.items = items;
+};
+
+export const wrapperOrderBy = (key: string, args: IndexTableProps) => {
+ args.isInternalLoading = true;
+ console.info('Ordering by >>>>', key);
+ const currentOrdination = args.ordination;
+
+ const newOrdination = currentOrdination!.map((item) => {
+ if (item.key === key) {
+ item.active = !item.active;
+ }
+ return item;
+ });
+
+ args.activeFilterTags = [{ key, label: args.ordination!.find((item) => item.key === key)!.label }];
+
+ if (key === 'order_by_name') {
+ orderByName(args);
+ }
+
+ if (key === 'order_by_updated') {
+ orderByUpdated(args);
+ }
+
+ if (key === 'order_by_created_at') {
+ orderByCreated(args);
+ }
+
+ args.ordination = newOrdination;
+
+ setTimeout(() => {
+ args.isInternalLoading = false;
+ }, 1000);
+};
diff --git a/src/components/ui/index-table/actions/IndexTableActions.scss b/src/components/ui/index-table/actions/IndexTableActions.scss
new file mode 100644
index 00000000..e8a057b4
--- /dev/null
+++ b/src/components/ui/index-table/actions/IndexTableActions.scss
@@ -0,0 +1,75 @@
+@import '../../../../scss/mixins.scss';
+
+.ui-index-table-actions {
+ &-tags {
+ padding: var(--s-spacing-x-small);
+ }
+
+ &.-mobile {
+ display: none;
+ }
+
+ &.-desktop {
+ display: inline-flex;
+ }
+
+ &-search {
+ position: relative;
+ min-width: 180px;
+ max-width: 300px;
+
+ @media screen and (max-width: 768px) {
+ width: 180px;
+ }
+ }
+
+ &-refresh {
+ @media screen and (max-width: 768px) {
+ display: none;
+ }
+ }
+
+ &-tag-loader {
+ position: relative;
+ overflow: hidden;
+ height: 56px;
+ transition: height var(--s-motion-duration-fast) var(--s-motion-ease-default);
+
+ &.-zero-items {
+ height: 0;
+ }
+ }
+
+ &-item {
+ padding: var(--s-spacing-x-small);
+ border-top: var(--s-border-light);
+ border-bottom: 1px solid var(--s-color-border-highlight);
+ min-width: max-content;
+ width: 100%;
+ }
+
+ &-item,
+ &-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ &-checkbox {
+ margin-right: var(--s-spacing-nano);
+ }
+
+ &-wrapper {
+ gap: var(--s-spacing-nano);
+ }
+
+ @media screen and (max-width: 768px) {
+ &.-mobile {
+ display: inline-flex;
+ }
+
+ &.-desktop {
+ display: none;
+ }
+ }
+}
diff --git a/src/components/ui/index-table/actions/IndexTableActions.vue b/src/components/ui/index-table/actions/IndexTableActions.vue
new file mode 100644
index 00000000..d7b74305
--- /dev/null
+++ b/src/components/ui/index-table/actions/IndexTableActions.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/ui/index-table/actions/IndexTableInternalLoader.scss b/src/components/ui/index-table/actions/IndexTableInternalLoader.scss
new file mode 100644
index 00000000..3b200126
--- /dev/null
+++ b/src/components/ui/index-table/actions/IndexTableInternalLoader.scss
@@ -0,0 +1,31 @@
+.table-list-loader {
+ background-color: var(--s-color-fill-success-light);
+ padding: var(--s-spacing-x-small);
+ overflow: hidden;
+ transition:
+ transform var(--s-motion-duration-fast) var(--s-motion-ease-default),
+ opacity var(--s-motion-duration-fast);
+ color: var(--s-color-content-success);
+ font-weight: var(--s-font-weight-medium);
+ display: flex;
+ align-items: center;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ transform: translateY(-100%);
+ opacity: 0;
+
+ &.-show {
+ transform: translateY(0);
+ opacity: 1;
+ }
+
+ > .table-list-loader-spinner {
+ margin-right: var(--s-spacing-nano);
+ }
+
+ > .table-list-loader {
+ margin-right: var(--s-spacing-nano);
+ }
+}
diff --git a/src/components/ui/index-table/actions/IndexTableInternalLoader.vue b/src/components/ui/index-table/actions/IndexTableInternalLoader.vue
new file mode 100644
index 00000000..f5cfe0b0
--- /dev/null
+++ b/src/components/ui/index-table/actions/IndexTableInternalLoader.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+ {{ loadingText }}
+
+
+
+
diff --git a/src/components/ui/index-table/actions/IndexTableOrderButton.scss b/src/components/ui/index-table/actions/IndexTableOrderButton.scss
new file mode 100644
index 00000000..fd6f1300
--- /dev/null
+++ b/src/components/ui/index-table/actions/IndexTableOrderButton.scss
@@ -0,0 +1,19 @@
+.ui-index-table-order-button-action {
+ &.-mobile {
+ display: none;
+ }
+
+ &.-desktop {
+ display: inline-flex;
+ }
+
+ @media screen and (max-width: 768px) {
+ &.-mobile {
+ display: inline-flex;
+ }
+
+ &.-desktop {
+ display: none;
+ }
+ }
+}
diff --git a/src/components/ui/index-table/actions/IndexTableOrderButton.vue b/src/components/ui/index-table/actions/IndexTableOrderButton.vue
new file mode 100644
index 00000000..47ce26c3
--- /dev/null
+++ b/src/components/ui/index-table/actions/IndexTableOrderButton.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/ui/index-table/actions/IndexTablePaginationItem.scss b/src/components/ui/index-table/actions/IndexTablePaginationItem.scss
new file mode 100644
index 00000000..fb8d211b
--- /dev/null
+++ b/src/components/ui/index-table/actions/IndexTablePaginationItem.scss
@@ -0,0 +1,31 @@
+.ui-index-table-pagination {
+ display: flex;
+ align-items: center;
+ gap: var(--s-spacing-nano);
+ margin-left: var(--s-spacing-nano);
+
+ &.-footer {
+ display: none;
+
+ @media screen and (max-width: 768px) {
+ margin-left: 0px;
+ justify-content: space-between;
+ display: flex;
+ }
+ }
+
+ &-previous {
+ @media screen and (max-width: 768px) {
+ order: -1;
+ }
+ }
+
+ &-item {
+ min-width: fit-content;
+ margin-right: var(--s-spacing-nano);
+ }
+
+ @media screen and (max-width: 768px) {
+ display: none;
+ }
+}
diff --git a/src/components/ui/index-table/actions/IndexTablePaginationItem.vue b/src/components/ui/index-table/actions/IndexTablePaginationItem.vue
new file mode 100644
index 00000000..005af618
--- /dev/null
+++ b/src/components/ui/index-table/actions/IndexTablePaginationItem.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/index-table/composables/useActionSelectAllItems.test.ts b/src/components/ui/index-table/composables/useActionSelectAllItems.test.ts
new file mode 100644
index 00000000..fc61ecdc
--- /dev/null
+++ b/src/components/ui/index-table/composables/useActionSelectAllItems.test.ts
@@ -0,0 +1,52 @@
+import { describe, test, expect, beforeEach, vi } from 'vitest';
+import { useActionSelectAllItems } from './useActionSelectAllItems';
+
+describe('useActionSelectAllItems', () => {
+ let emit: any;
+ let props: any;
+
+ beforeEach(() => {
+ emit = vi.fn();
+ props = {
+ checkboxSelectAllValue: false,
+ };
+ });
+
+ test('Dado os valores padrões Quando inicializado Então a ação em massa e o checkbox deve ter os estados de `false`', () => {
+ const { showBulkActions, checkboxAllSelected, checkboxAllSelectedIndeterminate } = useActionSelectAllItems(
+ emit,
+ props
+ );
+
+ expect(showBulkActions.value).toBe(false);
+ expect(checkboxAllSelected.value).toBe(false);
+ expect(checkboxAllSelectedIndeterminate.value).toBe(false);
+ });
+
+ test('Dado a visualização do checkbox geral Quando setado como `null` Então o checkbox deve estar indeterminado e deve exibir as ações em massa', () => {
+ const { setValueSelectedItemsCheckbox, checkboxAllSelected, checkboxAllSelectedIndeterminate, showBulkActions } =
+ useActionSelectAllItems(emit, props);
+
+ setValueSelectedItemsCheckbox(null);
+
+ expect(checkboxAllSelectedIndeterminate.value).toBe(true);
+ expect(checkboxAllSelected.value).toBe(false);
+ expect(showBulkActions.value).toBe(true);
+ });
+
+ test('Dado a visualização do checkbox geral Quando atualizado com valor `true` Então deve atualizar os valores do checkbox de forma a setar o ideterminado como false e emitir a ação de que tudo está selecionado', () => {
+ const {
+ updateCheckboxAllSelected,
+ checkboxAllSelected,
+ checkboxAllSelectedIndeterminate,
+ currentValueCheckboxSelectAll,
+ } = useActionSelectAllItems(emit, props);
+
+ updateCheckboxAllSelected(true);
+
+ expect(checkboxAllSelectedIndeterminate.value).toBe(false);
+ expect(checkboxAllSelected.value).toBe(true);
+ expect(currentValueCheckboxSelectAll.value).toBe(true);
+ expect(emit).toHaveBeenCalledWith('select-all', true);
+ });
+});
diff --git a/src/components/ui/index-table/composables/useActionSelectAllItems.ts b/src/components/ui/index-table/composables/useActionSelectAllItems.ts
new file mode 100644
index 00000000..a26f0d1a
--- /dev/null
+++ b/src/components/ui/index-table/composables/useActionSelectAllItems.ts
@@ -0,0 +1,62 @@
+import type { IndexTableActionsEmits, IndexTableActionsProps } from '../types';
+import { computed, onMounted, ref, watch } from 'vue';
+
+export const useActionSelectAllItems = (emit: IndexTableActionsEmits, props: IndexTableActionsProps) => {
+ const showBulkActions = ref(false);
+ const checkboxAllSelected = ref(false);
+ const checkboxAllSelectedIndeterminate = ref(false);
+
+ /** Valor computado do checkbox que exibe se está indeterminate ou se está selecionado */
+ const currentValueCheckboxSelectAll = computed(() => {
+ if (checkboxAllSelectedIndeterminate.value === true) return null;
+
+ return checkboxAllSelected.value;
+ });
+
+ /**
+ * Usado para alterar o valor do checkbox de seleção de todos os itens baseado no valor
+ * da prop
+ */
+ const setValueSelectedItemsCheckbox = (value: boolean | null) => {
+ if (value === null) {
+ checkboxAllSelectedIndeterminate.value = true;
+ checkboxAllSelected.value = false;
+
+ showBulkActions.value = true;
+ return;
+ }
+
+ checkboxAllSelectedIndeterminate.value = false;
+ checkboxAllSelected.value = value;
+ showBulkActions.value = value;
+ };
+
+ /**
+ * Atualiza o valor do checkbox de seleção de todos os itens e emite o evento com o valor do mesmo
+ */
+ const updateCheckboxAllSelected = (value: boolean | null) => {
+ setValueSelectedItemsCheckbox(value);
+
+ emit('select-all', currentValueCheckboxSelectAll.value);
+ };
+
+ onMounted(() => setValueSelectedItemsCheckbox(props.checkboxSelectAllValue as boolean | null));
+
+ watch(
+ () => props.checkboxSelectAllValue,
+ (newValue) => {
+ if (newValue === currentValueCheckboxSelectAll.value) return;
+
+ updateCheckboxAllSelected(newValue as boolean | null);
+ }
+ );
+
+ return {
+ showBulkActions,
+ checkboxAllSelected,
+ checkboxAllSelectedIndeterminate,
+ currentValueCheckboxSelectAll,
+ setValueSelectedItemsCheckbox,
+ updateCheckboxAllSelected,
+ };
+};
diff --git a/src/components/ui/index-table/composables/useIndexTableList.test.ts b/src/components/ui/index-table/composables/useIndexTableList.test.ts
new file mode 100644
index 00000000..dcecde57
--- /dev/null
+++ b/src/components/ui/index-table/composables/useIndexTableList.test.ts
@@ -0,0 +1,78 @@
+import { describe, test, expect, beforeEach, vi } from 'vitest';
+import { useIndexTableList } from './useIndexTableList';
+import type { IndexTableListProps, NameItemTableSelected } from '../types';
+
+describe('useIndexTableList', () => {
+ let emit: any;
+ let props: IndexTableListProps;
+
+ beforeEach(() => {
+ emit = vi.fn();
+ props = {
+ fields: [{ key: 'name', label: 'Name' }],
+ items: [{ name: 'Item 1' }, { name: 'Item 2' }],
+ checkboxSelectAllValue: false,
+ emptyResultDisplay: {
+ show: false,
+ },
+ };
+ });
+
+ test('Dado os valores padrões Quando inicializado Então nenhum item deve estar selecionado', () => {
+ const { selectedItems } = useIndexTableList(props, emit);
+
+ expect(selectedItems.value).toEqual([]);
+ });
+
+ test('Dado um item selecionado Quando atualizado pelo teclado Então o item deve ser adicionado à lista de selecionados', () => {
+ const { selectedItems, updateItemSelectedWithKeyboard } = useIndexTableList(props, emit);
+
+ updateItemSelectedWithKeyboard('item-0' as NameItemTableSelected);
+
+ expect(selectedItems.value).toContain('item-0');
+ });
+
+ test('Dado um item selecionado Quando atualizado novamente pelo teclado Então o item deve ser removido da lista de selecionados', () => {
+ const { selectedItems, updateItemSelectedWithKeyboard } = useIndexTableList(props, emit);
+
+ updateItemSelectedWithKeyboard('item-0' as NameItemTableSelected);
+ updateItemSelectedWithKeyboard('item-0' as NameItemTableSelected);
+
+ expect(selectedItems.value).not.toContain('item-0');
+ });
+
+ test('Dado a seleção de todos os itens Quando o checkbox geral é atualizado para true Então todos os itens devem ser selecionados', () => {
+ const { selectedItems, selectAllItems } = useIndexTableList(props, emit);
+
+ selectAllItems(true);
+
+ expect(selectedItems.value).toEqual(['item-0', 'item-1']);
+ });
+
+ test('Dado a seleção de todos os itens Quando o checkbox geral é atualizado para false Então nenhum item deve ser selecionado', () => {
+ const { selectedItems, selectAllItems } = useIndexTableList(props, emit);
+
+ selectAllItems(true);
+ selectAllItems(false);
+
+ expect(selectedItems.value).toEqual([]);
+ });
+
+ test('Dado um item selecionado Quando a função selectItem é chamada Então deve emitir os eventos corretos que indique que alguns itens foram selecionados e quais os itens selecionados', () => {
+ const { selectItem } = useIndexTableList(props, emit);
+
+ selectItem(['item-0']);
+
+ expect(emit).toHaveBeenCalledWith('selected-all-items', null);
+ expect(emit).toHaveBeenCalledWith('selected-items', [{ name: 'Item 1' }]);
+ });
+
+ test('Dado todos os itens selecionados Quando a função selectItem é chamada com todos os itens Então deve emitir que todos os itens estão selecionados e quais os itens estão selecionados', () => {
+ const { selectItem } = useIndexTableList(props, emit);
+
+ selectItem(['item-0', 'item-1']);
+
+ expect(emit).toHaveBeenCalledWith('selected-all-items', true);
+ expect(emit).toHaveBeenCalledWith('selected-items', [{ name: 'Item 1' }, { name: 'Item 2' }]);
+ });
+});
diff --git a/src/components/ui/index-table/composables/useIndexTableList.ts b/src/components/ui/index-table/composables/useIndexTableList.ts
new file mode 100644
index 00000000..ef8ccb97
--- /dev/null
+++ b/src/components/ui/index-table/composables/useIndexTableList.ts
@@ -0,0 +1,87 @@
+import { computed, ref } from 'vue';
+import type { IndexTableListEmits, IndexTableListProps, NameItemTableSelected } from '../types';
+
+export function useIndexTableList(props: IndexTableListProps, emit: IndexTableListEmits) {
+ const selectedItems = ref([]);
+
+ const prepareKeysToCell = computed(() => (items: object) => {
+ const itemsOfList = Object.entries(items).map((item: string[]) => {
+ return {
+ key: item[0],
+ value: item[1],
+ };
+ });
+
+ return props.fields.map((field) => ({
+ key: field.key,
+ value: itemsOfList.find((item) => item.key === field.key)?.value,
+ }));
+ });
+
+ /**
+ * O objeto dos itens selecionados retornados em uma lista
+ */
+ const itemsSelectedObject = computed(() => {
+ const allIndexes = selectedItems.value.map((item) => {
+ return parseInt(item.split('-')[1]);
+ });
+
+ return allIndexes.map((index) => {
+ return props.items[index];
+ });
+ });
+
+ /**
+ * Atualiza o item selecionado quando a ação é feita pelo teclado
+ */
+ const updateItemSelectedWithKeyboard = (item: NameItemTableSelected) => {
+ const listSelectedItems = [...selectedItems.value];
+
+ if (listSelectedItems.includes(item)) {
+ listSelectedItems.splice(listSelectedItems.indexOf(item), 1);
+ } else {
+ listSelectedItems.push(item);
+ }
+
+ selectItem(listSelectedItems);
+ };
+
+ /**
+ * Seleciona um item da lista
+ */
+ const selectItem = (value: NameItemTableSelected[]) => {
+ const anyItem = value.length >= 1 && value.length < props.items.length;
+ emit('selected-all-items', anyItem ? null : props.items.length === value.length);
+
+ selectedItems.value = value;
+ emit('selected-items', itemsSelectedObject.value);
+ };
+
+ /**
+ * Seleciona todos os itens da lista
+ */
+ const selectAllItems = (valueOfCheckbox: boolean | null) => {
+ if (valueOfCheckbox === null) return;
+
+ const currentSelectedItems = props.items.map((item, index: number) => `item-${index}` as NameItemTableSelected);
+
+ selectedItems.value = valueOfCheckbox ? currentSelectedItems : [];
+ };
+
+ /**
+ * Formata o nome da chave para o formato de classe
+ */
+ const formatKeyToClass = (key: string) => {
+ return key.replace(/_/g, '-');
+ };
+
+ return {
+ selectedItems,
+ prepareKeysToCell,
+ itemsSelectedObject,
+ updateItemSelectedWithKeyboard,
+ selectItem,
+ selectAllItems,
+ formatKeyToClass,
+ };
+}
diff --git a/src/components/ui/index-table/index.ts b/src/components/ui/index-table/index.ts
new file mode 100644
index 00000000..7cffcb64
--- /dev/null
+++ b/src/components/ui/index-table/index.ts
@@ -0,0 +1,2 @@
+export { default as IndexTable } from './IndexTable.vue';
+export * from './types';
diff --git a/src/components/ui/index-table/list/IndexTableEmptyMessage.scss b/src/components/ui/index-table/list/IndexTableEmptyMessage.scss
new file mode 100644
index 00000000..d598a607
--- /dev/null
+++ b/src/components/ui/index-table/list/IndexTableEmptyMessage.scss
@@ -0,0 +1,22 @@
+.index-table-empty-msg {
+ display: grid;
+ text-align: center;
+ height: 100%;
+ padding: var(--s-spacing-x-small);
+ transition: all var(--s-motion-ease-linear) var(--s-motion-duration-fast);
+
+ &-content {
+ border-radius: var(--s-border-radius-small);
+ background-color: var(--s-color-fill-default-light);
+ padding: var(--s-spacing-x-small);
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-height: 320px;
+ }
+
+ &-feedback {
+ height: fit-content;
+ }
+}
diff --git a/src/components/ui/index-table/list/IndexTableEmptyMessage.vue b/src/components/ui/index-table/list/IndexTableEmptyMessage.vue
new file mode 100644
index 00000000..134be8b5
--- /dev/null
+++ b/src/components/ui/index-table/list/IndexTableEmptyMessage.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/index-table/list/IndexTableList.scss b/src/components/ui/index-table/list/IndexTableList.scss
new file mode 100644
index 00000000..506e4571
--- /dev/null
+++ b/src/components/ui/index-table/list/IndexTableList.scss
@@ -0,0 +1,36 @@
+@import '../../../../scss/mixins.scss';
+
+.ui-index-table {
+ &-list {
+ min-width: 100%;
+ min-height: 300px;
+ border-top: var(--s-border-light);
+
+ &.-without-border-top {
+ border-top: none;
+ }
+
+ &-all-items-checkbox {
+ padding-left: var(--s-spacing-x-small);
+ padding-right: var(--s-spacing-x-small);
+ width: 20px;
+ }
+ }
+
+ &-empty-message {
+ min-height: 300px;
+ }
+
+ &-body {
+ .ui-table-row {
+ transition: all var(--s-motion-duration-fast) var(--s-motion-ease-default);
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ background-color: var(--s-color-fill-default-hover);
+ cursor: pointer;
+ }
+ }
+ }
+}
diff --git a/src/components/ui/index-table/list/IndexTableList.vue b/src/components/ui/index-table/list/IndexTableList.vue
new file mode 100644
index 00000000..70847c4a
--- /dev/null
+++ b/src/components/ui/index-table/list/IndexTableList.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ fieldHead.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ cell.value }}
+
+
+
+
+
+
+
diff --git a/src/components/ui/index-table/list/defaultPropEmptyResultDisplay.ts b/src/components/ui/index-table/list/defaultPropEmptyResultDisplay.ts
new file mode 100644
index 00000000..9ab6ab6c
--- /dev/null
+++ b/src/components/ui/index-table/list/defaultPropEmptyResultDisplay.ts
@@ -0,0 +1,14 @@
+import type { IndexTableEmptyResultProps } from '../types';
+
+export const defaultPropEmptyResultDisplay: IndexTableEmptyResultProps = {
+ title: 'Nenhum resultado encontrado',
+ subtitle:
+ 'Não encontramos nenhum item que corresponda à sua pesquisa.
Verifique o termo digitado ou tente um filtro diferente.',
+ button: {
+ label: 'Limpar filtros',
+ variant: 'primary',
+ },
+ showIcon: true,
+ showButton: true,
+ show: false,
+};
diff --git a/src/components/ui/index-table/tabs/IndexTableTabs.scss b/src/components/ui/index-table/tabs/IndexTableTabs.scss
new file mode 100644
index 00000000..20fc2402
--- /dev/null
+++ b/src/components/ui/index-table/tabs/IndexTableTabs.scss
@@ -0,0 +1,12 @@
+@import '../../../../scss/mixins.scss';
+
+.ui-index-table-tabs {
+ overflow: hidden;
+ height: 100%;
+
+ @include scrollbarStyle;
+
+ &.ui-tab {
+ border-bottom: none;
+ }
+}
diff --git a/src/components/ui/index-table/tabs/IndexTableTabs.vue b/src/components/ui/index-table/tabs/IndexTableTabs.vue
new file mode 100644
index 00000000..bfe9f48d
--- /dev/null
+++ b/src/components/ui/index-table/tabs/IndexTableTabs.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/ui/index-table/types.ts b/src/components/ui/index-table/types.ts
new file mode 100644
index 00000000..243e0caf
--- /dev/null
+++ b/src/components/ui/index-table/types.ts
@@ -0,0 +1,206 @@
+import type { FeedbackMessageProps } from '../feedback-message/types';
+
+export interface KeyLabelDefault {
+ label: string;
+ key: string;
+}
+
+export interface TabIndexTable extends KeyLabelDefault {
+ active: boolean;
+ disabled?: boolean;
+ badge?: string;
+}
+
+export interface IndexTableTabsProps {
+ /** Abas a exibir no componente que podem ser desabilitadas ou definidas como ativas conforme necessidade e também disponibiliza a `badge` da mesma forma que o componente `TabItem` */
+ tabs: TabIndexTable[];
+}
+
+export interface IndexTableTabsEmits {
+ /** Quando as abas estão visíveis essa ação é disparada ao clicar em uma aba */
+ (event: 'open-tab', key: string): void;
+}
+
+export interface ActionsToShow {
+ select: boolean;
+ reload: boolean;
+ search: boolean;
+ filters: boolean;
+ bulkActionDelete: boolean;
+}
+
+export interface IndexTableInternalLoaderProps {
+ /** Estado de carregamento interno do componente, deve ser usado para troca entre abas no componente IndexTable, ele permite a visualização do loading dentro do componente */
+ isInternalLoading: boolean;
+ /** Texto para o estado de carregamento interno do componente, se nada for passado assume um valor padrão */
+ loadingText?: string;
+}
+
+export interface IndexTablePaginationProp {
+ from: number;
+ to: number;
+ page: number;
+ size: number;
+ total: number;
+}
+
+export interface ActionOrdination extends KeyLabelDefault {
+ active: boolean;
+}
+
+export interface IndexTableOrderButtonProps {
+ /** Define as opções de ordenação a serem exibidas no componente, sempre deve ser definida uma opção como ativa, se nenhuma for definida seleciona a primeira. Uma característica da lista de opções de ordenação é não permitir seleção múltipla, assim ao selecionar uma opção desmarca a anterior. */
+ ordination: null | ActionOrdination[];
+}
+
+export interface IndexTableEmptyMessageProps extends FeedbackMessageProps {}
+
+export interface IndexTableEmptyResultProps extends Partial {
+ /** Define se irá exibir a visualização do resultado de pesquisa vazio */
+ show: boolean;
+}
+
+interface IndexTableEmptyResultDisplayProps {
+ /** Permite configurar se será exibido o resultado de pesquisa vazio e personalizar a mensagem, ícone e botão de acordo com a necessidade. */
+ emptyResultDisplay: IndexTableEmptyResultProps;
+}
+
+/** Propriedades para determinar a paginação no componente interno */
+export type IndexTablePaginationItemProps = IndexTableInternalLoaderProps &
+ IndexTablePaginationProp &
+ IndexTableEmptyResultDisplayProps;
+
+export interface IndexTableActionsProps
+ extends IndexTableOrderButtonProps,
+ IndexTableEmptyResultDisplayProps,
+ IndexTableInternalLoaderProps {
+ show: ActionsToShow;
+ /** Quando o valor `null` é passado libera o slot `#pagination` para o uso do componente desejado, se houver. Interface para implementação da prop `pagination` é a `IndexTablePaginationProp`. */
+ pagination: null | IndexTablePaginationProp;
+ /** Define quais tags de filtros estão aplicados a tabela no momento. Interface para implementação é a `KeyLabelDefault[]`. */
+ activeFilterTags: KeyLabelDefault[];
+ /** Define quais ações em massa serão exibidas ao selecionar itens da listagem */
+ bulkActions: KeyLabelDefault[];
+ /** Usado para simular a aplicação de uma busca inicial desejada na listagem, como no caso de obter a busca de query params da URL por exemplo */
+ searchValue?: string;
+ /** Usado para definir o valor do checkbox responsável por selecionar todos os itens. Quando `null` tem o aspecto indeterminate e quando `true` é exibido como checado */
+ checkboxSelectAllValue?: boolean | null;
+}
+
+export interface IndexTableOrderButtonEmits {
+ /** Ação disparada ao clicar em um botão de ordenação */
+ (event: 'order-by', key: string): void;
+}
+
+export interface IndexTableActionsSlots {
+ actions(): unknown;
+ ['action-pagination'](): unknown;
+ ['bulk-actions'](): unknown;
+}
+
+export interface IndexTablePaginationItemEmits {
+ /** Ação disparada quando o componente tem a paginação padrão e clica para avançar para a próxima página */
+ (event: 'next-page'): void;
+ /** Ação disparada quando o componente tem a paginação padrão e clica para voltar para a página anterior */
+ (event: 'previous-page'): void;
+}
+
+export interface IndexTableActionsEmits extends IndexTableOrderButtonEmits, IndexTablePaginationItemEmits {
+ /** Ação disparada no clique para limpar o campo de pesquisa */
+ (event: 'clear-search'): void;
+ /** Ação disparada ao efetuar uma pesquisa */
+ (event: 'search', word: string): void;
+ /** Ação disparada no clique para recarregar a listagem */
+ (event: 'reload'): void;
+ /** Ação disparada no botão `Filtros`, não necessário se o mesmo estiver oculto. */
+ (event: 'filters'): void;
+ /** Ação disparada ao clicar no checkbox da seção de ações que seleciona todos os itens da listagem, não necessário se o mesmo estiver oculto. Envia o valor setado no checkbox. */
+ (event: 'select-all', value: boolean | null): void;
+ /** Ação disparada ao clicar no botão "Deletar" exibido ao selecionar itens da tabela */
+ (event: 'delete-selected-items'): void;
+ /** Ação disparada ao clicar em uma das ações em massa listadas por padrão no componente */
+ (event: 'bulk-action', key: string): void;
+ /** Ação disparada ao clicar no botão fechar de uma tag de filtro da listagem */
+ (event: 'remove-filter', tagFilter: KeyLabelDefault): void;
+}
+
+export interface ColsToShow {
+ select: boolean;
+}
+
+export interface IndexTableListProps extends IndexTableEmptyResultDisplayProps {
+ /** Lista de items a serem exibidos na tabela com o tipo de objeto que for desejado. Para que os dados sejam exibidos corretamente, é necessário que o objeto tenha as chaves correspondentes aos campos de `key` definidos na prop `fields`, se uma chave não corresponder a um `field` o dado não será exibido. */
+ items: T[];
+ /** Define as colunas da tabela, e a ordem no array define a sequência de exibição na tabela. */
+ fields: KeyLabelDefault[];
+ show?: ColsToShow;
+ /** Usado para definir o valor do checkbox responsável por selecionar todos os itens. Quando `null` tem o aspecto indeterminate e quando `true` é exibido como checado */
+ checkboxSelectAllValue?: boolean | null;
+ /** Define uma classe para o cabeçalho da tabela com um objeto onde a chave é o nome da classe e o valor é um booleano para adicionar a tratativa de ativar ou não a classe */
+ headClass?: Record | null;
+ /** Define uma classe para a célula da tabela com um objeto onde a chave é o nome da classe e o valor é um booleano para adicionar a tratativa de ativar ou não a classe */
+ cellClass?: Record | null;
+}
+
+export interface IndexTablePropShow extends ActionsToShow {
+ tabs: boolean;
+}
+
+export interface IndexTableEmptyMessageEmits {
+ /** Quando existem 0 itens passados para o componente e a props emptyResultDisplay.show está ativa e o usuário clicou no item `outra opção de filtro` emite a ação para indicar que os filtros devem ser removidos */
+ (event: 'reset-filters'): void;
+}
+
+/** Nome do item selecionado na tabela, contendo o seu indice na tabela */
+export type NameItemTableSelected = `item-${number}`;
+
+/**
+ * Emits privados usados internamente pelo componente
+ */
+export interface IndexTableListPrivateEmits {
+ /** Quando todos itens são selecionados, emite essa ação para indicar a seleção do checkbox geral para o componente superior, serve apenas para controlar isso */
+ (event: 'selected-all-items', key: boolean | null): void;
+}
+
+/**
+ * Emits públicos usados externamente pelo componente
+ */
+export interface IndexTableListPublicEmits extends IndexTableEmptyMessageEmits {
+ /** Quando um item é selecionado é emitida essa ação mandando a key que consiste de uma string `item-{index}` sendo index o número do índice do item na lista */
+ (event: 'selected-items', items: T[]): void;
+ /** Quando um item é clicado é emitida essa ação mandando o item clicado */
+ (event: 'open-item', item: T): void;
+}
+
+export interface IndexTableListEmits extends IndexTableListPrivateEmits, IndexTableListPublicEmits {}
+
+interface IndexTableSlotCellProps {
+ item: T;
+ row: number;
+}
+
+interface IndexTableSlotHeadProps {
+ field: KeyLabelDefault;
+ label: string;
+}
+
+export interface IndexTableListSlots {
+ [key: `cell(${string})`]: (props: IndexTableSlotCellProps) => void;
+ [key: `head(${string})`]: (props: IndexTableSlotHeadProps) => void;
+}
+
+export interface IndexTableProps
+ extends IndexTableTabsProps,
+ Omit,
+ IndexTableListProps {
+ /** Define quais elementos internos serão exibidos no componente */
+ show?: IndexTablePropShow;
+ /** Estado de carregamento global do componente, deve ser usado no primeiro carregamento, quando ainda não há nenhum dado carregado nas abas do IndexTable */
+ isLoading: boolean;
+}
+
+export interface IndexTableEmits extends IndexTableTabsEmits, IndexTableActionsEmits, IndexTableListPublicEmits {}
+
+export interface IndexTableSlots extends IndexTableActionsSlots, IndexTableListSlots {
+ ['footer-actions']: () => void;
+}
diff --git a/src/components/ui/skeleton-table/SkeletonTable.vue b/src/components/ui/skeleton-table/SkeletonTable.vue
index 02ef6cbe..e50d207d 100644
--- a/src/components/ui/skeleton-table/SkeletonTable.vue
+++ b/src/components/ui/skeleton-table/SkeletonTable.vue
@@ -28,19 +28,19 @@ const skeletonTableStyleList = computed(() => {
|
- |
+ |
-
+ |
|
-
+
|
-
+ |
|
diff --git a/src/components/ui/tab/Tab.stories.ts b/src/components/ui/tab/Tab.stories.ts
index 9cfdda35..18273a3c 100644
--- a/src/components/ui/tab/Tab.stories.ts
+++ b/src/components/ui/tab/Tab.stories.ts
@@ -17,18 +17,34 @@ type Story = StoryObj;
export default meta;
-export const Default: Story = {
+export const minimum: Story = {
render: (args) => ({
components: { Tab, TabItem },
setup() {
- let tab = 'main';
+ const tab = 'main';
return { args, tab };
},
template: `
-
+
`,
}),
};
+
+export const withDisabled: Story = {
+ render: (args) => ({
+ components: { Tab, TabItem },
+ setup() {
+ const tab = 'second';
+ return { args, tab };
+ },
+ template: `
+
+
+
+
+ `,
+ }),
+};
diff --git a/src/components/ui/tab/Tab.vue b/src/components/ui/tab/Tab.vue
index 80ad38ff..02c3f76c 100644
--- a/src/components/ui/tab/Tab.vue
+++ b/src/components/ui/tab/Tab.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/src/components/ui/tab/TabItem.scss b/src/components/ui/tab/TabItem.scss
index 3d610d1b..64b13958 100644
--- a/src/components/ui/tab/TabItem.scss
+++ b/src/components/ui/tab/TabItem.scss
@@ -12,10 +12,11 @@
text-align: center;
white-space: nowrap;
z-index: var(--s-index-default);
- padding: var(--s-spacing-xx-small) var(--s-spacing-x-small);
+ padding: var(--s-spacing-nano) var(--s-spacing-xx-small);
color: inherit;
position: relative;
font: var(--s-typography-paragraph-regular);
+ transition: outline var(--s-motion-ease-linear) var(--s-motion-duration-fast);
&:hover:not(.-active) {
&:after {
@@ -23,6 +24,28 @@
}
}
+ &:disabled {
+ cursor: not-allowed;
+
+ .ui-tab-item-content {
+ font-weight: var(--s-font-weight-regular);
+ color: var(--s-color-content-disable);
+ }
+
+ &:hover:not(.-active) {
+ &:after {
+ background: transparent;
+ }
+ }
+ }
+
+ &:focus {
+ .ui-tab-item-content {
+ outline: 2px solid var(--b-color-base);
+ border-radius: 2px;
+ }
+ }
+
&::after {
content: '';
height: 2px;
@@ -50,13 +73,17 @@
align-items: center;
display: flex;
column-gap: var(--s-spacing-quark);
+ line-height: var(--s-line-height-24);
height: 100%;
+ padding: var(--s-spacing-quark);
}
&-badge {
- background-color: var(--s-color-fill-default);
+ background-color: var(--s-color-fill-default-light);
border-radius: var(--s-border-radius-pill);
padding: 0 var(--s-spacing-nano);
+ line-height: var(--s-line-height-16);
font: var(--s-typography-label-small);
+ color: var(--s-color-content-default);
}
}
diff --git a/src/components/ui/tab/TabItem.vue b/src/components/ui/tab/TabItem.vue
index 8086d44c..8e22ea66 100644
--- a/src/components/ui/tab/TabItem.vue
+++ b/src/components/ui/tab/TabItem.vue
@@ -6,21 +6,21 @@ const props = defineProps
();
const active = ref(false);
const tabsProvider: TabProviderInterface | undefined = inject('tabs');
-let _index: string | number = 0;
+let indexTab: string | number = 0;
if (tabsProvider) {
- _index = props.index || tabsProvider.tabs.length;
- tabsProvider.tabs.push(_index);
+ indexTab = props.index || tabsProvider.tabs.length;
+ tabsProvider.tabs.push(indexTab);
}
const onClick = (evt: MouseEvent) => {
if (tabsProvider) {
- tabsProvider.active(_index, evt);
+ tabsProvider.active(indexTab, evt);
}
};
watchEffect(() => {
- if (tabsProvider?.activeTabIndex == _index) {
+ if (tabsProvider?.activeTabIndex === indexTab) {
active.value = true;
} else {
active.value = false;
@@ -29,10 +29,10 @@ watchEffect(() => {
-