Como renderizar componentes próprios no cabeçalho iGRP — substituir a pesquisa, notificações ou definições, ou adicionar ações personalizadas.
Como renderizar componentes próprios no cabeçalho do template — substituindo a pesquisa integrada, as notificações ou o controlo de definições, ou adicionando slots inteiramente novos.
O modelo
O cabeçalho é renderizado pelo framework, não pela tua app:
(igrp)/layout.tsx [server]
→ IGRPLayoutFull @igrp/framework-next [server]
→ HeaderDataProvider (dentro de <Suspense>) [server]
getHeaderData() → flags, logo, URLs
fetchCurrentUser() → utilizador autenticado
→ IGRPTemplateHeader @igrp/framework-next-ui ['use client']Um slot entrega ao framework um nó para renderizar numa posição que ele gere. O framework continua a gerir todos os dados do cabeçalho — utilizador, logo, breadcrumbs, sidebar trigger. Um slot substitui o que renderiza numa posição, nunca de onde vêm os dados. Não abres mão de nada ao usá-lo.
Os slots
Passa-os como headerSlots em IGRPLayoutFull:
| Slot | Posição | Renderiza quando |
|---|---|---|
start | Região esquerda, após o logo/título, antes dos breadcrumbs | passas um nó |
search | Cluster direito, primeiro — substitui a command palette integrada | showSearch: true |
settings | Cluster direito — substitui o link de definições integrado | showSettings: true |
notifications | Cluster direito — substitui o dropdown de notificações integrado | showNotifications: true |
actions | Cluster direito, após notificações, antes do theme switcher | passas um nó |
Dois estilos de controlo, e a diferença importa:
search,settingsenotificationsocupam posições que o framework já conhece, pelo que a flagshow*correspondente emgetHeaderData()decide se a posição renderiza de todo. DefinirshowSearch: falsee passar um slotsearchnão renderiza nada — a flag ganha.starteactionsnão têm flag. Passar o nó é o contrato completo.
As flags show* vivem em src/igrp.template.config.ts sob layoutMockData.getHeaderData().
Quick start
// src/app/(igrp)/layout.tsx
import { IGRPLayoutFull } from "@igrp/framework-next";
import { EnvBadge } from "@/components/header/env-badge";
import { MyNotifications } from "@/components/header/my-notifications";
export default async function Layout({ children }: { children: React.ReactNode }) {
const config = await createConfig(layoutConfig);
return (
<IGRPLayoutFull
config={config}
headerSlots={{
start: <EnvBadge />,
notifications: <MyNotifications />,
}}
>
{children}
</IGRPLayoutFull>
);
}Os slots são elementos (<MyThing />), não referências a componentes (MyThing). Ver Regras e armadilhas.
Exemplo de referência: command palette no slot search
O template de referência implementa um slot real: a command palette, ligada através de search. O padrão divide-se em dois ficheiros:
| Ficheiro | Papel |
|---|---|
src/lib/header-search.ts | Servidor. Resolve os menus da aplicação em dados serializáveis para os comandos. Usa isAuthBypass() — menus mock em bypass, fetchMenusAction(appCode) caso contrário. Retorna [] em falha. |
src/components/header/app-search.tsx | Cliente ('use client'). Mapeia esses dados para IGRPCommandItem[], constrói cada handler onSelect, e renderiza IGRPTemplateCommandSearch. |
src/app/(igrp)/layout.tsx | Aguarda os comandos e passa o elemento como slot search. |
A ligação completa no layout resume-se a três linhas:
const searchCommands = await getHeaderSearchCommands(config);
<IGRPLayoutFull
config={config}
headerSlots={{ search: <AppSearch commands={searchCommands} /> }}
>Porquê dois ficheiros?
IGRPTemplateHeader monta o IGRPTemplateCommandSearch sem prop commands, por isso ⌘K / Ctrl+K abre uma palette vazia. O slot search é como a app lhe dá conteúdo.
A separação entre os dois ficheiros não é estilística: decidir se um URL é externo requer comparar com a origem atual, que só o browser conhece (igrpIsExternalUrl lê window.location.origin). O servidor passa pageSlug / url / target sem resolver, e o cliente decide no clique.
Receita: substituir as notificações
O dropdown integrado renderiza o que getHeaderData() retorna em notifications — um array estático. Para o alimentar com a tua própria API:
// src/components/header/my-notifications.tsx
"use client";
import { useQuery } from "@tanstack/react-query";
import { IGRPTemplateNotifications } from "@igrp/framework-next-ui";
export function MyNotifications() {
const { data } = useQuery({
queryKey: ["notifications"],
queryFn: fetchNotifications,
refetchInterval: 60_000,
});
return (
<IGRPTemplateNotifications
notifications={data ?? []}
onMarkAllRead={markAllRead}
/>
);
}Reutiliza IGRPTemplateNotifications para manter o visual do framework, ou renderiza algo inteiramente teu. IGRPQueryProvider já está montado acima de IGRPLayoutFull, por isso react-query funciona dentro de um slot.
Comportamento no nav-user
IGRPTemplateNavUser tem uma entrada de link "Notificações" no seu dropdown, controlada pela mesma flag showNotifications. Quando passas um slot notifications, o framework suprime essa entrada — evitando dois pontos de entrada para o mesmo recurso. O teu slot não é ocultado em ecrãs pequenos; o sino integrado é (hidden md:block) precisamente porque o nav-user cobre mobile.
Receita: adicionar ações próprias
actions é o slot aberto. Compõe um fragment e controlas a ordem interna:
headerSlots={{
actions: (
<>
<HelpButton />
<LanguageSwitcher />
<IGRPAuthorization permission="manage_access">
<AdminShortcut />
</IGRPAuthorization>
</>
),
}}É um único ReactNode, não um array, por isso o framework nunca itera sobre nós que não keyed. Aplica os teus próprios permission-gates a cada item individualmente.
Receita: badge junto ao logo
start situa-se na região esquerda, entre o logo/título e os breadcrumbs, e recebe um separador automático quando algo o precede:
headerSlots={{ start: <IGRPBadge variant="outline">STAGING</IGRPBadge> }}É colocado antes dos breadcrumbs propositadamente: os breadcrumbs colapsam sob pressão de layout, e um slot depois deles seria o primeiro a desaparecer num viewport estreito.
Slots servidor assíncronos
Um elemento de slot pode ser produzido por um Server Component assíncrono — útil quando os dados estão atrás de um cliente server-only:
async function TenantSwitcher() {
const tenants = await fetchTenants(); // server-side
return <TenantSwitcherClient tenants={tenants} />;
}
headerSlots={{ actions: <TenantSwitcher /> }}Cada slot renderiza dentro da sua própria <Suspense> com um placeholder do tamanho de um ícone, por isso um slot lento não atrasa o logo, breadcrumbs ou o menu do utilizador. Para controlar o fallback, envolve o teu slot:
headerSlots={{
actions: (
<Suspense fallback={<MySkeleton />}>
<TenantSwitcher />
</Suspense>
),
}}Regras e armadilhas
Passa elementos, não componentes. IGRPTemplateHeader é um Client Component renderizado a partir de um Server Component. Elementos cruzam essa fronteira; funções não.
headerSlots={{ search: <AppSearch /> }} // ✅
headerSlots={{ search: AppSearch }} // ❌ função não é serializávelA mesma regra aplica-se às props que passas para dentro de um slot. IGRPCommandItem.onSelect é um callback — um módulo servidor não pode construir os itens. Passa dados simples do servidor e constrói os handlers dentro do componente cliente.
Navega com o router, não com window.open. router.push() aplica basePath (NEXT_PUBLIC_BASE_PATH); window.open() não, por isso um path interno aberto dessa forma dá 404. Reserva window.open para URLs genuinamente noutro origin.
Um url absoluto não é automaticamente externo. Um item de menu pode endereçar uma página interna por URL (ex.: /reports). Decide com igrpIsExternalUrl(url) (ou um target: "_blank" explícito), nunca por ter url sem pageSlug.
typedRoutes está ativo. next.config.ts define typedRoutes: true, por isso router.push() é tipado contra rotas estaticamente conhecidas. Targets resolvidos de dados de runtime precisam de um cast:
router.push(href as Parameters<typeof router.push>[0]);Slots são código client-boundary. Qualquer coisa que importe do design system precisa de 'use client'.
O que não é injetável
| Não é slot | Porquê |
|---|---|
| Theme switcher | Comportamento do framework ligado ao next-themes |
| Nav-user menu | Ligado à sessão e ao sign-out; substituí-lo é como a UX de auth se parte |
| Logo, título, breadcrumbs, sidebar trigger | Configurados via flags de getHeaderData() e props breadcrumbs / breadcrumbRouteLabels |
| O cabeçalho como um todo | Não existe prop de substituição completa — isso entregaria-te a gestão de dados (getHeaderData(), fetchCurrentUser() e o re-seed do access-client que HeaderDataProvider faz). Usa slots. |
Para ocultar um elemento integrado em vez de substituir, muda a sua flag em getHeaderData() (showSearch, showSettings, showNotifications, showThemeSwitcher, showUser, showBreadcrumb, …).
Preview mode
Os slots são indiferentes à autenticação — renderizam da mesma forma com IGRP_PREVIEW_MODE=true ou AUTH_PROVIDER=none e com backend real. Qualquer dado que um slot resolva é responsabilidade tua de ramificar, e isAuthBypass() de @/lib/utils é a única forma correta de verificar.
Referência da API
// @igrp/framework-next-ui
export type IGRPHeaderSlots = {
start?: React.ReactNode;
search?: React.ReactNode;
notifications?: React.ReactNode;
settings?: React.ReactNode;
actions?: React.ReactNode;
};
// @igrp/framework-next
export type IGRPLayoutFullArgs = {
readonly children: React.ReactNode;
readonly config: IGRPConfigArgs;
readonly showSidebar?: boolean;
readonly breadcrumbs?: BreadcrumbItem[];
readonly breadcrumbRouteLabels?: Record<string, string>;
readonly headerSlots?: IGRPHeaderSlots;
};Componentes reutilizáveis dentro de um slot, todos de @igrp/framework-next-ui:
| Componente | Props principais |
|---|---|
IGRPTemplateCommandSearch | commands: IGRPCommandItem[] |
IGRPTemplateNotifications | notifications, notificationsUrl, onMarkAllRead |
IGRPAuthorization | permission |
usePermissions | hook — lista de permissões do utilizador atual |
Última atualização:
