iGRPIGRP 3.0Documentation

Header Slots

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:

SlotPosiçãoRenderiza quando
startRegião esquerda, após o logo/título, antes dos breadcrumbspassas um nó
searchCluster direito, primeiro — substitui a command palette integradashowSearch: true
settingsCluster direito — substitui o link de definições integradoshowSettings: true
notificationsCluster direito — substitui o dropdown de notificações integradoshowNotifications: true
actionsCluster direito, após notificações, antes do theme switcherpassas um nó

Dois estilos de controlo, e a diferença importa:

  • search, settings e notifications ocupam posições que o framework já conhece, pelo que a flag show* correspondente em getHeaderData() decide se a posição renderiza de todo. Definir showSearch: false e passar um slot search não renderiza nada — a flag ganha.
  • start e actions nã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.


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:

FicheiroPapel
src/lib/header-search.tsServidor. 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.tsxCliente ('use client'). Mapeia esses dados para IGRPCommandItem[], constrói cada handler onSelect, e renderiza IGRPTemplateCommandSearch.
src/app/(igrp)/layout.tsxAguarda 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.


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ável

A 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 é slotPorquê
Theme switcherComportamento do framework ligado ao next-themes
Nav-user menuLigado à sessão e ao sign-out; substituí-lo é como a UX de auth se parte
Logo, título, breadcrumbs, sidebar triggerConfigurados via flags de getHeaderData() e props breadcrumbs / breadcrumbRouteLabels
O cabeçalho como um todoNã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:

ComponenteProps principais
IGRPTemplateCommandSearchcommands: IGRPCommandItem[]
IGRPTemplateNotificationsnotifications, notificationsUrl, onMarkAllRead
IGRPAuthorizationpermission
usePermissionshook — lista de permissões do utilizador atual
How is this guide?

Última atualização: