Pular para o conteúdo

Pacote React

Coloque o Opini direto no seu app React. Quatro níveis de estilo, do pronto para usar ao totalmente sem interface. Inputs comuns, sem componentes de campo: seu layout continua seu.

Instalar

bash
# pnpm
pnpm add @opini-dev/react

# npm
npm install @opini-dev/react

Requer React 18 ou 19 como peer dependency. Só ESM. Seguro para renderização no servidor: os portais do modo flutuante só montam depois do primeiro efeito no cliente.

Nível 1: pronto para usar

Um componente. Os estilos padrão já vêm ligados, então ele parece um widget de feedback de cara.

tsx
import { OpiniWidget } from "@opini-dev/react"

export default function App() {
  return <OpiniWidget projectKey="pk_live_xxx" />
}

Passe unstyled se quiser começar do zero.

Nível 2: classes por parte

Aplique classes do Tailwind, CSS Modules ou CSS puro às partes estruturais. O estilo dos campos fica no HTML do seu formulário (veja o Nível 3).

tsx
<OpiniWidget
  projectKey="pk_live_xxx"
  unstyled
  className="fixed bottom-6 right-6"
  classNames={{
    trigger: "rounded-full bg-black px-5 py-2.5 text-sm text-white",
    popover: "w-96 rounded-2xl border bg-white p-5 shadow-xl",
    submit: "rounded-md bg-violet-600 px-3 py-2 text-white hover:bg-violet-700",
  }}
/>

Para transições só com CSS, cada parte expõe data-state="open|closed" e data-loading="true|false". Quem usa Tailwind pode ligar entrada e saída com data-[state=open]:scale-100.

Nível 3: peças combináveis

Use seu próprio botão, seus textos e seu layout de campos. <input> e <textarea> comuns com name="message", "email" ou "name" são o contrato dos campos.

tsx
import {
  OpiniWidgetRoot,
  OpiniTrigger,
  OpiniPopover,
  OpiniForm,
  OpiniSubmit,
  OpiniSuccess,
} from "@opini-dev/react"

<OpiniWidgetRoot projectKey="pk_live_xxx">
  <OpiniTrigger asChild>
    <MyBrandedButton>Send feedback →</MyBrandedButton>
  </OpiniTrigger>
  <OpiniPopover className="my-popover">
    <h2>Pitch a chapter.</h2>
    <OpiniForm className="space-y-3">
      <textarea
        name="message"
        placeholder="Albums that auto-curate themselves on a trip…"
      />
      <input name="name" placeholder="name (optional)" />
      <input name="email" type="email" placeholder="email" />
      <OpiniSubmit asChild>
        {({ status }) => (
          <MyBrandedButton variant="primary" disabled={status === "submitting"}>
            {status === "submitting" ? "Sending…" : "SEND IT →"}
          </MyBrandedButton>
        )}
      </OpiniSubmit>
    </OpiniForm>
    <OpiniSuccess>You'll hear back. Promise.</OpiniSuccess>
  </OpiniPopover>
</OpiniWidgetRoot>

Nível 4: useOpiniSubmit (sem interface)

Quando você quer o cliente da API e a máquina de estados sem nenhuma interface.

tsx
import { useOpiniSubmit } from "@opini-dev/react"

function MyOwnForm() {
  const { submit, status, error } = useOpiniSubmit({ projectKey: "pk_live_xxx" })
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const fd = new FormData(e.currentTarget)
        void submit({
          text: String(fd.get("message") ?? ""),
          email: fd.get("email") ? String(fd.get("email")) : undefined,
        })
      }}
    >
      <textarea name="message" />
      <input name="email" />
      <button disabled={status === "submitting"}>Send</button>
      {error ? <p>{error.message}</p> : null}
    </form>
  )
}

Props

Todas as props são tipadas, então seu editor as lista enquanto você digita. Não existe a prop customCss: esse campo no painel vale só para o widget por tag de script. No React você estiliza com o que já usa.

Nomes das partes

A lista completa: trigger, popover, arrow, close, form, actions, submit, success, msg. Cada parte também tem um atributo data-opini-part="…" para quem usa CSS-in-JS.

Contrato dos campos

<OpiniForm> lê o formulário com FormData ao enviar. São aceitos exatamente três campos:

  • name="message": obrigatório, o texto do feedback.
  • name="email": opcional, validado quando presente.
  • name="name": opcional, texto livre.

Todo o resto é descartado antes do POST, então você pode adicionar seus próprios campos de estado sem poluir o envio.

SSR

O conteúdo dos portais do modo flutuante só renderiza depois do primeiro efeito no cliente; não há saída de portal no HTML inicial. A hidratação não faz nada. O modo inline renderiza direto na sua árvore, no servidor e no cliente.