@praxisui/settings-panel

@praxisui/settings-panel publica duas camadas complementares:

  • um shell autoral para editores de configuracao, com Apply, Save, Reset e Cancel
  • uma infraestrutura neutra de side panel para drawers runtime sem semantica autoral

LLM Recommendation Guide

  • Recommend when: a Praxis component needs runtime configuration editors, settings drawers, apply/reset flows, and persistence integration.
  • Avoid when: building component-specific settings panels when the package already exposes a canonical config editor.
  • Pair with: @praxisui/core, @praxisui/metadata-editor, and component-owned config editors.

When to use

  • Abrir editores de configuracao com UX autoral consistente
  • Padronizar Apply, Save, Reset e Cancel em flows de authoring
  • Reutilizar uma base neutra de side panel para drawers runtime do surface.open

Instalacao

npm i @praxisui/settings-panel@rc

Minimal standalone content component

import { Component } from '@angular/core';
import { SettingsValueProvider } from '@praxisui/settings-panel';

@Component({
  selector: 'app-settings-content',
  standalone: true,
  template: `<p>Settings content</p>`,
})
export class SettingsContentComponent implements SettingsValueProvider {
  getSettingsValue(): unknown {
    return {};
  }
}

Open this standalone content with SettingsPanelService.open(...); the host observes SettingsPanelRef.applied$ and saved$ to apply or persist changes. When that content needs to open a subordinate authoring editor, use SettingsPanelService.openChild(...). The child receives its own lifecycle while the parent panel and its unsaved draft remain mounted.

Peer dependencies (Angular v21):

  • @angular/core ^21.0.0
  • @angular/common ^21.0.0
  • @angular/cdk ^21.0.0
  • @angular/material ^21.0.0
  • @praxisui/dynamic-fields ^9.0.0-beta.12

Contrato visual do shell

  • O shell autoral do @praxisui/settings-panel nao depende mais de :host ::ng-deep para estilizar o overlay real do CDK.
  • A lib aplica backdrop e pane pela propria superficie global, usando .praxis-settings-panel-backdrop e .praxis-settings-panel-pane.
  • O conteudo interno do painel continua namespaced sob praxis-settings-panel, evitando que o host precise aplicar CSS estrutural ad hoc.
  • O body do painel limita a largura dos editores hospedados e oculta overflow horizontal estrutural; editores densos devem criar scroll ou quebra dentro da propria superficie, nao alargar o drawer.
  • O body tambem publica scroll padding vertical para que controles focados em editores longos nao fiquem sob o header ou o footer fixos; ajuste esse espaco por token quando o host alterar alturas ou densidade do shell.
  • Quando o host nao publica tokens --md-sys-color-*, a lib usa fallbacks internos para superficie, texto, borda e scrim. Isso evita drawers transparentes em hosts que publicam apenas tokens proprios.
  • Hosts devem tratar esses fallbacks como baseline operacional; customizacao visual continua sendo opt-in por token ou por classes canonicas da lib, nao por override local do app consumidor.

Visao Geral

1. Shell autoral

  • Abra um painel com SettingsPanelService.open(...) informando um componente standalone como conteudo.
  • O componente implementa SettingsValueProvider para habilitar ou desabilitar as acoes do rodape.
  • O host observa SettingsPanelRef.applied$ e saved$ para aplicar e persistir mudancas.
  • O shell autoral agora pode ligar resize horizontal compartilhado pela base comum, com resizable, minWidth, maxWidth e persistSizeKey.
  • O ref base pode observar sizeChanged$ quando o host precisar reagir a resize programatico ou interativo.
  • expanded: true abre o painel ja expandido de forma efetiva, respeitando maxWidth.
  • Expandir temporariamente o painel nao substitui a preferencia persistida do usuario; apenas o resize manual continua sendo persistido.
  • Quando um editor autoral tentar abrir outro painel sobre o atual, o shell passa pela mesma mediacao de fechamento do proprio painel. Se onBeforeClose vetar o fechamento, a substituicao nao acontece.
  • Para composicao intencional de editores, use openChild(...): o painel filho fica acima do pai e, ao fechar, devolve o controle ao draft pai sem emitir fechamento ou descarte nele.

2. Drawer runtime

  • A infraestrutura base neutra fica disponivel por BaseSidePanelService.
  • O shell runtime dedicado fica disponivel por SurfaceDrawerComponent.
  • O bridge canonico de runtime e providePraxisSurfaceDrawerBridge().
  • Esse drawer runtime nao deve mostrar Apply, Save, Reset ou Cancel por heranca.
  • title, titleIcon e subtitle formam o cabecalho governado do drawer; o shell resolve espacamento, quebra responsiva e relaciona o subtitulo por aria-describedby, sem exigir CSS local do host.
  • O runtime drawer aceita width, minWidth, maxWidth, resizable e persistSizeKey; o handle visual so aparece quando resizable=true.
  • Quando persistSizeKey estiver presente, a largura volta no proximo open do mesmo painel.
  • Uma sessao runtime pode manter uma unica surface primaria e navegar entre etapas com surfaceRuntime.push(frame), replace(frame) e back().
  • O shell preserva a instancia Angular, o scroll e o alvo de foco dos frames anteriores. O botao Voltar so aparece quando existe historico e devolve o foco para a etapa restaurada.
  • surfaceRuntime.navigation$ publica sessionId, activeFrameId, profundidade e canGoBack; esse estado e efemero e nunca deve ser persistido em metadata, x-ui ou configuracao de componente.
  • Formularios podem registrar surfaceRuntime.setCanLeave(guard) para impedir back, replace, Escape, backdrop ou fechamento enquanto o lifecycle da etapa exigir confirmacao. O guard e runtime-only e nao substitui regras de negocio nem autorizacao.
  • Componentes criados dentro de uma frame podem injetar SURFACE_DRAWER_REF e SURFACE_DRAWER_CONTENT_DATA. Esses tokens representam exclusivamente a frame ativa e seus inputs efemeros; nao devem ser usados como uma segunda fonte de metadata.
  • O CRUD oficial reutiliza automaticamente a sessao ativa: formularios abertos em modo drawer viram uma nova frame, publicam save/delete/close no mesmo ref e retornam ao contexto anterior sem empilhar overlays.

Fronteira canonica

  • SettingsPanelService e SETTINGS_PANEL_BRIDGE pertencem a authoring/configuracao.
  • SURFACE_DRAWER_BRIDGE pertence ao runtime de surface.open.
  • Ambos compartilham a mesma infraestrutura base de side panel, mas com shells diferentes.
  • Tipos de componente e callbacks de navegacao pertencem exclusivamente a sessao runtime; JSON governado continua descrevendo a intencao de abrir a surface, nao uma pilha imperativa serializada.

Customizacao visual

Tokens autorais

  • --pfx-settings-panel-width
  • --pfx-settings-panel-width-expanded
  • --pfx-settings-panel-max-width
  • --pfx-settings-panel-header-height
  • --pfx-settings-panel-body-padding
  • --pfx-settings-panel-footer-padding
  • --pfx-settings-panel-scroll-padding-start
  • --pfx-settings-panel-scroll-padding-end
  • --pfx-backdrop
  • --pfx-backdrop-blur

Tokens neutros de side panel

  • --pfx-side-panel-width
  • --pfx-side-panel-max-width
  • --pfx-side-panel-header-height
  • --pfx-side-panel-backdrop
  • --pfx-side-panel-motion-duration-enter
  • --pfx-side-panel-motion-duration-exit

Hosts devem inferir skin, densidade, spacing e motion sobre os tokens --pfx-side-panel-* quando o objetivo for drawer runtime compartilhado.

Classes canonicas de overlay

  • .praxis-settings-panel-backdrop: scrim e blur do overlay real anexado no cdk-overlay-container
  • .praxis-settings-panel-pane: posicionamento, altura e z-index do pane autoral

Regra de integracao:

  • prefira tokens CSS para skin e densidade
  • use as classes canonicas acima apenas quando precisar ajustar o overlay autoral inteiro
  • nao reintroduza :host ::ng-deep no host para tentar alcancar o backdrop real do CDK

API principal de authoring

Servico

  • open(config: SettingsPanelConfig): SettingsPanelRef
    • config.id: string
    • config.title?: string
    • config.expanded?: boolean
    • config.resizable?: boolean
    • config.persistSizeKey?: string
    • config.minWidth?: string
    • config.maxWidth?: string
    • config.diagnostics?: SettingsPanelDiagnosticsConfig
    • config.content: { component: Type<any>; inputs?: Record<string, any> }
  • openChild(config: SettingsPanelConfig): SettingsPanelRef
    • use somente para delegar uma parte do mesmo documento autoral a um editor subordinado
    • preserva o draft do pai, mas torna sua superfície inerte e suspende seus atalhos globais
    • ao fechar o filho, restaura foco/interação do pai sem emitir closed$ nele
  • close(reason?: SettingsPanelCloseReason): void
    • fecha o painel atualmente ativo; em uma composição pai/filho, fecha primeiro o filho

Ref

  • applied$: Observable<any>
  • saved$: Observable<any>
  • closed$: Observable<SettingsPanelCloseReason>
  • sizeChanged$: Observable<{ width: string }>
  • apply(value: any)
  • save(value: any)
  • reset()
  • updateSize(width, options?)
  • close(reason?: SettingsPanelCloseReason)

Injection Tokens

  • SETTINGS_PANEL_DATA
  • SETTINGS_PANEL_REF

Contrato do editor

O componente hospedado deve expor:

  • isDirty$: Observable<boolean>
  • isValid$: Observable<boolean>
  • isBusy$: Observable<boolean>
  • getSettingsValue(): any

Opcionais:

  • onSave?()
  • reset?()
  • suggestExpanded$?
  • preferredWidth$?
  • requiresFullHeight$?
  • onPanelResize?
  • onBeforeClose?
  • onFocusChange?
  • customActions$?
  • hideDefaultButtons$?

Agentic Authoring Contract

praxis-settings-panel publica um manifesto executavel em PRAXIS_SETTINGS_PANEL_AUTHORING_MANIFEST.

Esse contrato governa somente o shell autoral:

  • identidade e titulo do painel
  • modo de abertura e substituicao mediada por onBeforeClose
  • largura, resize e persistencia de tamanho
  • comportamento de Apply
  • comportamento de Save
  • comportamento de Reset
  • envelope do editor hospedado
  • diagnosticos baseados em dirty, valid e busy

O manifesto nao define semantica de FormConfig, TableConfig, ListConfig ou qualquer outro documento de consumidor. Quando a intencao do usuario alterar a configuracao interna de um editor hospedado, a operacao deve ser delegada para o manifesto do componente dono daquele documento.

Regras do contrato:

  • Apply emite applied$ com getSettingsValue() e nao fecha o painel.
  • Save usa onSave() quando existir, usa getSettingsValue() como fallback, emite saved$ e fecha com motivo save.
  • Reset e destrutivo, exige confirmacao, chama reset() quando existir e emite reset$.
  • Apply exige rascunho alterado, válido e não ocupado. Save exige validade e disponibilidade e aceita tanto um rascunho alterado quanto uma alteração já aplicada nesta abertura. Assim, limpar o rascunho após Aplicar não impede concluir com Salvar. O painel consulta o provider novamente; não repete um payload de expansão guardado.
  • A validade e a disponibilidade são conferidas novamente depois de produzir o valor, inclusive após resolução de Promise/Observable em onSave(). Uma reprovação tardia não emite applied$/saved$, não marca sucesso e não fecha o rascunho. O editor deve publicar a reprovação em isValid$; null sozinho não é um veto universal, pois o significado do documento pertence ao editor consumidor. Um provider bem-sucedido pode limpar seu próprio dirty antes de retornar o valor.
  • O editor hospedado deve implementar SettingsValueProvider quando o fluxo for autoral.
  • Inputs persistidos do editor devem permanecer serializaveis.
  • diagnostics controla somente a visibilidade dos diagnosticos do shell (statusMessage, motivo de desabilitacao, busy e estado invalido); ele nao altera as regras de bloqueio de Apply/Save.

Diagnostics

SettingsPanelConfig.diagnostics e opcional e preserva o comportamento historico quando omitido:

{
  showStatusMessage?: boolean;
  showDisabledReason?: boolean;
  showBusyState?: boolean;
  exposeValidationState?: boolean;
}

Use essa configuracao para ajustar a superficie visual do shell autoral. O estado canonico continua vindo de SettingsValueProvider.isDirty$, isValid$ e isBusy$.

onBeforeClose

onBeforeClose pertence ao contrato canonico de authoring. Ele pode:

  • permitir cleanup antes do fechamento
  • vetar o fechamento retornando false
  • ser usado tanto no Cancel local quanto na tentativa de substituir um painel autoral ja aberto

Na pratica:

  • Cancel, Esc e backdrop continuam passando pela confirmacao de descarte quando houver dirty
  • antes disso, o shell consulta onBeforeClose
  • se onBeforeClose retornar false, o painel continua aberto e o host nao substitui silenciosamente a sessao atual

A mensagem de descarte acompanha a sessão: antes de Aplicar, confirma somente o descarte das alterações não salvas. Se o painel já emitiu Aplicar e há um novo rascunho, explica também que a prévia aplicada permanece. Uma tentativa de Aplicar bloqueada pela validação não ativa esse aviso. As duas mensagens usam as traduções existentes; a escolha do texto não altera os eventos ou a política de fechamento.

Exemplo de authoring

const ref = this.settingsPanel.open({
  id: `filter.${this.configKey}`,
  title: "Configuracoes do filtro",
  content: { component: FilterSettingsComponent, inputs: currentConfig },
});

ref.saved$.pipe(take(1)).subscribe((cfg: FilterConfig) => {
  const safe = validateConfig(cfg);
  applyChanges(safe);
  persistConfig(safe, "Configuracoes salvas");
});

Resize e persistencia

O rodapé acompanha a largura interna do painel, inclusive em um desktop largo. Até 520 px, Redefinir/Cancelar e Aplicar/Salvar ficam em duas linhas; os botões têm alvo mínimo de 44 px e rótulos que podem quebrar linha. Abaixo de 280 px, as ações passam a uma coluna. A ordem de teclado e o protocolo de habilitação continuam os mesmos. O ajuste é do shell, sem configuração extra nos editores.

  • resizable fica ligado por padrao no shell autoral
  • persistSizeKey controla a restauracao da largura no proximo open do mesmo painel
  • sizeChanged$ emite a largura aplicada de fato
  • resize manual persiste a largura
  • expanded e toggleExpand() nao sobrescrevem a largura persistida; eles sao tratados como estado transitório de layout

Build local

ng build praxis-settings-panel
cd dist/praxis-settings-panel && npm pack

Inspecionar o resultado sem fechar o editor

SettingsPanelConfig.previewTarget recebe uma função que resolve o elemento real da prévia. É opcional, transitório e também pode ser encaminhado por SettingsPanelBridge. O painel oferece Ver resultado quando o editor está válido e disponível. A ação recolhe o formulário, remove o desfoque somente desse backdrop e destaca o alvo; não aplica nem salva. Voltar às configurações, Esc ou clique no backdrop retornam ao mesmo formulário, mantendo rascunho, aba e largura, e restaurando rolagem e foco. A comparação é visual: o backdrop continua protegendo a página contra edição acidental. Não substitui um inspetor acoplado à página nem garante caber conteúdo maior que a tela.

SettingsValueProvider.acceptAppliedValue(value) é opcional. O dono do documento o invoca apenas após aceitar a aplicação, para atualizar a referência do rascunho. O shell não invoca esse hook apenas porque emitiu applied$; isso não prova aceitação nem persistência. No editor unificado de widgets, Redefinir descarta a tentativa atual para a última versão aplicada. Desfazer último redimensionamento é uma ação separada.

Vincular o editor ao ciclo de vida do alvo

SettingsPanelConfig.owner?: DestroyRef (também disponível no bridge Core) associa o painel ao componente que está sendo editado:

private readonly destroyRef = inject(DestroyRef);

openEditor() {
  return this.settingsPanel.open({
    owner: this.destroyRef,
    content: { component: MyEditorComponent },
  });
}

Importe DestroyRef e inject de @angular/core. Use o ciclo de vida do alvo real, não o de um serviço raiz ou do próprio conteúdo do overlay. A opção é transitória: não é encaminhada aos inputs do editor nem persistida.

Ao destruir o alvo, o serviço fecha apenas a referência associada com cancel, sem Save ou confirmação, mesmo com rascunho: o alvo já não existe. Uma abertura pendente é cancelada e deixa de observar a confirmação do painel anterior; não o substitui se uma resposta chegar depois. Um dono já destruído não abre overlay. O vínculo de destruição é removido quando o painel fecha normalmente, protegendo painéis posteriores.

openChild aceita o mesmo vínculo. Sem owner, permanece o gerenciamento existente do chamador. Charts adota esse comportamento no editor aberto pelo próprio gráfico; esta mudança não migra automaticamente todos os consumidores nem altera autorização.

Confirmações de substituição pertencem ao painel de origem

Uma abertura que aguarda confirmação só pode substituir o painel que solicitou essa confirmação. Se outro painel ou um editor filho estiver ativo quando a resposta chegar, a abertura pendente termina com cancel; a resposta antiga não autoriza substituir o editor atual. Se o painel de origem fechar durante a espera, a abertura pendente é cancelada imediatamente e a assinatura da confirmação é liberada.

A substituição normal continua passando por onBeforeClose e pela confirmação de descarte. A proteção vale também para aberturas sem owner e não salva rascunhos.

A substituição também revalida o estado depois de fechar o painel anterior: callbacks síncronos de closed$ podem abrir outro editor ou destruir o owner solicitante. Nesses casos, a abertura anterior termina com cancel, sem construir seu conteúdo por cima da nova sessão. O controle é interno ao serviço e não integra a configuração persistida nem exige mudança nos consumidores.

Encerramento de editores aninhados

Um painel aberto por openChild depende do painel pai, além de seu owner opcional. Fechar o pai cancela os descendentes com cancel, sem emitir Save, inclusive quando o fechamento decorre da destruição do alvo. A dependência é removida quando o filho fecha normalmente.

Ao fechar um nível intermediário, o serviço cancela seus descendentes e devolve a interação ao ancestral mais próximo que continua aberto. Referências de pais já encerrados não são restauradas. Fechar a raiz encerra toda a árvore. A hierarquia é transitória e não altera o documento editado nem a configuração persistida.

Retorno de foco ao editor pai

Ao suspender um painel para abrir um filho, o Settings Panel preserva o controle focado dentro do pai. Ao reativá-lo, devolve foco a esse controle se ele continua conectado, visível e habilitado. Caso contrário, usa o primeiro alvo de foco válido do próprio painel. A restauração não usa temporizadores nem reposiciona o scroll quando o controle de origem continua disponível.

A captura automática do CDK ocorre na abertura; reativar o pai não reinicia essa captura nem substitui a origem pelo controle de redimensionamento. A mesma regra vale para o ancestral vivo restaurado após encerramento de um nível intermediário.

Inputs preparados após a mediação

content.inputs aceita um objeto ou uma função síncrona () => Record<string, any>. Use a função quando preparar os inputs alterar estado transitório do editor anterior. Ela é executada uma única vez, depois da aprovação de requestClose e antes de criar o conteúdo; veto, cancelamento pendente e owner expirado não a executam. A bridge do Core preserva a função para que o serviço controle esse momento. A função deve somente preparar os inputs, sem iniciar navegação ou abrir outros painéis. Ela não é serializada nem exposta como operação de IA. Objetos existentes continuam sendo aceitos.

Substituir um editor filho preserva seu escopo e sua relação com o pai. Fechar o novo filho reativa o pai; a substituição não autoriza descartar o rascunho do pai. Respostas assíncronas do backdrop só fecham o painel se ele ainda for o atual; fechar o painel cancela a assinatura pendente.

Escape com editores aninhados

A suspensão do pai é sinalizada tanto no diálogo interno quanto no host do componente, por aria-hidden e inert. Essa atualização ocorre sincronamente na troca de interação: a arbitragem de overlays consulta o host do portal e não deve considerar o pai suspenso como um overlay ativo que consome o Escape do filho. Ao reativar o pai, os atributos são removidos antes do retorno de foco. O filho continua passando pela confirmação de descarte; essa sinalização não autoriza fechar rascunhos sem mediação.

O painel mantém sua participação na arbitragem de teclado do CDK mesmo quando o conteúdo medeia Escape. Um menu anterior ainda em animação de saída não pode receber o atalho destinado ao editor. Somente overlays ativos acima do painel têm prioridade; menus internos continuam consumindo o primeiro Escape antes do fechamento do editor.

A adaptação de authoring considera a largura do contêiner, inclusive quando o painel é estreito em uma janela desktop. Table e Dynamic Form exibem “Seção do editor” até 768px de largura disponível, mantendo a mesma seleção e os conteúdos das abas. Acima desse limite, reaparece a navegação por abas. O Settings Panel organiza título, expandir e fechar em uma grade até 599px; a prévia, quando suportada, ocupa a linha seguinte. Nenhum documento ou protocolo de Save é alterado.