Skip to main content

frontend-angular

Use ao implementar frontend em Angular, quando project.config.md indicar esta stack. Cobre estrutura de projeto Angular 17+, standalone components, signals, biblioteca de componentes padrão (Angular Material ou PrimeNG), acessibilidade WCAG 2.2 AA, e padrão de UI assíncrona.

Source facts

Repository
marquesmarcelo/ia-dev-team
Last source activity
October 13, 2026 at 12:41
Detected SKILL.md language
Portuguese
Stars
3
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
7 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
frontend-angular
description
Use ao implementar frontend em Angular, quando project.config.md indicar esta stack. Cobre estrutura de projeto Angular 17+, standalone components, signals, biblioteca de componentes padrão (Angular Material ou PrimeNG), acessibilidade WCAG 2.2 AA, e padrão de UI assíncrona.
# Frontend: Angular (v17+) + Standalone Components > Leia também `.claude/skills/accessibility/SKILL.md` antes de implementar > qualquer tela nova. Se o projeto requer o Design System do Governo Federal > (DS Gov), leia adicionalmente `.claude/skills/frontend-angular-dsgov/SKILL.md`. ## Biblioteca de componentes padrão (escolher no project.config.md) A pesquisa atual indica: - **Angular Material** — melhor para: acessibilidade nativa (ARIA embutido no CDK), SSR, consistência visual, apps SaaS/produto. Escolha padrão quando acessibilidade é prioridade. - **PrimeNG** — melhor para: dashboards com tabelas pesadas, charts, 80+ componentes prontos, grids com virtual scrolling. Preferir quando o app é data-heavy (painéis administrativos). - A decisão vai registrada em `project.config.md` — não inventar durante a implementação. ## ⚠️ Verificar versão antes de implementar ```bash npm show @angular/core version # versão atual do Angular cat package.json | grep angular # versão instalada no projeto ``` Consultar https://angular.dev para APIs atuais (signals, resource(), linkedSignal()). Angular 17+ usa standalone por padrão e tem mudanças frequentes no sistema de reatividade — verificar se resource() e linkedSignal() estão disponíveis na versão instalada. ## Estrutura de pastas ``` src/ app/ core/ # singleton services, guards, interceptors auth/ http/ # interceptors (loading, error, auth token) layout/ # AppShell, Header, Sidebar, Footer shared/ ui/ # componentes visuais sem lógica de domínio status-badge/ # StatusBadge skeleton-table/ # SkeletonTable empty-state/ # EmptyState error-state/ # ErrorState confirm-delete/ # ConfirmDelete forms/ # componentes de formulário reutilizáveis autocomplete-create/ # AutocompleteCreate (com criação inline) rich-text-editor/ # RichTextEditor pipes/ # pipes reutilizáveis features/ # um diretório por funcionalidade processo/ components/ processo-list/ # componentes específicos da feature processo-form/ services/ processo.service.ts # chama API, expõe signals ou Observables models/ processo.model.ts # interfaces/types — nunca classes anêmicas processo.routes.ts # lazy loading usuario/ components/ services/ models/ usuario.routes.ts app.routes.ts app.config.ts ``` **Regra de decisão shared/ vs features/:** - Pode ser usado em mais de uma feature → `shared/ui/`, `shared/forms/` ou `shared/pipes/` - Específico de uma entidade → `features/<entidade>/` - Componente `shared/` nunca importa de `features/` ## Standalone Components (padrão Angular 17+) ```typescript // features/processo/components/processo-list/processo-list.component.ts @Component({ selector: 'app-processo-list', standalone: true, imports: [CommonModule, MatTableModule, MatPaginatorModule, RouterModule], templateUrl: './processo-list.component.html', }) export class ProcessoListComponent { private processoService = inject(ProcessoService); // Signals — estado reativo sem Zone.js processos = this.processoService.processos; isLoading = this.processoService.isLoading; hasError = this.processoService.hasError; meta = this.processoService.meta; } ``` ## Service com estado (Signals) ```typescript // features/processo/services/processo.service.ts @Injectable({ providedIn: 'root' }) export class ProcessoService { private http = inject(HttpClient); // Signals de estado processos = signal<Processo[]>([]); isLoading = signal(false); hasError = signal(false); isSubmitting = signal(false); meta = signal<PaginationMeta | null>(null); // Sort/filtro persistidos em localStorage private readonly STORAGE_KEY = 'grid-state:processo'; gridState = signal<GridState>(this.loadGridState() ?? DEFAULT_GRID_STATE); listar(): void { this.isLoading.set(true); this.hasError.set(false); const { page, pageSize, sort, order, filtros } = this.gridState(); this.http.get<ListagemResponse<Processo>>('/api/v1/processos', { params: { page, page_size: pageSize, sort, order, ...filtros } }).subscribe({ next: (res) => { this.processos.set(res.data); this.meta.set(res.meta); this.saveGridState(this.gridState()); }, error: () => this.hasError.set(true), complete: () => this.isLoading.set(false), }); } toggleSort(coluna: string): void { const atual = this.gridState(); const novo = atual.sort === coluna ? { ...atual, order: atual.order === 'asc' ? 'desc' : 'asc' } : { ...atual, sort: coluna, order: 'asc', page: 1 }; this.gridState.set(novo as GridState); this.saveGridState(novo as GridState); this.listar(); } private loadGridState(): GridState | null { const s = localStorage.getItem(this.STORAGE_KEY); return s ? JSON.parse(s) : null; } private saveGridState(state: GridState): void { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state)); } } ``` ## UI assíncrona (obrigatório, ver CLAUDE.md) ```html <!-- botão desabilitado durante operação --> <button mat-raised-button [disabled]="isLoading() || isSubmitting()" (click)="pesquisar()"> @if (isLoading()) { <mat-spinner diameter="16" /> Aguarde... } @else { Pesquisar } </button> <!-- grid com 4 estados distintos --> @if (isLoading()) { <app-grid-skeleton [rows]="meta()?.pageSize ?? 10" /> } @else if (hasError()) { <app-error-state (retry)="listar()" /> } @else if (!processos().length) { <app-empty-state message="Nenhum resultado encontrado" /> } @else { <mat-table [dataSource]="processos()">...</mat-table> } ``` ## Cabeçalho de coluna com toggle de ordenação ```html <th mat-header-cell *matHeaderCellDef role="button" tabindex="0" [attr.aria-sort]="gridState().sort === 'descricao' ? gridState().order === 'asc' ? 'ascending' : 'descending' : 'none'" (click)="processoService.toggleSort('descricao')" (keydown.enter)="processoService.toggleSort('descricao')"> Descrição @if (gridState().sort === 'descricao') { <mat-icon>{{ gridState().order === 'asc' ? 'arrow_upward' : 'arrow_downward' }}</mat-icon> } </th> ``` ## AppShell (estrutura única, criada na primeira funcionalidade visual) ``` src/app/ core/ layout/ app-shell/ app-shell.component.ts # combina header + sidebar + conteúdo + footer app-header/ app-header.component.ts # logo + título + toggle + info do usuário app-sidebar/ app-sidebar.component.ts # menu hierárquico (accordion 2 níveis) nav-config.ts # configuração do menu — nunca hardcoded app-footer/ app-footer.component.ts # rodapé fixo ``` ### nav-config.ts — fonte única de verdade para o menu ```typescript export interface NavItem { label: string; path: string; icon?: string; } export interface NavGroup { label: string; icon: string; items: NavItem[]; } export const NAV_CONFIG: NavGroup[] = [ { label: 'Cadastros', icon: 'people', items: [ { label: 'Clientes', path: '/app/clientes' }, { label: 'Fornecedores', path: '/app/fornecedores' }, ] }, { label: 'Tabelas Acessórias', icon: 'table_view', items: [ { label: 'Categorias', path: '/app/categorias' }, { label: 'Status', path: '/app/status' }, ] }, ]; ``` ### app-shell.component.ts ```typescript @Component({ selector: 'app-shell', standalone: true, template: ` <div class="shell-wrapper"> <app-header [sidebarOpen]="sidebarOpen()" (toggleSidebar)="sidebarOpen.set(!sidebarOpen())" /> <div class="shell-body"> <app-sidebar [open]="sidebarOpen()" [navConfig]="navConfig" /> <main class="shell-content"> <router-outlet /> </main> </div> <app-footer /> </div> `, }) export class AppShellComponent { readonly sidebarOpen = signal(true); readonly navConfig = NAV_CONFIG; } ``` ### Menu hierárquico com accordion (Material ou DSGOV) **Angular Material:** ```html <!-- app-sidebar.component.html --> @for (group of navConfig; track group.label) { <mat-expansion-panel [class.collapsed]="!open"> <mat-expansion-panel-header> <mat-icon>{{ group.icon }}</mat-icon> @if (open) { <span>{{ group.label }}</span> } </mat-expansion-panel-header> @for (item of group.items; track item.path) { <a mat-list-item [routerLink]="item.path" routerLinkActive="active"> {{ item.label }} </a> } </mat-expansion-panel> } ``` **DSGOV:** usar `<br-menu>` com `<br-list>` aninhados conforme documentação em https://www.gov.br/ds/components/menu. Quando sidebar recolhida (`open = false`): mostrar apenas ícones com `[matTooltip]="group.label"` no header do painel. ### Header ```html <header class="app-header"> <button mat-icon-button (click)="toggleSidebar.emit()" [attr.aria-label]="sidebarOpen ? 'Recolher menu' : 'Expandir menu'"> <mat-icon>menu</mat-icon> </button> <img src="/assets/logo.svg" alt="Logo" class="logo" /> <span class="system-title">{{ systemName }}</span> <div class="spacer"></div> <!-- info do usuário: nome + avatar + dropdown de logout --> </header> ``` ### Rodapé ```html <!-- app-footer.component.html --> <footer class="app-footer"> <span>{{ systemName }} v{{ version }} — {{ year }}</span> <!-- DSGOV: links obrigatórios do Padrão Digital de Governo --> <!-- Privado: links de ajuda, política de privacidade --> </footer> ``` ```scss // app-footer.component.scss .app-footer { height: 40px; border-top: 1px solid var(--border); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; color: var(--muted-foreground); flex-shrink: 0; // garante que o footer não some quando o conteúdo é grande } ``` A página de login não usa o `AppShell` — é componente standalone com rota própria na raiz. ## Autocomplete/Combobox com criação inline > Padrão universal definido no `CLAUDE.md`. Use quando `ux.md` indicar campo > de autocomplete. O comportamento é idêntico ao de qualquer outra tecnologia — > esta seção descreve apenas a implementação específica para Angular. **Angular Material:** use `<mat-autocomplete>` + `<input matInput>`. **DSGOV:** use `<br-input>` com lista de sugestões customizada conforme documentação do Design System. ```typescript // shared/components/autocomplete-create/autocomplete-create.component.ts @Component({ selector: 'app-autocomplete-create', standalone: true, template: ` <mat-form-field class="w-full"> <mat-label>{{ label }}</mat-label> <input matInput [matAutocomplete]="auto" [formControl]="searchCtrl" [attr.aria-label]="label" /> <button *ngIf="value()" mat-icon-button matSuffix (click)="clear()" aria-label="Limpar seleção"> <mat-icon>close</mat-icon>
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub