npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

publisher-property-editor

v0.5.0

Published

Editor de imóveis em React — componente único com contrato de injeção (services + commands), sem estado global nem dependência de rede.

Readme

publisher-property-editor

Editor de imóveis em React — um componente, dirigido por injeção. Você monta o <PropertyEditor>, passa os dados do imóvel e implementa as portas (services, commands, …); a biblioteca cuida da UI e da edição. Ela não fala com rede, não tem store global e não decide nada sobre persistência ou permissão — tudo isso entra por você.

| | | |---|---| | Pacote | publisher-property-editor | | Versão | 0.4.0 | | Formato | ESM — dist/index.js · tipos dist/index.d.ts · CSS dist/style.css | | Peer deps | react@^18.2 \|\| ^19 · react-dom@^18.2 \|\| ^19 |

Docs internas: arquitetura · decisões · atributos e modelos.

ÍndiceInstalação · Início rápido · Contrato de injeção · Props · services · commands · data · Permissões · Seções e layout · Roteamento · Atributos · Tipos.


Instalação

pnpm add publisher-property-editor

O CSS é distribuído à parte e é obrigatório — já embute os estilos de react-master-gallery, jaster-form e tippy.js:

import "publisher-property-editor/style.css";

Todos os estilos vivem sob o escopo .property-editor, aplicado na raiz do <main> renderizado — sem reset global nem vazamento pra fora.

react e react-dom são peer dependencies (você já os tem). Runtime extra que a lib traz e o seu bundler resolve: @radix-ui/*, @headlessui/react, react-select, react-master-gallery, @react-google-maps/api, moment, ramda e afins.


Início rápido

O mínimo para renderizar e salvar. services e commands têm suas seções abaixo — aqui só o essencial:

import PropertyEditor from "publisher-property-editor";
import "publisher-property-editor/style.css";

<PropertyEditor
  user={{ id: "usr_1", name: "Ana", email: "[email protected]", role: "master" }}
  attributes={[
    { key: "reference", value: "REF-001" },
    { key: "model", value: "[email protected]" },
    { key: "title", value: "Casa à venda no Centro" },
    { key: "published", value: true },
    // …definições do modelo + valores salvos (ver Atributos)
  ]}
  models={{ "[email protected]": { name: "casa-padrao", label: "Casa Padrão" } }}
  categories={[
    { name: "main", label: "Principal" },
    { name: "feature", label: "Benfeitorias" },
    { name: "improvement", label: "Características" },
  ]}
  permissions={["property:edit", "property:publish"]}
  settings={{ mobile: false }}
  services={services}      // ver "services"
  commands={commands}      // ver "commands"
  data={{}}
  onUpdate={(values) => salvarNoBackend(values)}
/>;

attributes é copiado para o estado interno na montagem. Alterações posteriores da prop são ignoradas — para carregar outro imóvel, remonte com key={propertyId}.


O contrato de injeção

A biblioteca dispara intenções; o host as realiza. As portas seguem sempre o mesmo princípio — a lib declara a necessidade, você decide a implementação.

| Porta | Prop | Obrig. | O host fornece | |---|---|:--:|---| | services | services | ✅ | I/O e leituras: CEP, cidades, integrações, menu de opções, upload | | commands | commands | ✅ | Ações de negócio disparadas pelo editor: salvar, publicar… | | services.notify | (em services) | — | Como apresentar notificações | | services.inspect / getInspectMenu | (em services) | — | Modo inspeção / itens do menu de contexto | | permissão | can / permissions | ✅¹ | Decisão de permissão por chave | | roteamento | <LinkProvider> | — | Componente de link do seu roteador | | layout | sections / chrome | — | Quais abas, em que ordem, e o cromo |

¹ permissions é obrigatório no tipo, mas ignorado se você passar can.

As opcionais têm fallback: sem notify o editor fica calado; sem inspect o modo inspeção não existe; sem can a permissão vem de permissions[]; sem LinkProvider os links viram âncoras; sem sections usa o conjunto padrão com cromo.


Props

| Prop | Tipo | Obrig. | Descrição | |---|---|:--:|---| | attributes | AttributeType[] | ✅ | Estado editável do imóvel. Ver Atributos. | | models | { [id]: { name, label } } | ✅ | Catálogo de modelos. O label do modelo corrente aparece no cabeçalho. | | user | User | ✅ | Usuário logado. Define o ACL junto de permissions/can. | | permissions | string[] | ✅ | Chaves de permissão do usuário. Ignorado se can for passado. | | categories | { name, label }[] | ✅ | Grupos da aba Atributos. name casa com categories.view do atributo. | | services | PropertyServices | ✅ | Porta de I/O. Ver services. | | commands | CommandFunctions | ✅ | Porta de ações. Ver commands. | | settings | Settings | ✅ | { mobile?, readOnly?, darkMode? } — só mobile tem efeito hoje. | | data | object | ✅ | Semente do estado volátil. Pode ser {}. | | onUpdate | (data) => any | ✅ | Repassado ao contexto; quem chama é o seu command, não o componente. | | owner | User | — | Responsável pelo imóvel. Exibido no rodapé e usado no ACL de edição. | | can | PermissionCheck | — | Verificação de permissão injetada. Ver Permissões. | | sections | SectionSchema[] | — | Quais abas e em que ordem. Ver Seções. | | chrome | boolean | — | false renderiza só as seções, sem cabeçalho/abas/rodapé. Padrão true. | | active | boolean | — | Foco do painel. false esmaece e desliga o modo inspeção. Omitido, detecta por clique. | | pinnedAttributes | string[] | — | Chaves fixadas no topo da aba Atributos. | | tags | string[] | — | Tags do imóvel. "highlighted" liga o selo de destaque. |

Comportamento a conhecer

  • attributes é copiado no mount — remonte com key para trocar de imóvel.
  • settings.readOnly e settings.darkMode existem no tipo mas ainda não têm efeito.
  • user.role === "master" (ou user.master === true) concede tudo, ignorando permissions — salvo se você injetar can (aí a decisão é 100% sua).

services — porta de I/O

Objeto implementado por você. Tudo que sai para a rede — e o menu de opções — passa aqui.

type PropertyServices = {
  // Menu "Opções" do cabeçalho — obrigatórios
  getOptions(context: TableContext): Item[];
  handleAction(key: string, item: any, context: TableContext): void;

  // Aba Localização
  fetchAddress(cep: string): Promise<{ city; uf; address; neighborhood; complement? }>;
  fetchCities(): Promise<{ name: string; state: string }[]>;
  createCity(data: { name: string; state: string }): Promise<any>;
  fetchDistricts(data: { city: string }): Promise<{ name: string }[]>;
  createDistrict(data: { name; city; state }): Promise<unknown>;

  // Aba Principal — carga preguiçosa de selects
  loadTypes(model: string, context: TableContext): Promise<Option[]>;
  fetchItem(key: "client" | "agent", context: TableContext): Promise<Option[]>;

  // Aba Publicação
  fetchIntegrations(): Promise<Integration[]>;

  // Opcionais
  notify?(input: NotifyInput): void;
  inspect?(key: string, context: TableContext, action?: string): void;
  getInspectMenu?(key: string, context: TableContext): InspectMenuItem[];
};

Option = { value: string; label: string }. Item = a linha do menu de opções: { key, label, icon?, link?, target?, disabled?, reason? }.

Quando cada um é chamado

| Método | Gatilho | |---|---| | getOptions | A cada render do cabeçalho. Deve ser puro e barato. | | handleAction | Clique num item do menu Opções. Recebe o key que você definiu. | | fetchAddress | Botão Atualizar endereço. A resposta preenche rua, cidade e bairro. | | fetchCities | Montagem da aba Localização. | | fetchDistricts | Sempre que a cidade selecionada muda. | | createCity / createDistrict | Após fetchAddress trazer cidade/bairro fora da lista, com confirmação. | | loadTypes | 1º render do select Tipo. Resultado cacheado em data.options. | | fetchItem | 1º render dos selects Cliente/Corretor. Idem cache. | | fetchIntegrations | Montagem da aba Publicação. | | notify | Cadastro de cidade/bairro e falha ao buscar CEP. |

Contrato morto: o tipo ainda exige dispatch, setPropertyTags e fileUpload — o componente nunca os chama (upload passa pelo command handleUpload). Implemente como no-op até serem removidos do tipo.

notify — notificação sem impor UI

A lib não embute UI de notificação: descreve a intenção, você apresenta. Opcional — sem ela, o editor segue calado.

type NotifyInput =
  | { type?: "info" | "success" | "warning" | "error"; message: string }
  | { promise: Promise<unknown>; pending?: string; success?: string; error?: string };

const notify: PropertyServices["notify"] = (input) => {
  if ("promise" in input) {
    const { promise, ...messages } = input;
    toast.promise(promise, messages); // acompanha pending/success/error
    return;
  }
  toast[input.type ?? "info"](input.message);
};

Menu de opções — getOptions / handleAction

O menu Opções do cabeçalho é 100% seu: getOptions devolve a lista, o clique vai para handleAction. Itens aceitam disabled + reason — um item bloqueado aparece apagado, com cadeado e o motivo num tooltip, sem sumir (mais honesto que ocultar):

const getOptions: PropertyServices["getOptions"] = ({ can }) => [
  { key: "publish", label: "Publicar", icon: <World /> },
  { key: "remove", label: "Excluir", icon: <Trash />,
    disabled: !can("property:delete"),
    reason: "Sem permissão para excluir o imóvel" },
];

const handleAction: PropertyServices["handleAction"] = (key, item, { command }) => {
  if (key === "publish") command("publish");
  if (key === "remove") command("DeleteProperty");
};

Modo inspeção — inspect / getInspectMenu

Ajuda a documentar a própria ferramenta. Opcional: sem inspect, o modo não existe.

  • ALT+clique: com ALT segurado e o painel ativo, os componentes inspecionáveis (publicar, salvar, opções, atualizar endereço) acendem; o clique — em vez de agir — dispara inspect(key, context). Funciona até em botões desabilitados.
  • Clique-direito: abre um menu cujos itens você injeta por getInspectMenu — rótulos (no seu idioma), ícones, quais itens. No clique de um item, a lib chama inspect(key, context, item.action). A biblioteca não conhece item algum.
type InspectMenuItem = { action: string; label: string; icon?: ReactNode; disabled?: boolean };

const getInspectMenu: PropertyServices["getInspectMenu"] = (key) => [
  { action: "about", label: "Sobre", icon: <Info /> },
  { action: "action", label: "O que faz", icon: <Bolt /> },
];

const inspect: PropertyServices["inspect"] = (key, _ctx, action) => {
  abrirDrawer(key, action); // você controla o conteúdo e o layout
};

A prop active controla o foco quando há vários painéis na tela.


commands — porta de ações

Mapa { [chave]: (props, context, controls?) => any }. O componente dispara uma chave; você decide o que ela faz. Nenhum é obrigatório — chave ausente é ignorada em silêncio.

Disparados pelo componente:

| Chave | Quando | props | |---|---|---| | onSubmit | Botão Salvar alterações | { values: { [key]: value }, modifiedAt } | | publish | Botão de publicação (e menu) | { id, isUpdate } | | archive | Menu Opções (via run no handleAction) | { id } | | handleUpload | Seleção ou arraste de imagens | { id, files: File[] } | | Attributes_${name} | Menu de contexto de um atributo (jaster-form) | { name, item } |

Commands de ciclo assíncrono (publish/archive/onSubmit) — o padrão recomendado

Um command é uma operação do host — no modelo recomendado, uma função pura (props) => Promise<Result>. Quem cuida do estado visual (loading, publishState, lock) é o editor, pelo ciclo da promise. O command só:

  1. faz qualquer confirmação/pré-condição;
  2. chama controls.begin() quando a operação de verdade começa;
  3. resolve com o resultado (ex.: { modified_at }), que o editor aplica ao data.
export const publish: CommandFunction = async ({ id, isUpdate }, _ctx, controls) => {
  if (!isUpdate) await confirmar();   // antes de begin → sem "publicando..." mentiroso
  controls?.begin();                  // editor mostra o estado pendente + trava o botão
  return api.publish(id);             // Promise<{ modified_at }>
};

O begin é o único acoplamento à apresentação — e é opt-in: um command que não o chama mantém o comportamento antigo (transiciona publishState você mesmo via context.setData). Na rejeição após begin, o editor reverte o estado; o feedback rico do erro é seu (mostre-o dentro da operação, antes de rejeitar). Rejeitar antes de begin (ex.: cancelar a confirmação) não muda nada.

Toda ação do menu Opções (destacar, duplicar, excluir, trocar responsável…) não é disparada pelo componente — vai do seu getOptions para o seu handleAction, que decide o que fazer. Para dar a uma ação de menu o ciclo async do editor (como archive), o handleAction chama context.run("archive", { id }) em vez de command.

O 2º argumento é o context (leituras e o estilo antigo); o 3º é controls ({ begin }). A superfície de context (service, command, run, runAsync, data, setData, services, user, can…) está em docs/ARCHITECTURE.md.


data — estado volátil

Estado de UI que o componente e os commands compartilham — não é persistido. A prop data semeia; a maior parte é gerenciada pelo próprio editor.

| Campo | Tipo | Efeito | |---|---|---| | publishState | "published" \| "archived" \| "publishing" \| "archiving" \| "synchronizing" | Define o botão do cabeçalho. Ausente, é derivado do atributo published. | | publishLock | boolean | Desabilita o botão de publicação. | | syncRequired | boolean | Troca Público pelo Atualizar pulsante. | | deleted | boolean | Substitui o editor pela tela de imóvel excluído. | | tags | string[] | "highlighted" exibe o selo de destaque. | | pinnedAttributes | string[] | Atributos fixados no topo da aba Atributos. | | modifiedAt | string | ISO date exibida no rodapé. | | options | { [attrKey]: Option[] } | Cache dos selects. Gerenciado pelo componente. |

Os estados de ciclo (publishing/archiving/synchronizingpublished/archived) são dirigidos pelo editor pelo ciclo da promise do command — você não os transiciona. Basta o command chamar controls.begin() e resolver com { modified_at } (ver commands). O modifiedAt do resultado é aplicado aqui.


Permissões — can

O editor consulta permissões por chaves nomeadas — um vocabulário fixo:

| Chave | Lida pela biblioteca? | Efeito | |---|:--:|---| | property:edit | ✅ | Sem ela, o botão Salvar alterações desabilita. | | property:publish | ✅ | Sem ela, o botão de publicação desabilita. | | property:private · property:delete · … | — | Convenção sua — quem consome é o seu getOptions. |

Duas formas de responder:

  • Simples — passe permissions: string[]. A lib deriva: master || permissions.includes(key).
  • Injetada — passe can: (key) => boolean e mapeie cada chave para o ACL real do seu app. Aí can é a autoridade final: a lib não aplica regra própria por cima (nem o bypass de master) e permissions é ignorado.
<PropertyEditor can={(key) => app.can(MAPA[key])} … />

Seções e layout — sections

As abas do editor vêm de um schema. Omitido, usa o conjunto padrão; passe sections para reordenar, filtrar, renomear, exigir permissão ou registrar seções próprias.

type SectionSchema =
  | string                               // chave do registro embutido
  | {
      type: string;                      // chave, ou "spacer"
      variant?: "full" | "icon";         // ícone+texto (padrão) ou só ícone (tooltip)
      label?: string;                    // renomear a aba
      icon?: ReactNode;                  // trocar o ícone
      permission?: string;               // só aparece se can(permission)
      component?: (props) => ReactNode;  // seção própria (substitui a do registro)
    };

Seções embutidas (chave → rótulo padrão): main→Principal, attributes→Atributos, address→Localização, media→Mídias, publish→Publicação, inspect→Análise, private→Privado.

  • { type: "spacer" } divide a barra em duas zonas: à esquerda as abas normais; à direita, as seções seguintes viram um grupo compacto só-ícone (auxiliares como Análise e Privado).
  • chrome={false} renderiza apenas as seções (sem cabeçalho/abas/rodapé) — útil para exibir uma página isolada, ex.: sections={["address"]} chrome={false}.
<PropertyEditor
  sections={[
    "main",
    "attributes",
    { type: "media", label: "Fotos" },              // renomeada
    { type: "publish", permission: "property:publish" }, // condicional
    "spacer",
    "inspect",                                       // vira botão à direita
    "private",
  ]}
  …
/>

Para montar uma UI de configuração de layout, a lib exporta o catálogo sem arrastar as seções pesadas:

import { SECTION_META, DEFAULT_SECTIONS } from "publisher-property-editor";
// SECTION_META: { type, label }[]   ·   DEFAULT_SECTIONS: string[]

Roteamento — LinkProvider

Onde o editor renderiza um link (menu de opções, integrações), ele usa um Link injetável. Sem configuração, é uma âncora comum (navegação com recarga). Para preservar a navegação SPA do seu app, injete o Link do seu roteador:

import PropertyEditor, { LinkProvider } from "publisher-property-editor";
import { Link as RouterLink } from "react-router-dom";

<LinkProvider component={({ href, ...props }) => <RouterLink to={href} {...props} />}>
  <PropertyEditor … />
</LinkProvider>;

Atributos

O imóvel não é um objeto tipado — é uma lista plana de AttributeType:

type AttributeType = {
  key: string;                       // identificador único e estável
  value?: any;                       // valor do usuário (ausente = não preenchido)
  type?: "bool" | "number" | "options" | string;
  label?: string;
  categories?: { view?: string };    // casa com o `name` de `categories`
  $custom?: boolean;                 // atributo fora do modelo
};

Monte a lista concatenando identidade + definições do modelo + valores salvos. As chaves reservadas (id, reference, model, title, published, media.pictures, geo-position, $private…), os tipos, as categorias e os modelos estão detalhados em docs/ATTRIBUTES.md.


Tipos exportados

import PropertyEditor, {
  // valores
  Link, LinkProvider, useLinkComponent,
  SECTION_META, DEFAULT_SECTIONS,
} from "publisher-property-editor";

import type {
  PropertyProps, PropertyServices, CommandFunction, CommandFunctions, CommandControls,
  User, Integration, AttributeGroup, Settings,
  PermissionCheck, NotifyInput, NotifyType, InspectMenuItem,
  SectionSchema, SectionMeta,
  TableContext, AttributeType, AttributeMethods,
  LinkProps, LinkComponent,
} from "publisher-property-editor";

Desenvolvimento

Monorepo pnpm. Da raiz:

pnpm install
pnpm dev                                   # playground (apps/playground) em Vite
pnpm -C packages/property-editor build     # dist/index.js + .d.ts + style.css
pnpm -C packages/property-editor test      # Vitest
pnpm -C packages/property-editor typecheck

Build com tsup (ESM + tipos) + Tailwind CLI para o CSS. O playground implementa services/commands com mocks e é a referência viva de integração — se em dúvida sobre como implementar uma porta, olhe apps/playground/src/editor-host/.