@luminix/mui-cms 0.2.13 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/acoes.md ADDED
@@ -0,0 +1,196 @@
1
+ # Ações
2
+
3
+ O sistema de ações permite adicionar comportamentos às telas de listagem dos modelos. Existem três tipos:
4
+
5
+ | Tipo | Contexto | Exemplo de uso |
6
+ |---|---|---|
7
+ | **Estáticas** | Nível de listagem, sem item selecionado | "Criar novo post" |
8
+ | **De instância** | Por linha da tabela | "Editar", "Excluir" |
9
+ | **Em massa** | Múltiplos itens selecionados | "Excluir selecionados", "Restaurar" |
10
+
11
+ ---
12
+
13
+ ## ActionCallbackEvent
14
+
15
+ Todo callback de ação recebe um objeto `ActionCallbackEvent` com utilitários prontos:
16
+
17
+ ```ts
18
+ type ActionCallbackEvent = {
19
+ navigate: (path: string) => void; // navega para uma rota interna
20
+ refresh: () => void; // recarrega a listagem atual
21
+ notify: NotifyFunction; // (notification: string | Notification) => void
22
+ dialog: DialogFunction; // (message: string | DialogMessage) => Promise<boolean | string>
23
+ t: TFunction; // função de tradução (i18next)
24
+ };
25
+ ```
26
+
27
+ Ações de instância recebem também `item: ModelType` (o registro da linha):
28
+
29
+ ```ts
30
+ type InstanceActionCallbackEvent = ActionCallbackEvent & {
31
+ item: Model;
32
+ };
33
+ ```
34
+
35
+ Ações em massa recebem `selected: Collection<Model>` (todos os itens selecionados):
36
+
37
+ ```ts
38
+ type MassActionCallbackEvent = ActionCallbackEvent & {
39
+ selected: Collection<Model>;
40
+ };
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Ações estáticas (StaticAction)
46
+
47
+ Aparecem na barra de ferramentas da listagem (ex.: botão "Criar").
48
+
49
+ ```ts
50
+ type StaticAction = {
51
+ key?: string;
52
+ label: string;
53
+ icon?: React.ReactNode;
54
+ callback: (e: ActionCallbackEvent) => void;
55
+ };
56
+ ```
57
+
58
+ **Padrão registrado:** "Create {Model}" — navega para a rota de criação quando a aba ativa não é "trashed".
59
+
60
+ ### Adicionar uma ação estática global
61
+
62
+ ```ts
63
+ import { Cms } from '@luminix/mui-cms';
64
+
65
+ Cms.reducer('staticActions', (actions, ModelClass, tab) => [
66
+ ...actions,
67
+ {
68
+ key: 'export',
69
+ label: 'Exportar CSV',
70
+ callback: ({ notify }) => {
71
+ // lógica de exportação
72
+ notify('Exportação iniciada!', 'info');
73
+ },
74
+ },
75
+ ]);
76
+ ```
77
+
78
+ ### Adicionar ação apenas para um modelo específico
79
+
80
+ ```ts
81
+ Cms.reducer('staticPostActions', (actions, ModelClass, tab) => [
82
+ ...actions,
83
+ {
84
+ key: 'publish-all',
85
+ label: 'Publicar todos',
86
+ callback: async ({ refresh, notify }) => {
87
+ await fetch('/api/posts/publish-all', { method: 'POST' });
88
+ refresh();
89
+ notify('Posts publicados!', 'success');
90
+ },
91
+ },
92
+ ]);
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Ações de instância (InstanceAction)
98
+
99
+ Aparecem no menu de contexto de cada linha da tabela.
100
+
101
+ ```ts
102
+ type InstanceAction = {
103
+ key?: string;
104
+ label: string;
105
+ icon?: React.ReactNode;
106
+ callback: (e: InstanceActionCallbackEvent) => void;
107
+ };
108
+ ```
109
+
110
+ **Padrão registrado:**
111
+ - "Send to trash" / "Delete permanently" — para modelos com/sem soft deletes.
112
+ - "Restore" + "Delete permanently" — quando a aba ativa é "trashed".
113
+
114
+ ### Adicionar ação de instância
115
+
116
+ ```ts
117
+ import { Cms } from '@luminix/mui-cms';
118
+
119
+ Cms.reducer('instanceActions', (actions, ModelClass, tab) => [
120
+ ...actions,
121
+ {
122
+ label: 'Ver detalhes',
123
+ callback: ({ item, navigate }) => {
124
+ navigate(`/posts/${item.getKey()}/detalhes`);
125
+ },
126
+ },
127
+ ]);
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Ações em massa (MassAction)
133
+
134
+ Aparecem quando um ou mais itens estão selecionados na tabela.
135
+
136
+ ```ts
137
+ type MassAction = {
138
+ key: string; // obrigatório — identificador único
139
+ label: string;
140
+ callback: (e: MassActionCallbackEvent) => void;
141
+ };
142
+ ```
143
+
144
+ **Padrão registrado:**
145
+ - "Send to trash" / "Delete permanently" — conforme soft deletes e aba ativa.
146
+ - "Restore" + "Delete permanently" — na aba "trashed".
147
+
148
+ ### Adicionar ação em massa
149
+
150
+ ```ts
151
+ import { Cms } from '@luminix/mui-cms';
152
+ import { Http } from '@luminix/core';
153
+
154
+ Cms.reducer('massActions', (actions, ModelClass, tab) => [
155
+ ...actions,
156
+ {
157
+ key: 'archive',
158
+ label: 'Arquivar selecionados',
159
+ callback: async ({ selected, refresh, notify, dialog }) => {
160
+ const confirmed = await dialog({
161
+ title: 'Arquivar itens',
162
+ message: `Deseja arquivar ${selected.count()} itens?`,
163
+ type: 'confirm',
164
+ });
165
+ if (confirmed) {
166
+ const ids = selected.map(item => item.getKey()).toArray();
167
+ await Http.post('/api/posts/archive', { ids });
168
+ refresh();
169
+ notify('Itens arquivados!');
170
+ }
171
+ },
172
+ },
173
+ ]);
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Ações padrão (comportamento automático)
179
+
180
+ O `CmsServiceProvider` registra automaticamente as ações padrão com prioridade `0`. Suas ações customizadas podem ser adicionadas com prioridade maior (padrão `undefined`) para aparecerem antes ou depois das padrão.
181
+
182
+ Para remover uma ação padrão, filtre pelo `key`:
183
+
184
+ ```ts
185
+ Cms.reducer('staticActions', (actions) =>
186
+ actions.filter(a => a.key !== 'create')
187
+ );
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Próximos passos
193
+
194
+ - [Extensibilidade](extensibilidade.md) — redutores em profundidade e como encadear transformações
195
+ - [Hooks](hooks.md) — `useActionEvent` para montar o evento manualmente em componentes customizados
196
+ - [Volta ao índice](index.md)
@@ -0,0 +1,147 @@
1
+ # Componentes
2
+
3
+ ## LuminixCms
4
+
5
+ Componente raiz da aplicação. Deve ser renderizado uma única vez no ponto de entrada do projeto.
6
+
7
+ ```tsx
8
+ import { LuminixCms } from '@luminix/mui-cms';
9
+
10
+ <LuminixCms
11
+ theme={themeOptions} // ThemeOptions (MUI) — opcional
12
+ themeArgs={[extraTheme]} // object[] — argumentos adicionais para createTheme
13
+ providers={[MyProvider]} // ServiceProvider[] — providers adicionais
14
+ />
15
+ ```
16
+
17
+ Internamente, o `LuminixCms`:
18
+ 1. Cria o tema MUI com suporte automático a modo escuro.
19
+ 2. Registra `CmsServiceProvider` e `i18NextServiceProvider`.
20
+ 3. Delega a renderização ao `LuminixProvider` de `@luminix/react`, que inicializa o contêiner de aplicação e o roteador.
21
+
22
+ ---
23
+
24
+ ## Link
25
+
26
+ Componente de navegação interna. Equivale ao `Link` do `react-router-dom`, mas integrado ao sistema de layout da biblioteca.
27
+
28
+ ```tsx
29
+ import { Link } from '@luminix/mui-cms';
30
+
31
+ <Link to="/posts">Ver posts</Link>
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Providers de contexto
37
+
38
+ Os providers a seguir são usados internamente pelas views e componentes da biblioteca. Em cenários avançados, você pode usá-los para acessar os contextos em componentes customizados.
39
+
40
+ ### DialogProvider
41
+
42
+ Fornece o contexto de diálogos modais. Necessário para que `useDialog` funcione.
43
+
44
+ ```tsx
45
+ import { DialogProvider } from '@luminix/mui-cms';
46
+
47
+ <DialogProvider>
48
+ {/* seus componentes */}
49
+ </DialogProvider>
50
+ ```
51
+
52
+ ### NotificationProvider
53
+
54
+ Fornece o contexto de notificações toast. Necessário para `useNotify`.
55
+
56
+ ```tsx
57
+ import { NotificationProvider } from '@luminix/mui-cms';
58
+
59
+ <NotificationProvider
60
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} // SnackbarProps
61
+ variant="filled" // 'filled' | 'outlined' | 'standard'
62
+ >
63
+ {/* seus componentes */}
64
+ </NotificationProvider>
65
+ ```
66
+
67
+ ### LayoutProvider
68
+
69
+ Gerencia o estado do layout (drawer aberto/fechado, breakpoint, título da página).
70
+
71
+ ```tsx
72
+ import { LayoutProvider } from '@luminix/mui-cms';
73
+
74
+ <LayoutProvider>
75
+ {/* seus componentes */}
76
+ </LayoutProvider>
77
+ ```
78
+
79
+ ### ModelProvider
80
+
81
+ Disponibiliza a classe de modelo atual para os componentes filhos (ex.: dentro de uma tela de listagem).
82
+
83
+ ```tsx
84
+ import { ModelProvider } from '@luminix/mui-cms';
85
+
86
+ <ModelProvider Model={PostModel}>
87
+ {/* seus componentes */}
88
+ </ModelProvider>
89
+ ```
90
+
91
+ ### TableProvider
92
+
93
+ Fornece o estado da tabela (seleção, ordenação, paginação) para os componentes de `ModelIndex`.
94
+
95
+ ```tsx
96
+ import { TableProvider } from '@luminix/mui-cms';
97
+
98
+ <TableProvider>
99
+ {/* seus componentes */}
100
+ </TableProvider>
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Componentes internos (registrados via redutor)
106
+
107
+ O `CmsServiceProvider` registra um mapa de componentes acessível via `Cms.getComponent(name)`. Esses componentes podem ser substituídos por customizados usando o redutor `componentMap`.
108
+
109
+ | Chave | Componente padrão |
110
+ |---|---|
111
+ | `Layout` | Layout raiz da aplicação |
112
+ | `Dashboard` | Tela inicial |
113
+ | `ModelIndex` | Tela de listagem de modelos |
114
+ | `ModelItem` | Tela de criação/edição |
115
+ | `Error` | Tela de erro |
116
+ | `Layout.AppBar` | Barra superior |
117
+ | `Layout.AppLogo` | Logo no drawer |
118
+ | `Layout.Drawer` | Menu lateral |
119
+ | `Layout.SearchBar` | Campo de busca |
120
+ | `Layout.BackButton` | Botão voltar |
121
+ | `ModelIndex.Filter` | Painel de filtros avançados |
122
+ | `ModelIndex.Table` | Tabela de dados |
123
+ | `ModelIndex.Pagination` | Paginação |
124
+ | `ModelIndex.MassActions` | Ações em massa |
125
+ | `ModelIndex.InstanceActions` | Ações por linha |
126
+ | `ModelIndex.StaticActions` | Ações globais (ex.: "Criar") |
127
+ | `ModelIndex.Tabs` | Abas de filtro (ex.: lixeira) |
128
+
129
+ Para substituir um componente:
130
+
131
+ ```ts
132
+ import { Cms } from '@luminix/mui-cms';
133
+ import MeuDashboard from './MeuDashboard';
134
+
135
+ Cms.reducer('componentMap', (map) => ({
136
+ ...map,
137
+ Dashboard: MeuDashboard,
138
+ }));
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Próximos passos
144
+
145
+ - [Facades](facades.md) — como usar `Cms`, `Filter` e `Icon`
146
+ - [Hooks](hooks.md) — hooks disponíveis para uso em componentes customizados
147
+ - [Volta ao índice](index.md)
@@ -0,0 +1,117 @@
1
+ # Configuração
2
+
3
+ ## Tema MUI
4
+
5
+ O `LuminixCms` aceita uma prop `theme` do tipo `ThemeOptions` (Material-UI). O tema padrão é:
6
+
7
+ ```ts
8
+ {
9
+ palette: {
10
+ primary: { main: '#1d9798' },
11
+ secondary: { main: '#fa510c' },
12
+ },
13
+ }
14
+ ```
15
+
16
+ Para sobrescrever, passe seu próprio objeto:
17
+
18
+ ```tsx
19
+ <LuminixCms
20
+ theme={{
21
+ palette: {
22
+ primary: { main: '#3f51b5' },
23
+ secondary: { main: '#f50057' },
24
+ },
25
+ typography: {
26
+ fontFamily: 'Inter, sans-serif',
27
+ },
28
+ }}
29
+ />
30
+ ```
31
+
32
+ O modo escuro (`dark`) é ativado automaticamente quando o sistema operacional do usuário preferir `prefers-color-scheme: dark`.
33
+
34
+ ### themeArgs
35
+
36
+ Para argumentos adicionais do `createTheme` (ex.: variáveis de componentes):
37
+
38
+ ```tsx
39
+ <LuminixCms
40
+ theme={{ palette: { primary: { main: '#1d9798' } } }}
41
+ themeArgs={[{ components: { MuiButton: { defaultProps: { disableElevation: true } } } }]}
42
+ />
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Layout
48
+
49
+ O layout é configurado via chave `luminix.admin.layout` na configuração da aplicação (injetada pelo `luminix/frontend`). As opções disponíveis são definidas pelo tipo `CmsConfig`:
50
+
51
+ ```ts
52
+ type CmsConfig = {
53
+ layout?: {
54
+ breakpoint?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; // padrão: 'md'
55
+ drawer?: {
56
+ width?: number; // largura do Drawer em px
57
+ };
58
+ appBar?: {
59
+ height?: number; // altura da AppBar em px
60
+ color?: string; // cor de fundo da AppBar
61
+ };
62
+ };
63
+ };
64
+ ```
65
+
66
+ No Laravel, publique e edite o arquivo de configuração do `luminix/admin` para definir esses valores.
67
+
68
+ ---
69
+
70
+ ## URL base do painel
71
+
72
+ Por padrão, o painel é montado em `/admin`. Para alterar, configure `luminix.admin.url` no backend:
73
+
74
+ ```php
75
+ // config/luminix.php
76
+ 'admin' => [
77
+ 'url' => '/painel',
78
+ ],
79
+ ```
80
+
81
+ O `CmsServiceProvider` lê esse valor e configura o `basename` do `react-router-dom` automaticamente.
82
+
83
+ ---
84
+
85
+ ## Operadores de filtro disponíveis
86
+
87
+ Os operadores usados nos filtros da tabela também são configuráveis via `luminix.admin.filter.operators`. O conjunto padrão inclui:
88
+
89
+ `equals`, `notEquals`, `contains`, `startsWith`, `endsWith`, `greaterThan`, `greaterThanOrEquals`, `lessThan`, `lessThanOrEquals`, `between`, `notBetween`, `null`, `notNull`, `relation`
90
+
91
+ ---
92
+
93
+ ## Providers customizados
94
+
95
+ Para registrar providers adicionais na inicialização da aplicação, use a prop `providers` do `LuminixCms`:
96
+
97
+ ```tsx
98
+ import { LuminixCms } from '@luminix/mui-cms';
99
+ import { ServiceProvider } from '@luminix/support';
100
+
101
+ class MyProvider extends ServiceProvider {
102
+ register() { /* ... */ }
103
+ boot() { /* ... */ }
104
+ }
105
+
106
+ <LuminixCms providers={[MyProvider]} />
107
+ ```
108
+
109
+ Os providers `CmsServiceProvider` e `i18NextServiceProvider` são sempre incluídos automaticamente.
110
+
111
+ ---
112
+
113
+ ## Próximos passos
114
+
115
+ - [Componentes](componentes.md) — `LuminixCms`, `Link` e providers de contexto
116
+ - [Extensibilidade](extensibilidade.md) — customizações via redutores
117
+ - [Volta ao índice](index.md)
@@ -0,0 +1,216 @@
1
+ # Extensibilidade
2
+
3
+ O `@luminix/mui-cms` foi projetado para ser profundamente customizável sem exigir fork ou modificação do código-fonte. O mecanismo central é o padrão **Reducible** de `@luminix/support`.
4
+
5
+ ## O padrão Reducible
6
+
7
+ Um serviço `Reducible` expõe "pontos de extensão" chamados **redutores**. Cada redutor é uma cadeia de funções que transforma um valor. O resultado final é a composição de todas as funções registradas, aplicadas em ordem de prioridade.
8
+
9
+ ```ts
10
+ // assinatura geral
11
+ ServiceFacade.reducer(
12
+ 'nomeDoReducer', // string — nome do ponto de extensão
13
+ (valorAtual, ...args) => novoValor, // função transformadora
14
+ prioridade // number — menor número roda primeiro (padrão: 10)
15
+ );
16
+ ```
17
+
18
+ Os redutores internos do `CmsServiceProvider` usam prioridade `0`. Registre os seus com prioridade maior para rodar depois (comportamento aditivo) ou menor para rodar antes (comportamento de pré-processamento).
19
+
20
+ ---
21
+
22
+ ## Onde registrar redutores
23
+
24
+ Os redutores devem ser registrados **antes** da aplicação inicializar. O lugar ideal é dentro de um `ServiceProvider` customizado:
25
+
26
+ ```ts
27
+ // src/providers/AppServiceProvider.ts
28
+ import { ServiceProvider } from '@luminix/support';
29
+ import { Cms, Icon, Filter } from '@luminix/mui-cms';
30
+ import { Star } from '@mui/icons-material';
31
+
32
+ class AppServiceProvider extends ServiceProvider {
33
+ register() {
34
+ // registre ícones aqui
35
+ Icon.registerIcon('Star', Star);
36
+ }
37
+
38
+ boot() {
39
+ // registre redutores aqui
40
+ Cms.reducer('menuItems', (items) => [
41
+ ...items,
42
+ {
43
+ key: 'relatorios',
44
+ text: 'Relatórios',
45
+ to: '/relatorios',
46
+ icon: Icon.render('Star'),
47
+ },
48
+ ]);
49
+ }
50
+ }
51
+
52
+ export default AppServiceProvider;
53
+ ```
54
+
55
+ E passe o provider para o `LuminixCms`:
56
+
57
+ ```tsx
58
+ import { LuminixCms } from '@luminix/mui-cms';
59
+ import AppServiceProvider from './providers/AppServiceProvider';
60
+
61
+ <LuminixCms providers={[AppServiceProvider]} />
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Redutores do CmsService
67
+
68
+ ### componentMap
69
+
70
+ Substitui componentes internos do painel:
71
+
72
+ ```ts
73
+ import MeuLayout from './components/MeuLayout';
74
+
75
+ Cms.reducer('componentMap', (map) => ({
76
+ ...map,
77
+ Layout: MeuLayout,
78
+ }));
79
+ ```
80
+
81
+ ### menuItems
82
+
83
+ Personaliza o menu lateral:
84
+
85
+ ```ts
86
+ Cms.reducer('menuItems', (items, models) => {
87
+ // remove modelos do menu
88
+ return items.filter(item => item.key !== 'user');
89
+ });
90
+ ```
91
+
92
+ ### cmsRoutes
93
+
94
+ Adiciona rotas customizadas ao painel. A estrutura de rotas é um array com **um único elemento raiz** que envolve o `Layout` e os providers. Todas as páginas são filhas desse elemento raiz via `children`. Para que o layout (AppBar, Drawer, Notification, Dialog) seja aplicado na nova página, a rota deve ser adicionada em `routes[0].children`:
95
+
96
+ ```ts
97
+ import Relatorios from './views/Relatorios';
98
+
99
+ Cms.reducer('cmsRoutes', (routes, components) => {
100
+ const [root, ...rest] = routes;
101
+ return [
102
+ {
103
+ ...root,
104
+ children: [
105
+ ...(root.children ?? []),
106
+ {
107
+ path: '/relatorios',
108
+ element: <Relatorios />,
109
+ },
110
+ ],
111
+ },
112
+ ...rest,
113
+ ];
114
+ });
115
+ ```
116
+
117
+ > Adicionar a rota fora de `routes[0].children` (ex.: `[...routes, { path: '...' }]`) faz a página renderizar sem o Layout do painel.
118
+
119
+ ### wireModelFormProps
120
+
121
+ Personaliza o formulário de modelo:
122
+
123
+ ```ts
124
+ import { Cms } from '@luminix/mui-cms';
125
+
126
+ Cms.reducer('wireModelFormProps', (props, item) => {
127
+ if (item?.getType() === 'post') {
128
+ return {
129
+ ...props,
130
+ // adicione campos confirmados, campos ocultos, etc.
131
+ };
132
+ }
133
+ return props;
134
+ });
135
+ ```
136
+
137
+ ### model{Name}Columns
138
+
139
+ Define as colunas da tabela para um modelo específico:
140
+
141
+ ```ts
142
+ Cms.reducer('modelPostColumns', () => [
143
+ { key: 'title', label: 'Título', scope: 'row', component: 'th' },
144
+ { key: 'author', label: 'Autor', align: 'right' },
145
+ { key: 'created_at', label: 'Criado em', align: 'right', size: 'small' },
146
+ ]);
147
+ ```
148
+
149
+ > A convenção de nome é `model` + nome do modelo em StudlyCase + `Columns`.
150
+ > Ex.: `modelBlogPostColumns` para o modelo `blog_post`.
151
+
152
+ ---
153
+
154
+ ## Redutores do FilterService
155
+
156
+ ### filterableColumns
157
+
158
+ Adiciona ou remove colunas do painel de filtros:
159
+
160
+ ```ts
161
+ import { Filter } from '@luminix/mui-cms';
162
+
163
+ Filter.reducer('filterableColumns', (columns, ModelClass) => {
164
+ if (ModelClass.getSchemaName() === 'post') {
165
+ return [
166
+ ...columns,
167
+ {
168
+ key: 'category_id',
169
+ label: 'Categoria',
170
+ type: 'autocomplete',
171
+ nullable: true,
172
+ is_relation: true,
173
+ },
174
+ ];
175
+ }
176
+ return columns;
177
+ });
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Redutores do Model (@luminix/core)
183
+
184
+ O `@luminix/core` expõe o redutor `model` (e `model{Name}`) para customizar a classe de modelo:
185
+
186
+ ```ts
187
+ import { Model } from '@luminix/core';
188
+
189
+ Model.reducer('modelPost', (Base) => {
190
+ return class extends Base {
191
+ static plural() { return 'Posts do Blog'; }
192
+ static icon() { return Icon.render('Article'); }
193
+ };
194
+ });
195
+ ```
196
+
197
+ Consulte a documentação do `@luminix/core` para a lista completa de redutores de modelo.
198
+
199
+ ---
200
+
201
+ ## Prioridades
202
+
203
+ | Prioridade | Quando usar |
204
+ |---|---|
205
+ | `0` | Valores padrão da biblioteca (já usados internamente) |
206
+ | `1`–`9` | Extensões do pacote `luminix/admin` ou integrações |
207
+ | `10` (padrão) | Customizações da aplicação |
208
+ | `> 10` | Overrides que precisam sobrescrever customizações anteriores |
209
+
210
+ ---
211
+
212
+ ## Próximos passos
213
+
214
+ - [Ações](acoes.md) — exemplos práticos de redutores de ações
215
+ - [Facades](facades.md) — referência completa dos redutores disponíveis por serviço
216
+ - [Volta ao índice](index.md)