Pular para o conteúdo
UI

Note Editor

Editor de texto rico para as anotações dos alunos, construído sobre o Tiptap. O modelo de documento e o schema ficam em @medway-ui/notes-core, compartilhados pelas duas plataformas abaixo: uma nota criada em uma abre sem conversão na outra.

Exemplos

Básico

Título

Uma área de título fica acima do conteúdo, com estilo de topo da mesma folha que o editor renderiza, no estilo do Notion, e não um <input> separado com borda ou fundo próprios. Ele é estado local simples, nunca um nó dentro do documento do Tiptap: nunca aparece em document, onChange nem no json/html de getSaveData(), só no próprio campo title.

Título vazio (nota nova)
  • initialTitle define o título uma única vez, do mesmo jeito que document define o conteúdo: mudar o valor depois não atualiza um editor já montado. Remonte com uma key do React ao trocar de nota, como no document (veja Trocando de nota).
  • titlePlaceholder (padrão "Título") só aparece enquanto o título está vazio, no estilo apagado de placeholder, e some assim que o usuário digita. Ele nunca faz parte do valor salvo (title === "" enquanto aparece, nunca o próprio texto do placeholder).
  • Enter no título nunca insere quebra de linha e nunca transforma o título em heading: o título continua em uma linha e o foco vai para o primeiro bloco do conteúdo, igual a apertar "seta para baixo" saindo do título de uma página do Notion. Backspace bem no início do conteúdo faz o contrário: devolve o foco para o fim do título em vez de não fazer nada. Antes disso, o editor usa o Backspace quando tem o que fazer com ele: logo depois de um atalho como - ou # ele desfaz o atalho, no início de uma lista ou citação ele tira o bloco dela e num título de seção vazio ele volta a ser parágrafo. O Backspace seguinte vai para o título.
  • autofocus é a mesma prop de antes; nenhuma prop nova foi criada para isso. Quando ainda não há initialTitle (uma nota nova), o autofocus foca primeiro o título; quando há (uma nota existente sendo aberta), foca o conteúdo, como já acontecia.
  • showTitle (padrão true) esconde a área de título por completo quando é false. Serve para um host cujo document já abre com o próprio heading fazendo o papel de título, para a barra de título separada não duplicar isso. initialTitle/titlePlaceholder e getSaveData().title continuam funcionando por baixo de qualquer jeito; isso só esconde a UI. Com ela desligada, o autofocus nunca vai para o título (não há nada para focar) e o Backspace bem no início do conteúdo não passa mais o foco para um campo de título: ele segue o comportamento normal do editor. As visualizações acima (nesta seção "Título") usam o caso padrão, showTitle={true}; a visualização "Básico" mais acima usa showTitle={false}, porque o documento de exemplo já abre com o próprio heading "Meu título 01".

Uso na web (@medway-ui/core/note-editor)

Roda o Tiptap direto no DOM, sem WebView. Use em qualquer app React que já renderiza @medway-ui/core.

Notes.tsx
import { useRef } from "react";
import {
  NoteEditor,
  type EditorDocument,
  type NoteEditorHandle,
} from "@medway-ui/core/note-editor";
 
function Notes({ initialDoc }: { initialDoc: EditorDocument }) {
  const ref = useRef<NoteEditorHandle>(null);
 
  return (
    <NoteEditor
      ref={ref}
      document={initialDoc}
      autofocus="end"
      onChange={(doc) => {
        // doc: EditorDocument. Persista (ex.: debounce + salvar na sua API)
      }}
    />
  );
}
 
// Acesso imperativo quando o host tem a própria toolbar ou o próprio slash menu:
// ref.current?.focus("end");
// ref.current?.blur();
// ref.current?.getDocument();
// ref.current?.getSaveData(); // { title, json, html, removedImages }; veja "Salvando" abaixo
// ref.current?.setDocument(nextDoc, { focus: true });
// ref.current?.editor // instância `Editor` do Tiptap por baixo

O NoteEditor é exportado pelo subpath @medway-ui/core/note-editor, não pela raiz do pacote. É isso que torna o Tiptap uma peer dependency de fato opcional: o barrel da raiz não importa nada dele, então um app que só renderiza Button nunca precisa instalá-lo. Para usar o editor, instale os pacotes do Tiptap:

npm install @tiptap/react @tiptap/core @tiptap/pm \
  @tiptap/suggestion @tiptap/extensions @tiptap/extension-document \
  @tiptap/extension-paragraph @tiptap/extension-text @tiptap/extension-heading \
  @tiptap/extension-bold @tiptap/extension-italic @tiptap/extension-underline \
  @tiptap/extension-strike @tiptap/extension-code @tiptap/extension-code-block \
  @tiptap/extension-blockquote @tiptap/extension-horizontal-rule \
  @tiptap/extension-hard-break @tiptap/extension-link @tiptap/extension-list

O @medway-ui/notes-core, que define o documento e o schema, é dependência do @medway-ui/core desde a versão 1.36.0 e é instalado junto com ele. O tipo do documento sai da mesma entrada do editor: import type { EditorDocument } from "@medway-ui/core/note-editor". Só declare o @medway-ui/notes-core no seu package.json se usar funções dele (como normalizeDocument ou EMPTY_DOCUMENT), e na mesma faixa que o core usa (^0.2.1), nunca >=: com >=, uma versão nova do notes-core entraria no app sem entrar no core, e os dois leriam o documento com regras diferentes. Em versões do core anteriores à 1.36.0, instale também o @medway-ui/notes-core.

Imagens

O NoteEditor nunca fala com um backend: ele não sabe como são o R2, o S3 ou a sua própria API. O suporte a imagens é só um par de callbacks; você decide o que acontece de fato com o arquivo:

Notes.tsx
<NoteEditor
  ref={ref}
  document={initialDoc}
  toolbar
  onImageUpload={async (file) => {
    // Faça o upload para onde o seu app guarda arquivos e resolva com a URL que
    // vai parar no JSON do documento. Ex.: um PUT pré-assinado no R2/S3 e depois a
    // URL pública resultante.
    return uploadToR2(file);
  }}
  onImageRemoved={(src) => {
    // Upload que nunca entrou na nota: URL recusada pelo editor ou upload que
    // terminou depois de o editor fechar. Pode apagar na hora.
    deleteFromR2(src);
  }}
/>
salvar.ts
const pendingRemovals = useRef<string[]>([]);
 
async function save() {
  const { removedImages, ...note } = ref.current!.getSaveData();
  await api.saveNote(note);
  // Só depois de salvar com sucesso. Substitua a lista, não acumule.
  pendingRemovals.current = removedImages;
}
 
// Ao fechar o editor (desmontar ou trocar de nota com `key`).
useEffect(() => () => pendingRemovals.current.forEach(deleteFromR2), []);

Passar onImageUpload é o que ativa o suporte a imagens. Sem ele, a toolbar não tem o botão "Inserir imagem" (o ícone de imagem), o menu / não tem a opção "Imagem", e soltar ou colar um arquivo de imagem no editor é ignorado em silêncio. Então um host que não quer imagens só omite a prop e não recebe nada dessa UI.

Imagens que vêm dentro de HTML colado ou solto (de outra página, de um e-mail) ficam de fora, com ou sem a prop: elas entrariam como link para o servidor de outra pessoa ou como base64 dentro do JSON, sem passar pelo onImageUpload. O texto em volta entra normalmente. Copiar, recortar e colar uma imagem dentro da própria nota continua funcionando; uma imagem copiada de outra nota fica de fora.

Com ela, quatro caminhos passam por ele: o botão "Inserir imagem" da toolbar (precisa de toolbar também), o slash command /Imagem, soltar um arquivo de imagem no editor e colar uma imagem da área de transferência. Cada um só insere o nó de imagem depois que a promise do seu onImageUpload resolve com uma URL; se ela rejeitar, o editor registra o erro no console e não insere nada. A imagem entra onde o envio começou (no cursor ou no ponto em que foi solta), mesmo que o texto mude enquanto o arquivo sobe, e sem texto alternativo: o nome do arquivo (IMG_2034.JPG) não descreve a imagem para quem usa leitor de tela.

Imagens removidas durante a edição saem em getSaveData().removedImages. O NoteEditor registra toda URL que voltou do onImageUpload nesta sessão, e removedImages lista as que não estão no json daquele save:

  • Apague só o que um save que deu certo deixou de fora, e só quando o editor fechar. Guarde a lista do último save com sucesso (substitua a cada save, não acumule) e apague os arquivos quando o editor desmontar ou trocar de nota. São dois motivos. Se o save falhar ou for cancelado (um autosave com debounce cancelado ao sair da tela, por exemplo), a nota salva ainda usa a imagem. E enquanto o editor está aberto, Ctrl/Cmd+Z pode trazer de volta uma imagem removida: se o arquivo já tivesse sido apagado, ela voltaria apontando para um arquivo que não existe mais.
  • onImageRemoved só dispara na hora, nas três situações em que a imagem nunca chegou ao documento: quando o onImageUpload resolve com uma URL que o editor recusa (tudo que não é http(s): ou data: absoluto), quando o upload termina depois de a edição ser desligada e quando ele termina depois de o editor desmontar. Imagens removidas durante a edição nunca passam por ele.
  • setDocument esquece as imagens enviadas antes dele, porque o conteúdo anterior ainda pode fazer referência a elas.
  • Imagens que já estavam na prop document quando o editor montou, ou seja, carregadas de uma nota existente e já salva, nunca entram em removedImages. O seu endpoint de salvar é o único lugar que conhece o documento antigo e o novo, então deixe que ele compare e apague por lá.
  • O editor não enxerga as outras notas. Se a pessoa copia ou recorta uma imagem enviada nesta nota e cola em outra, ela pode aparecer no removedImages desta nota e continuar em uso na outra. Se o seu app permite isso, confira no backend que nenhuma nota usa o arquivo antes de apagar.
  • É uma limpeza de melhor esforço. Uma imagem enviada depois do último save que deu certo, com o editor fechado sem salvar de novo, fica no storage sem nada apontando para ela. Se arquivos órfãos importam para você, combine com uma varredura periódica no backend.
PropriedadeTipoPadrãoDescrição
onImageUpload(file: File) => Promise<string>-Faz o upload do arquivo para onde o seu app guarda imagens e resolve com a URL a ser gravada no documento. É ela que ativa o botão "Inserir imagem" da toolbar, o slash command /Imagem, o arrastar e soltar e o colar. Sem ela, a inserção de imagens fica desativada em todos os lugares.
onImageRemoved(src: string) => void-Função chamada na hora para um upload que nunca entrou na nota: URL recusada pelo editor ou upload que terminou depois de a edição ser desligada ou de o editor desmontar. Imagens removidas durante a edição vêm em getSaveData().removedImages.
imageDeleteGraceMsnumber-Obsoleta e ignorada. As imagens removidas agora vêm em getSaveData().removedImages, e o host apaga quando o editor fecha.

Redimensionando

Selecionar uma imagem (clicando nela ou chegando nela pelas setas) mostra uma barra flutuante com tamanhos prontos (25%, 50%, 75%, 100% e Original) e uma alça em cada canto para arrastar até qualquer tamanho entre eles. Um arraste inteiro vira um único passo de desfazer, e escolher o tamanho que a imagem já tem não mexe no documento (sem passo de desfazer, sem onChange).

Pelo teclado: com a imagem selecionada, Tab leva o foco para a barra; ←/→ passam entre os tamanhos, Enter aplica um, Esc ou Shift+Tab voltam para a imagem e Tab segue para o texto depois dela.

O tamanho fica no nó da imagem como attrs.width: uma porcentagem da largura do bloco em que a imagem está (a coluna de conteúdo do editor), um inteiro de MIN_IMAGE_WIDTH (10) a MAX_IMAGE_WIDTH (100). Sem width (ou com null), a imagem fica no tamanho natural, limitado à largura da coluna, como todas as imagens criadas antes deste recurso.

{ "type": "image", "attrs": { "src": "https://…/a.png", "width": 50 } }

Como o tamanho é relativo, a mesma nota mantém as proporções em qualquer tela. Numa tela estreita, uma porcentagem pequena ficaria minúscula (25% de um celular de 360px dá ~90px), então uma imagem redimensionada nunca fica mais estreita que 120px, ou que a coluna, se ela for ainda mais estreita.

normalizeDocument (e sanitizeImageWidth, exportado pelo @medway-ui/notes-core junto com as duas constantes) só mantém width quando ele é um número ou uma string decimal simples, arredonda e limita entre 10 e 100; qualquer outro valor é descartado, então um documento editado à mão continua abrindo.

Saída em HTML. Quando a nota é salva ou renderizada como HTML (veja Salvando), uma imagem redimensionada sai assim:

<img src="…" data-width="50" style="width: 50%; min-width: min(100%, 120px)">

Se o seu host sanitiza esse HTML, mantenha o atributo data-width e as propriedades width / min-width do style na img, senão as imagens redimensionadas voltam ao tamanho natural. Ao ler o HTML de volta, o editor procura primeiro o data-width e, se não houver, um style="width: N%".

Versão necessária. O redimensionamento precisa do @medway-ui/notes-core 0.2 ou mais novo, na web e no mobile (o bundle do editor e o normalizeDocument do @medway-ui/native também precisam manter o width). Um notes-core mais antigo descarta o atributo quando o documento é normalizado. A partir do @medway-ui/core 1.36.0 e do @medway-ui/native 1.9.0 o notes-core vem junto, na versão certa; em versões anteriores, atualize os pacotes juntos.

Salvando: título, JSON e HTML

A fonte da verdade do documento é sempre JSON (EditorDocument). É o que o @medway-ui/native também lê pela ponte do WebView (veja abaixo), então uma nota criada em uma plataforma abre na outra sem conversão. Mas um campo do backend que já guarda HTML não precisa travar este editor até ser migrado: o próprio NoteEditor lê e escreve os dois formatos. O título fica separado dos dois (veja Título) e vai junto deles como um campo próprio.

Notes.tsx
import { useRef } from "react";
import { Button } from "@medway-ui/core";
import {
  NoteEditor,
  type EditorDocument,
  type NoteEditorHandle,
} from "@medway-ui/core/note-editor";
 
function Notes({ initialNote }: { initialNote: { title: string; content: EditorDocument | string } }) {
  const ref = useRef<NoteEditorHandle>(null);
 
  const handleSave = async () => {
    const saveData = ref.current?.getSaveData();
    if (!saveData) return;
    const { title, json, html } = saveData;
 
    // `title` nunca aparece em `json`/`html`; envie como um campo próprio.
    await NotesAPI.save({ title, content: html, content_json: json });
  };
 
  return (
    <>
      {/* `initialNote.content` pode ser um EditorDocument ou uma string HTML;
         os dois passam pelo mesmo schema que o editor renderiza. */}
      <NoteEditor
        ref={ref}
        initialTitle={initialNote.title}
        document={initialNote.content}
        toolbar
      />
      <Button onClick={handleSave}>Salvar</Button>
    </>
  );
}
  • document (prop) e setDocument (ref) aceitam EditorDocument | string. Uma string é tratada como HTML e passa pelo schema de notas (generateJSON) antes de ser filtrada pela mesma whitelist que o JSON já passa (normalizeDocument). O HTML de um cliente mais antigo segue exatamente o mesmo caminho de "entrada não confiável" que qualquer outro documento, sem confiança cega.
  • A saída passa pela mesma whitelist. O JSON de onChange, getDocument e getSaveData (e o html gerado a partir dele) já sai do normalizeDocument. Um link colado de outra página guarda só o href: class, title, target e rel do HTML de origem nunca entram na nota.
  • getSaveData() calcula tudo de uma vez ({ title, json, html, removedImages }) e foi feito para ser chamado pela sua própria ação de salvar (um clique de botão, um timer de debounce), nunca pelo onChange. O onChange continua entregando só o JSON do conteúdo, a cada tecla, e nunca o título; rodar generateHTML com essa frequência seria trabalho jogado fora em edições que nunca são salvas.
  • title nunca faz parte de json nem de html. Digitar "Arquitetura do projeto" no título e "Hoje vamos estudar cache distribuído." no conteúdo salva como { title: "Arquitetura do projeto", json: {"type": "doc", "content": [{"type": "paragraph", ...}]}, html: "<p>Hoje vamos estudar cache distribuído.</p>" }, nunca um <h1> dentro de html ou um nó heading dentro de json.
  • O JSON continua sendo o formato canônico. Envie HTML enquanto o campo do backend precisar dele, mas continue persistindo o JSON junto (mesmo numa coluna sem uso): é o único formato que todo cliente (incluindo o @medway-ui/native) consegue ler sem perder informação numa ida e volta pelo schema mais restrito do HTML.
  • O tipo de retorno de getSaveData é exportado como NoteEditorValue: import type { NoteEditorValue } from "@medway-ui/core/note-editor". O nome antigo NoteEditorSaveData (de antes de existir title) continua como alias dele, então imports existentes e a desestruturação { json, html } continuam compilando sem mudança.

Trocando de nota

setDocument substitui o conteúdo de um editor em execução: a instância do ProseMirror, e com ela o histórico de desfazer, continua viva. Esse é o comportamento certo para substituir o conteúdo da nota que o usuário já está editando (aplicar um template, recarregar depois de um conflito), mas é o errado para abrir uma nota diferente: Ctrl/Cmd+Z voltaria para o conteúdo da nota anterior e depois o salvaria por cima da nova. Ele também não mexe no título, que é um estado separado.

Para trocar de nota, remonte o componente, nas duas plataformas:

Notes.tsx
<NoteEditor
  key={note.id}
  initialTitle={note.title}
  document={note.content}
/>

Mudar a key dá à nova nota uma instância nova: pilha de desfazer vazia, o próprio initialTitle e o próprio controle de imagens. setDocument serve para todo o resto.

O mobile se comporta do mesmo jeito e precisa da mesma key: o setDocument do @medway-ui/native envia setContent com reset: false. O caminho reset: true do protocolo recria o editor do lado do bundle, mas só roda quando o NoteEditor monta; ele não é acessível pela ref pública.

Os textos embutidos são em pt-BR e não são configuráveis

Todo texto que o próprio editor renderiza está fixo em português do Brasil. Não existe prop locale/labels, em nenhuma das plataformas:

  • os rótulos da toolbar (aria-label e item do menu "Mais opções"): "Negrito", "Título 2", "Desfazer", "Inserir imagem", e o próprio "Mais opções";
  • as dicas (tooltips) da toolbar, que somam o atalho ao rótulo: "Negrito (Ctrl/Cmd+B)". O atalho também vai em aria-keyshortcuts;
  • o aviso de upload "enviando imagem…";
  • os aria-labels do próprio editor: "Conteúdo da anotação", "Formatação", "Título da anotação", "Comandos";
  • o nome do checkbox de cada item da checklist: "Marcar tarefa: …" ou, já marcado, "Desmarcar tarefa: …", com o texto do item ("tarefa vazia" sem texto);
  • as opções do menu / e suas descrições ("Título 1" / "Título de seção grande", "Checklist" / "Lista de tarefas", …) e o estado vazio "Nenhum resultado";
  • o dialog de link (na web, "Adicionar link"/"Editar link" com os campos "Texto" e "Link", aberto pela barra ou por Ctrl/Cmd+K; no mobile, o dialog "Inserir link") e o erro "Link inválido. Use um endereço como medway.com.br". Nas duas plataformas o campo aceita medway.com.br, um e-mail ou a URL completa (normalizeLinkInput, do @medway-ui/notes-core), e sem texto selecionado a própria URL entra como texto do link;
  • os placeholders do conteúdo: Comece a escrever ou digite "/" para comandos e Digite "/" para comandos.

Só o titlePlaceholder é prop. Os placeholders do conteúdo são opções do buildExtensions (placeholder, caretHint) uma camada abaixo, em @medway-ui/notes-core, mas o NoteEditor não os repassa. Um host que precisa de outros textos hoje tem que montar o próprio editor a partir de @medway-ui/notes-core/schema em vez de configurar este.

Uso no mobile (@medway-ui/native/note-editor)

O React Native não tem DOM, então o mesmo schema roda dentro de um WebView travado (navegação bloqueada, file:// desligado, sem novas janelas). O conteúdo continua atravessando a ponte como JSON (o próprio documento, nunca HTML), mas desde a versão 3 do protocolo essa ponte é totalmente interna: o NoteEditor aqui recebe as mesmas props declarativas do NoteEditor da web (document, onChange, editable, autofocus, toolbar, onImageUpload, onImageRemoved e a obsoleta imageDeleteGraceMs) e controla sozinho o WebView e a ponte de mensagens. O host não precisa mais ligar onMessage/injectJavaScript na mão.

NotesScreen.tsx
import { useRef } from "react";
import {
  NoteEditor,
  type EditorDocument,
  type NoteEditorHandle,
} from "@medway-ui/native/note-editor";
 
function NotesScreen({ initialDoc }: { initialDoc: EditorDocument }) {
  const ref = useRef<NoteEditorHandle>(null);
 
  return (
    <NoteEditor
      ref={ref}
      document={initialDoc}
      autofocus="end"
      onChange={(doc) => {
        // doc: EditorDocument. Persista (ex.: debounce + salvar na sua API)
      }}
    />
  );
}
 
// Acesso imperativo, com os mesmos nomes da ref da web. Aqui é assíncrono, porque ler
// o documento é uma ida e volta pela ponte nesta plataforma:
// ref.current?.focus("end");
// ref.current?.blur();
// await ref.current?.getDocument();
// await ref.current?.getSaveData(); // { title, json, removedImages }, sem `html`; veja abaixo
// ref.current?.setDocument(nextDoc, { focus: true });

O tema também é automático: o NoteEditor lê os tokens do design system para o esquema de cores atual (useThemeColor/useColorScheme do NativeWind) e mantém o documento do WebView sincronizado. O host nunca precisa montar nem enviar um EditorTheme por conta própria.

Diferenças em relação à web e por quê

Três props só existem por causa da plataforma, do mesmo jeito que ...props (atributos de div repassados) só faz sentido na web:

PropriedadePor que só existe no mobile
heightAltura inicial fixa, em dp, para o modo embutido (o editor dentro de um scroll container nativo, redimensionado conforme o conteúdo que ele informa pela ponte). Na web, a superfície só cresce junto com o bloco que a contém.
backgroundColorEvita um flash antes de o WebView pintar o fundo do próprio documento (sensível no Android). Na web, a superfície só herda o fundo da página.
onLinkPressO WebView nunca navega; um link tocado precisa ser entregue ao host (ex.: Linking.openURL, depois de validar a URL), como sempre foi nesta plataforma.

Outras duas props existem nas duas plataformas, mas não puderam manter o mesmo formato, porque aquilo a que elas se adaptam é hardware/runtime de fato diferente, não só uma escolha de nome:

PropriedadeFormato na webFormato no mobilePor quê
onImageUpload(file: File) => Promise<string>() => Promise<{ src: string; alt?: string | null } | null>Na web, o &lt;input type="file"&gt; do navegador já escolheu um arquivo quando isto é chamado; só falta o host fazer o upload. Nesta plataforma não existe input de arquivo do DOM e o @medway-ui/native não tem dependência própria de seletor de imagens, então escolher a imagem (com o seletor que o app host já usa) também é papel desta função, não só o upload. Retorne null se o seletor do host foi cancelado.
getSaveData (ref)() => NoteEditorValue, síncrono, { title, json, html, removedImages }() => Promise<{ title, json, removedImages }>, assíncrono, sem htmlGerar html exige um DOM; esta plataforma de propósito nunca carrega um fora do WebView, e ler o documento já é uma ida e volta pela ponte, não uma chamada no mesmo processo. Persista o json; gere o HTML no backend se precisar.

Todo o resto (document/setDocument (JSON ou string HTML, que passa pelo schema de notas nos dois casos, só que aqui do lado do bundle, porque é o único lado com um DOM para interpretar HTML), initialTitle, titlePlaceholder, onChange, editable, autofocus, toolbar, onImageRemoved, a obsoleta imageDeleteGraceMs e os métodos focus/blur/setDocument da ref) é a mesma prop, com o mesmo tipo e o mesmo comportamento do NoteEditor da web. Veja Título, Imagens e Salvando acima; tudo o que está documentado lá vale aqui também.

Como na web, o NoteEditor é exportado por um subpath, @medway-ui/native/note-editor, e não pela raiz do pacote. O barrel da raiz não importa react-native-webview nem o HTML embutido do editor, então um app que só renderiza Button não paga por nenhum dos dois. Isso importa mais aqui do que na web: o Metro não faz tree-shaking de forma confiável, então tudo o que é alcançável pelo barrel vai parar no bundle do app. Os tipos de mensagem ficam sozinhos em @medway-ui/native/editor/protocol.

Instalação:

npm install @medway-ui/native react-native-webview

Como na web, o @medway-ui/notes-core vem junto com o @medway-ui/native (desde a 1.9.0), e o tipo EditorDocument sai de @medway-ui/native/note-editor. Declare o notes-core no app só se usar funções dele, em ^0.2.1.

Veja o README do @medway-ui/native para a configuração de Metro/Babel/NativeWind que este pacote precisa num app brownfield.

API

Web: props do NoteEditor (@medway-ui/core/note-editor)

PropriedadeTipoPadrãoDescrição
documentEditorDocument | string-Conteúdo inicial: JSON ou uma string HTML (que passa pelo schema de notas). Só é lido uma vez; use o setDocument da ref para substituir o conteúdo depois. Veja Salvando.
initialTitlestring-Título inicial, exibido acima do conteúdo. É um estado próprio, nunca um nó em document/json/html. Só é lido uma vez, como o document. Veja Título.
titlePlaceholderstring"Título"Exibido enquanto o título está vazio. Nunca faz parte do valor salvo. Veja Título.
showTitlebooleantrueDefine se a área de título é renderizada. Use false quando o document já abre com o próprio heading fazendo o papel de título. Veja Título.
onChange(doc: EditorDocument) => void-Dispara a cada atualização do Tiptap. Só para o conteúdo; nunca dispara em mudanças do título.
editablebooleantrueSomente leitura quando false. Vale também para a área de título.
autofocusboolean | "start" | "end"falseFoca na montagem: o título se initialTitle estiver vazio (uma nota nova), senão o editor. Veja Título.
extraExtensionsTiptap Extension[]-Extensões só de UX, somadas ao schema de notas (floating menu, menção etc.). O slash menu / já vem embutido e não precisa de extensão.
toolbarbooleanfalseMostra uma barra de formatação acima do editor, em cinco grupos: marcas (negrito, itálico, sublinhado, tachado, código inline), headings (H2, H3), blocos (lista com marcadores, lista numerada, citação, bloco de código), link e histórico (desfazer, refazer). Um sexto grupo, com o botão "Inserir imagem" (o ícone de imagem), entra quando onImageUpload está definido; veja Imagens. Cada botão tem uma dica com o nome e, quando houver, o atalho. Quando a largura não comporta todos os botões, os do fim da barra vão, na mesma ordem, para um menu Mais opções (⋮); a conta usa o tamanho real dos botões, então acompanha a fonte do navegador. No menu, os comandos de alternância (negrito, títulos, listas…) aparecem marcados quando ativos. É independente do menu /: use um, os dois ou nenhum.
onImageUpload(file: File) => Promise<string>-Ativa o suporte a imagens (botão da toolbar, /Imagem, arrastar e soltar, colar). Veja Imagens.
onImageRemoved(src: string) => void-Função chamada na hora para um upload que nunca entrou na nota (URL recusada ou upload que terminou tarde). Imagens removidas durante a edição vêm em getSaveData().removedImages. Veja Imagens.
imageDeleteGraceMsnumber-Obsoleta e ignorada. Use getSaveData().removedImages. Veja Imagens.
...propsReact.ComponentProps<"div">-Repassadas para a div que envolve o editor (className etc.).

Web: NoteEditorHandle (ref)

PropriedadeTipoDescrição
editorEditor | nullInstância do Tiptap por baixo, para uma toolbar ou atalhos personalizados. É null até o editor ser criado: no Next.js o @tiptap/react só cria o editor depois da primeira renderização. Nesse intervalo, getDocument e getSaveData já devolvem a nota inicial e um setDocument é aplicado assim que o editor existe.
focus / blur(position?) => void / () => void-
getDocument() => EditorDocumentLê o conteúdo atual como JSON.
getSaveData() => NoteEditorValue: { title: string; json: EditorDocument; html: string; removedImages: string[] }Calcula o título, os dois formatos de conteúdo e as imagens enviadas nesta sessão que saíram do documento (veja Imagens), para a sua ação de salvar. Não dispara a cada tecla como o onChange; chame só quando for de fato persistir. Veja Salvando.
setDocument(doc: EditorDocument | string, options?: { focus?: boolean }) => voidSubstitui o conteúdo na mesma instância. Também aceita uma string HTML; veja Salvando. Três coisas que ele de propósito não faz: nunca dispara onChange (a substituição não é uma edição do usuário, então um host que marca a nota como "suja" pelo onChange continua limpo), nunca mexe no título (não existe setter imperativo de título) e nunca limpa o histórico de desfazer. Para trocar de nota, remonte com uma key do React; veja Trocando de nota.

Mobile: props do NoteEditor (@medway-ui/native/note-editor)

As mesmas props da web (document, initialTitle, titlePlaceholder, onChange, editable, autofocus, toolbar, onImageRemoved e a obsoleta imageDeleteGraceMs) mais as específicas da plataforma abaixo. Veja Diferenças em relação à web e por quê para o formato adaptado do onImageUpload.

PropriedadeTipoPadrãoDescrição
onImageUpload() => Promise<{ src: string; alt?: string | null } | null>-Escolhe (com o seletor do próprio host) e faz o upload de uma imagem; resolva com null se o seletor do host foi cancelado. Ativa o botão "Inserir imagem" da toolbar e o slash command /Imagem. Como na web, sem ela os dois ficam indisponíveis.
onLinkPress(href: string) => void-Só no nativo. O WebView nunca navega; um link tocado é entregue aqui (ex.: Linking.openURL depois de validar a URL).
heightnumber-Só no nativo. Altura inicial fixa para o modo embutido (dentro de um scroll container nativo), redimensionada conforme o conteúdo cresce ou diminui. Omita para o WebView preencher o pai e rolar dentro dele.
backgroundColorstring-Só no nativo. Use a cor do tema do host para evitar um flash antes de o documento ser pintado (sensível no Android).
testIDstring--

Mobile: NoteEditorHandle (ref)

Os mesmos nomes de métodos da ref da web. getDocument/getSaveData são async aqui (ler o documento é uma ida e volta pela ponte nesta plataforma, não uma chamada no mesmo processo) e não existe o campo editor (não há instância do Tiptap no mesmo processo para entregar, só um WebView).

PropriedadeTipoDescrição
focus / blur(position?) => void / () => void-
getDocument() => Promise<EditorDocument>Lê o conteúdo atual como JSON, pela ponte.
getSaveData() => Promise<{ title: string; json: EditorDocument; removedImages: string[] }>Sem html; veja Diferenças em relação à web e por quê.
setDocument(doc: EditorDocument | string, options?: { focus?: boolean }) => voidSubstitui o conteúdo na mesma instância. Também aceita uma string HTML, interpretada do lado do bundle. Mesmas ressalvas da web (sem onChange, título intocado, histórico de desfazer preservado), então trocar de nota aqui também exige uma key do React. Veja Trocando de nota.

Avançado: a ponte do WebView (@medway-ui/native/editor/protocol)

Tudo o que está acima é o que um host típico precisa. O protocol.ts (tipos de mensagem, EDITOR_PROTOCOL_VERSION, EDITOR_RECEIVE_HANDLER, isEditorMessage) continua exportado, mas desde a versão 3 do protocolo é um detalhe de implementação que o NoteEditor controla internamente. Use só se estiver construindo uma superfície personalizada direto sobre o WebView em vez de usar o NoteEditor, ou mexendo no próprio editor-bundle/NoteEditor.tsx. O EDITOR_PROTOCOL_VERSION continua protegendo contra um bundle desatualizado: ele sobe sempre que o formato de uma mensagem muda, e o bundle se recusa a conversar com um lado nativo que informa uma versão diferente.