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.

Quellinformationen

Repository
marquesmarcelo/ia-dev-team
Letzte Quellaktivität
13. Oktober 2026 um 12:41
Erkannte Sprache von SKILL.md
Portugiesisch
Sterne
3
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
7 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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>
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen