explicai 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +51 -0
  6. package/dist/core/annotations.d.ts +7 -0
  7. package/dist/core/annotations.js +110 -0
  8. package/dist/core/assets.d.ts +17 -0
  9. package/dist/core/assets.js +116 -0
  10. package/dist/core/errors.d.ts +9 -0
  11. package/dist/core/errors.js +14 -0
  12. package/dist/core/export/annotationCss.d.ts +5 -0
  13. package/dist/core/export/annotationCss.js +26 -0
  14. package/dist/core/export/annotationShapes.d.ts +37 -0
  15. package/dist/core/export/annotationShapes.js +38 -0
  16. package/dist/core/export/callout.d.ts +11 -0
  17. package/dist/core/export/callout.js +26 -0
  18. package/dist/core/export/collect.d.ts +27 -0
  19. package/dist/core/export/collect.js +49 -0
  20. package/dist/core/export/color.d.ts +11 -0
  21. package/dist/core/export/color.js +33 -0
  22. package/dist/core/export/fonts.d.ts +35 -0
  23. package/dist/core/export/fonts.js +40 -0
  24. package/dist/core/export/headings.d.ts +7 -0
  25. package/dist/core/export/headings.js +17 -0
  26. package/dist/core/export/html.d.ts +4 -0
  27. package/dist/core/export/html.js +242 -0
  28. package/dist/core/export/imageSize.d.ts +9 -0
  29. package/dist/core/export/imageSize.js +72 -0
  30. package/dist/core/export/index.d.ts +29 -0
  31. package/dist/core/export/index.js +151 -0
  32. package/dist/core/export/labels.d.ts +17 -0
  33. package/dist/core/export/labels.js +31 -0
  34. package/dist/core/export/logo.d.ts +10 -0
  35. package/dist/core/export/logo.js +29 -0
  36. package/dist/core/export/markdown.d.ts +8 -0
  37. package/dist/core/export/markdown.js +83 -0
  38. package/dist/core/export/pdf.d.ts +6 -0
  39. package/dist/core/export/pdf.js +68 -0
  40. package/dist/core/export/refs.d.ts +24 -0
  41. package/dist/core/export/refs.js +66 -0
  42. package/dist/core/export/textColors.d.ts +7 -0
  43. package/dist/core/export/textColors.js +18 -0
  44. package/dist/core/export/theme.d.ts +28 -0
  45. package/dist/core/export/theme.js +140 -0
  46. package/dist/core/frontmatter.d.ts +5 -0
  47. package/dist/core/frontmatter.js +17 -0
  48. package/dist/core/fs.d.ts +13 -0
  49. package/dist/core/fs.js +45 -0
  50. package/dist/core/index.d.ts +14 -0
  51. package/dist/core/index.js +15 -0
  52. package/dist/core/manuals.d.ts +23 -0
  53. package/dist/core/manuals.js +86 -0
  54. package/dist/core/pages.d.ts +43 -0
  55. package/dist/core/pages.js +179 -0
  56. package/dist/core/redact.d.ts +9 -0
  57. package/dist/core/redact.js +107 -0
  58. package/dist/core/text.d.ts +5 -0
  59. package/dist/core/text.js +28 -0
  60. package/dist/core/trash.d.ts +25 -0
  61. package/dist/core/trash.js +84 -0
  62. package/dist/core/tree.d.ts +13 -0
  63. package/dist/core/tree.js +28 -0
  64. package/dist/core/types.d.ts +111 -0
  65. package/dist/core/types.js +7 -0
  66. package/dist/core/versions.d.ts +27 -0
  67. package/dist/core/versions.js +82 -0
  68. package/dist/server/errorPage.d.ts +9 -0
  69. package/dist/server/errorPage.js +122 -0
  70. package/dist/server/http.d.ts +31 -0
  71. package/dist/server/http.js +132 -0
  72. package/dist/server/index.d.ts +4 -0
  73. package/dist/server/index.js +60 -0
  74. package/dist/server/router.d.ts +30 -0
  75. package/dist/server/router.js +28 -0
  76. package/dist/server/routes/assets.d.ts +2 -0
  77. package/dist/server/routes/assets.js +63 -0
  78. package/dist/server/routes/export.d.ts +2 -0
  79. package/dist/server/routes/export.js +43 -0
  80. package/dist/server/routes/fonts.d.ts +3 -0
  81. package/dist/server/routes/fonts.js +24 -0
  82. package/dist/server/routes/manuals.d.ts +2 -0
  83. package/dist/server/routes/manuals.js +40 -0
  84. package/dist/server/routes/pages.d.ts +2 -0
  85. package/dist/server/routes/pages.js +79 -0
  86. package/dist/server/routes/versions.d.ts +2 -0
  87. package/dist/server/routes/versions.js +43 -0
  88. package/dist/server/routes/web.d.ts +3 -0
  89. package/dist/server/routes/web.js +29 -0
  90. package/dist/server/security.d.ts +6 -0
  91. package/dist/server/security.js +17 -0
  92. package/dist/server/workspace.d.ts +5 -0
  93. package/dist/server/workspace.js +17 -0
  94. package/dist/web/assets/archivo-latin-ext-wdth-normal-7khWdh9v.woff2 +0 -0
  95. package/dist/web/assets/archivo-latin-wdth-normal-DY7AcnAa.woff2 +0 -0
  96. package/dist/web/assets/archivo-vietnamese-wdth-normal-rJmnGBSt.woff2 +0 -0
  97. package/dist/web/assets/atkinson-hyperlegible-next-latin-400-normal-DPp7N0cD.woff +0 -0
  98. package/dist/web/assets/atkinson-hyperlegible-next-latin-400-normal-FfmJh7DR.woff2 +0 -0
  99. package/dist/web/assets/atkinson-hyperlegible-next-latin-700-normal-C2qZD_Q9.woff +0 -0
  100. package/dist/web/assets/atkinson-hyperlegible-next-latin-700-normal-Dpiyiu63.woff2 +0 -0
  101. package/dist/web/assets/atkinson-hyperlegible-next-latin-ext-400-normal-BalHKn7d.woff2 +0 -0
  102. package/dist/web/assets/atkinson-hyperlegible-next-latin-ext-400-normal-qxMiU51n.woff +0 -0
  103. package/dist/web/assets/atkinson-hyperlegible-next-latin-ext-700-normal-292QVhS1.woff +0 -0
  104. package/dist/web/assets/atkinson-hyperlegible-next-latin-ext-700-normal-CBw-mJf_.woff2 +0 -0
  105. package/dist/web/assets/index-B7Gr83D6.js +262 -0
  106. package/dist/web/assets/index-DFBrhkII.css +1 -0
  107. package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  108. package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  109. package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  110. package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  111. package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  112. package/dist/web/favicon.svg +1 -0
  113. package/dist/web/index.html +23 -0
  114. package/fonts/OFL-archivo.txt +93 -0
  115. package/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
  116. package/fonts/OFL-inter.txt +93 -0
  117. package/fonts/OFL-jetbrains-mono.txt +93 -0
  118. package/fonts/OFL-lora.txt +93 -0
  119. package/fonts/OFL-montserrat.txt +93 -0
  120. package/fonts/OFL-playfair-display.txt +93 -0
  121. package/fonts/OFL-source-sans-3.txt +93 -0
  122. package/fonts/OFL-source-serif-4.txt +93 -0
  123. package/fonts/archivo-latin-wght-normal.woff2 +0 -0
  124. package/fonts/atkinson-hyperlegible-next-latin-400-italic.woff2 +0 -0
  125. package/fonts/atkinson-hyperlegible-next-latin-400-normal.woff2 +0 -0
  126. package/fonts/atkinson-hyperlegible-next-latin-700-normal.woff2 +0 -0
  127. package/fonts/inter-latin-wght-italic.woff2 +0 -0
  128. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  129. package/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  130. package/fonts/lora-latin-wght-italic.woff2 +0 -0
  131. package/fonts/lora-latin-wght-normal.woff2 +0 -0
  132. package/fonts/montserrat-latin-wght-normal.woff2 +0 -0
  133. package/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
  134. package/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
  135. package/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  136. package/fonts/source-serif-4-latin-wght-italic.woff2 +0 -0
  137. package/fonts/source-serif-4-latin-wght-normal.woff2 +0 -0
  138. package/package.json +124 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,72 @@
1
+ # Changelog
2
+
3
+ Todas as mudanças importantes do Explicaí Studio ficam aqui. O formato segue o [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e o projeto usa [Versionamento Semântico](https://semver.org/lang/pt-BR/).
4
+
5
+ All notable changes to Explicaí Studio are listed here, in Portuguese first with a short English summary at the end of each release. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2026-11-03
10
+
11
+ A 0.1.0 foi a primeira versão do Studio; este bloco reúne o que entrou desde a tag `v0.1.0`.
12
+
13
+ ### Adicionado
14
+
15
+ - Exportação: fontes empacotadas (Archivo, Atkinson Hyperlegible Next, Inter, Source Sans 3, Source Serif 4, Lora, Montserrat, Playfair Display e JetBrains Mono, todas OFL) que vão junto do HTML exportado, e escolha da fonte do texto e dos títulos no diálogo e pela barra do editor.
16
+ - Exportação: tema do manual com cor do texto, dos títulos por nível, de realce e de destaque, com o nome de cada cor, e logo do cliente na capa (enviado e retirado no diálogo de exportação, limite de 2 MB).
17
+ - Exportação: o diálogo mostra a etapa, o resultado e o erro com nova tentativa; "Exportar manual" foi para o fim do topo, à direita.
18
+ - Editor: cor do texto com paleta fixa e nome de cada cor, também pelo teclado e na barra fixa; barra flutuante na seleção com o tipo do bloco, formatação e cor.
19
+ - Editor: bloco de alerta (salvo como alerta GFM, exportado com rótulo em texto e cor por tipo), menu `/` com blocos agrupados e prévia, controles de linha e coluna direto na tabela, botão "Colar print" com Ctrl+V visível, barra de formatação rolável no celular.
20
+ - Marcações do print: atalhos de letra (V, M, D, S, A) e barra compacta; seta, marcador e legenda redesenhados como no brand book, no editor e no export.
21
+ - Páginas: numeração na árvore e na busca com a mesma regra do sumário exportado, status escrito e legível, e o nome do arquivo da página só muda quando se para de digitar o título.
22
+ - Interface: telas de erro em tela cheia (servidor fora com reconexão de espera crescente, rota inexistente, erro inesperado), erros do núcleo com código e parâmetros estáveis, traduzidos em português e inglês, menu Ajustes com Idioma e Aparência, e página HTML de erro do servidor alinhada à marca.
23
+ - Layout: árvore em coluna estreita de 900 a 1023 px, marca na barra do topo, alvos de toque de 44 px no celular.
24
+ - Testes e CI: GitHub Actions com lint, tipos, testes com cobertura e ponta a ponta; cobertura medida com `npm run test:cov`; testes novos para sistema de arquivos, segurança, UTF-8, exportação, editor e pacote npm.
25
+ - Edição feita fora do Studio (MCP, outro editor): o servidor devolve a versão da página (`If-Match`, 409 `disk-conflict`) e o Studio avisa em vez de sobrescrever, com "Recarregar do disco" e "Manter a minha versão".
26
+ - Imagem: ferramenta Ocultar (O) para dados sensíveis, com Tarja (padrão) ou Desfoque forte (pixelado e borrado), queimados nos pixels; o original é apagado, com aviso quando a pasta está no git e lembrete de que exports antigos ainda têm o original.
27
+ - Imagem: tamanho de exibição Pequena, Média ou Larga, guardado no JSON ao lado da imagem e respeitado no editor, no HTML e no PDF.
28
+ - Imagem: Recortar (R), com as marcações reposicionadas e a confirmação listando as que vão sumir; Reduzir resolução e peso, sugerindo 1600 px de largura e sem nunca ampliar.
29
+ - Pacote npm: CHANGELOG, descrição bilíngue, palavras-chave e README para quem chega pelo npm.
30
+
31
+ ### Alterado
32
+
33
+ - O Studio usa sempre o cobalto da marca, com seleção de texto em cobalto translúcido; raio de 3 px vindo de um token; rótulos de seção em mono maiúsculo; ícones decorativos removidos.
34
+ - "Versão do manual" é o nome único da versão apontada para o manual final.
35
+ - Imagens do Studio carregam com `loading="lazy"` e o servidor entrega arquivos com ETag, 304 e streaming.
36
+ - O botão do balão da imagem e o título do editor dizem "Editar imagem" (era "Marcar imagem").
37
+ - Legenda da imagem: três ou mais marcadores seguidos sem descrição viram um selo só com intervalo ("1–8").
38
+ - Lixeira: o painel não conta mais as imagens sem uso ao abrir; o botão fica sempre visível e avisa quando não há nada.
39
+ - O pacote publicado não leva mais os mapas de código (`.map`).
40
+
41
+ ### Corrigido
42
+
43
+ - Autosave: grava uma página por vez, guarda uma mudança pendente por página e descarta a resposta antiga ao trocar de página.
44
+ - Autosave: o renome do arquivo (5 s depois do título) espera a gravação em voo e leva a versão nova, sem faixa de conflito falsa; com a faixa de conflito aberta, o rodapé diz "Aguardando sua escolha" em vez de "Não foi possível salvar".
45
+ - Núcleo: `updateManual` passou a ter fila por pasta (sem perder mudanças simultâneas); `addPage` grava o arquivo antes da árvore; a lixeira pula item com `entry.json` quebrado; `findUnusedAssets` ignora arquivo que sumiu; dois envios do mesmo nome de imagem não dão mais `EEXIST`; manual com BOM no começo abre; restaurar uma página fecha a lixeira.
46
+ - Limpeza de imagens sem uso: grava a fila do autosave antes, poupa imagem gravada há menos de 1 minuto e volta a 60 s quando a carência é inválida.
47
+ - Exportação: gera em pasta temporária e troca só no fim; marcação com JSON quebrado ou de forma errada vira aviso e a imagem sai pura; aviso quando a página cita imagem que não existe; `%` solto em link ou imagem não derruba mais a exportação; lê `width` e `height` de SVG; sem pulo de nível de título; "Só páginas aprovadas" fica desabilitado quando nenhuma está aprovada.
48
+ - Servidor: `%` malformado na URL responde 400 com código estável; corpo JSON que não é UTF-8 válido é recusado; diretório vira 404.
49
+ - Editor: Enter depois de imagem, ordem de várias imagens coladas, Esc no balão da imagem devolve o foco, Ctrl+End leva ao fim da página, menu `/` não fica atrás da barra fixa nem deixa de rolar com a seta, negrito e link junto de código no texto, popover da imagem ancorado dentro da área útil.
50
+ - Interface: título longo da página não corta ao carregar a fonte; erro ao trocar a fonte do manual aparece; contraste de 3:1 na borda de campo e na amostra de cor nos dois temas, seletor de tema com uma só opção marcada, título longo da árvore truncado, marca que não quebra em duas linhas.
51
+
52
+ - Telas: foco do painel Aparência sem opção marcada, rodapé em tela estreita mostra o caminho inteiro, botões da barra de marcação com 44 px e nome no toque, grupo Versões/Exportar alinhado em 375 px, uma marca só com a gaveta aberta, "Nesta página" sem rolagem lateral, nome inteiro do título truncado na árvore ao passar o mouse.
53
+ - Desempenho: `imageSize` lê só o começo do arquivo.
54
+
55
+ ### Segurança
56
+
57
+ - Recusa link simbólico que sai da pasta do manual ao servir e ao copiar arquivos (`realpath`).
58
+ - CSP em `/files/` (SVG não executa script) e `Origin` aceito só da mesma porta do `Host`.
59
+ - Erros de arquivo sem caminho absoluto na resposta.
60
+ - Testes de escape contra XSS na exportação e de HTML colado no texto.
61
+
62
+ ### Interno
63
+
64
+ - CI: publicação no npm com provenance e release no GitHub ao enviar uma tag `vX.Y.Z`; suíte e2e estável no CI (espera a seleção do editor antes do próximo passo); pastas temporárias do e2e apagadas ao fim.
65
+ - Regras do projeto (Conventional Commits, arquitetura, testes, revisão) e plugin da equipe de agentes; tickets em `docs/tickets`; documentação do formato do manual atualizada com tema, cor do texto, alertas e logo.
66
+
67
+ ### English summary
68
+
69
+ New since 0.1.0: bundled OFL fonts and font choice for exports, manual theme colors and client logo, alert blocks, text colors with names, a floating selection toolbar, a richer `/` menu, table row and column controls, annotation shortcuts, full-screen error pages, translated errors, GitHub Actions CI and a much larger test suite. New: warning instead of overwrite when a page changed on disk outside the Studio. Image tools: hide sensitive data (solid bar or strong blur burned into the pixels), display size (small, medium, wide), crop with annotations remapped, and resolution/weight reduction. Fixes cover autosave races, per-folder write queue, atomic exports, unused-image cleanup and many editor and accessibility details. Security: symlinks that leave the manual folder are refused, CSP on `/files/`, same-port `Origin` check. The npm package no longer ships source maps.
70
+
71
+ [Unreleased]: https://github.com/gabriel-codesampaio/explicai-studio/compare/v0.2.0...HEAD
72
+ [0.2.0]: https://github.com/gabriel-codesampaio/explicai-studio/compare/v0.1.0...v0.2.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gabriel Sampaio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,114 @@
1
- # Temporary Holding Version
1
+ <p>
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://gabriel-codesampaio.github.io/explicai-brandbook/logo/explicai-logo-horizontal-escuro.svg">
4
+ <img src="https://gabriel-codesampaio.github.io/explicai-brandbook/logo/explicai-logo-horizontal.svg" alt="Explicaí" height="56">
5
+ </picture>
6
+ </p>
2
7
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
8
+ Construtor de manuais open source e gratuito. Do print ao manual pronto em PDF, HTML e Markdown.
9
+
10
+ > Tira o print e explica. O Explicaí monta o manual.
11
+
12
+ [Português](#português) · [English](#english)
13
+
14
+ ## Português
15
+
16
+ ### Como usar
17
+
18
+ Precisa do Node 20.12 ou mais novo. Para exportar em PDF, precisa também do Chrome, do Edge ou do Chromium instalado na máquina.
19
+
20
+ ```bash
21
+ npx explicai manuais
22
+ ```
23
+
24
+ O Explicaí Studio abre no navegador, e os manuais ficam na pasta `manuais` (sem pasta, vale a pasta atual). Opções:
25
+
26
+ - `--port 4321` troca a porta;
27
+ - `--no-open` não abre o navegador;
28
+ - `--help` mostra o uso.
29
+
30
+ ### O que dá para fazer
31
+
32
+ - Escrever cada tela ou tarefa do sistema numa página, organizada em árvore.
33
+ - Colar prints e marcar com números, destaques, setas e áreas clicáveis.
34
+ - Escrever passos numerados com o print e o resultado esperado de cada um.
35
+ - Guardar versões de cada página e escolher qual vai para o manual final.
36
+ - Marcar cada página como rascunho, em revisão, aprovada ou publicada.
37
+ - Escolher cores, fontes e logo do manual exportado.
38
+
39
+ ### Exportar
40
+
41
+ Pelo botão Exportar do Studio, em três formatos:
42
+
43
+ | Formato | O que sai |
44
+ | --- | --- |
45
+ | HTML | Um manual com sumário, que abre offline, com as imagens e as fontes ao lado. |
46
+ | PDF | O mesmo HTML impresso pelo navegador da máquina (Chrome, Edge ou Chromium). Se ele estiver num caminho fora do comum, aponte com a variável `EXPLICAI_BROWSER`. |
47
+ | Markdown | Um arquivo só, com a legenda de cada print em texto, bom para ler com IA. |
48
+
49
+ Os arquivos saem em `exportacao/`, dentro da pasta do manual.
50
+
51
+ ### Formato da pasta
52
+
53
+ Cada manual é uma pasta de arquivos Markdown que você pode versionar no git junto com o código:
54
+
55
+ ```
56
+ manuais/
57
+ financeiro/
58
+ manual.json título, idioma e árvore de páginas
59
+ pages/cadastrar-cliente.md
60
+ assets/tela-clientes.png
61
+ assets/tela-clientes.png.json marcações do print
62
+ exportacao/ HTML, PDF e Markdown gerados
63
+ ```
64
+
65
+ O formato completo está em [docs/manual-format.md](https://github.com/gabriel-codesampaio/explicai-studio/blob/main/docs/manual-format.md).
66
+
67
+ ### Usar como biblioteca
68
+
69
+ O mesmo código que o Studio usa para ler, gravar e exportar manuais está em `explicai/core`. É assim que o [Explicaí Agent](https://github.com/gabriel-codesampaio/explicai-mcp) monta manuais pelo MCP.
70
+
71
+ ```js
72
+ import { createManual, addPage, exportManual } from "explicai/core";
73
+
74
+ const manual = await createManual("manuais", { title: "Financeiro", language: "pt-BR" });
75
+ await addPage(manual.dir, { title: "Cadastrar cliente", body: "1. Clique em **Novo cliente**." });
76
+ await exportManual(manual.dir, "html");
77
+ ```
78
+
79
+ ### Contribuir
80
+
81
+ Contribuições são bem-vindas. Abra uma [issue](https://github.com/gabriel-codesampaio/explicai-studio/issues) para conversar sobre a ideia, ou mande um pull request com `npm run check` passando. O código está em [github.com/gabriel-codesampaio/explicai-studio](https://github.com/gabriel-codesampaio/explicai-studio).
82
+
83
+ ## English
84
+
85
+ ### Usage
86
+
87
+ Needs Node 20.12 or newer. PDF export also needs Chrome, Edge or Chromium installed on the machine.
88
+
89
+ ```bash
90
+ npx explicai manuals
91
+ ```
92
+
93
+ Explicaí Studio opens in your browser, and the manuals live in the `manuals` folder (with no folder, the current one is used). Options: `--port 4321` changes the port, `--no-open` skips opening the browser, `--help` prints the usage. The interface is available in Portuguese and English and follows the browser language.
94
+
95
+ ### What you can do
96
+
97
+ - Write one page per screen or task, organized as a tree.
98
+ - Paste screenshots and mark them up with numbered markers, highlights, arrows and clickable areas.
99
+ - Keep versions of each page and pick the one that goes into the final manual.
100
+ - Export to HTML (offline, with images and fonts next to it), PDF (printed by your Chrome, Edge or Chromium; set `EXPLICAI_BROWSER` if it lives somewhere unusual) or a single Markdown file.
101
+
102
+ ### Folder format
103
+
104
+ Each manual is a folder of Markdown files you can keep in git next to your code: a `manual.json` with the page tree, one `.md` per page under `pages/`, images and their annotations under `assets/`, and the exports under `exportacao/`. Details (in Portuguese) in [docs/manual-format.md](https://github.com/gabriel-codesampaio/explicai-studio/blob/main/docs/manual-format.md).
105
+
106
+ ### Library
107
+
108
+ `explicai/core` exports the functions the Studio uses to read, write and export manuals (`createManual`, `addPage`, `exportManual` and more), as in the example above. Types are included.
109
+
110
+ ## Licença / License
111
+
112
+ MIT. Um projeto [codesampa.io](https://codesampa.io) / A [codesampa.io](https://codesampa.io) project. Veja o arquivo [LICENSE](https://github.com/gabriel-codesampaio/explicai-studio/blob/main/LICENSE).
113
+
114
+ As fontes em `fonts/` (Archivo, Atkinson Hyperlegible Next, Inter, JetBrains Mono, Lora, Montserrat, Playfair Display, Source Sans 3 e Source Serif 4) seguem a SIL Open Font License 1.1, com a licença de cada uma ao lado. Elas vão junto com o manual exportado em HTML. / The fonts in `fonts/` are licensed under the SIL Open Font License 1.1, with each license next to the files.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { mkdir } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { parseArgs } from "node:util";
7
+ import { createStudioServer } from "./server/index.js";
8
+ const { values, positionals } = parseArgs({
9
+ allowPositionals: true,
10
+ options: {
11
+ port: { type: "string", short: "p", default: "4321" },
12
+ "no-open": { type: "boolean", default: false },
13
+ help: { type: "boolean", short: "h", default: false },
14
+ },
15
+ });
16
+ if (values.help) {
17
+ console.log(`Uso: explicai [pasta] [--port 4321] [--no-open]
18
+
19
+ Abre o Explicaí Studio no navegador. Os manuais ficam em [pasta] (padrão: pasta atual).`);
20
+ process.exit(0);
21
+ }
22
+ const workspace = path.resolve(positionals[0] ?? ".");
23
+ const port = Number(values.port);
24
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
25
+ console.error(`Porta inválida: ${values.port}`);
26
+ process.exit(1);
27
+ }
28
+ await mkdir(workspace, { recursive: true });
29
+ const webDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "web");
30
+ const server = createStudioServer({ workspace, webDir });
31
+ server.on("error", (err) => {
32
+ console.error(err.code === "EADDRINUSE" ? `A porta ${port} já está em uso. Tente --port ${port + 1}.` : err.message);
33
+ process.exit(1);
34
+ });
35
+ server.listen(port, "127.0.0.1", () => {
36
+ const url = `http://localhost:${port}`;
37
+ console.log(`Explicaí Studio em ${url}\nManuais em ${workspace}\nCtrl+C para sair.`);
38
+ if (!values["no-open"])
39
+ openBrowser(url);
40
+ });
41
+ function openBrowser(url) {
42
+ const [cmd, args] = process.platform === "win32"
43
+ ? ["cmd", ["/c", "start", "", url]]
44
+ : process.platform === "darwin"
45
+ ? ["open", [url]]
46
+ : ["xdg-open", [url]];
47
+ spawn(cmd, args, { stdio: "ignore", detached: true })
48
+ .on("error", () => { })
49
+ .unref();
50
+ }
51
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,7 @@
1
+ import { type Annotations } from "./types.js";
2
+ export declare function emptyAnnotations(): Annotations;
3
+ /**
4
+ * Valida anotações vindas do disco ou do Agent. Lança Error com o caminho do campo inválido.
5
+ * Coordenadas fora de 0..1 são rejeitadas para que nada aponte para fora da imagem.
6
+ */
7
+ export declare function parseAnnotations(input: unknown): Annotations;
@@ -0,0 +1,110 @@
1
+ import { FORMAT_VERSION } from "./types.js";
2
+ export function emptyAnnotations() {
3
+ return { version: FORMAT_VERSION, markers: [], highlights: [], arrows: [], hotspots: [] };
4
+ }
5
+ /**
6
+ * Valida anotações vindas do disco ou do Agent. Lança Error com o caminho do campo inválido.
7
+ * Coordenadas fora de 0..1 são rejeitadas para que nada aponte para fora da imagem.
8
+ */
9
+ export function parseAnnotations(input) {
10
+ const obj = record(input, "anotações");
11
+ if (obj.version !== FORMAT_VERSION)
12
+ throw new Error(`anotações.version: esperado ${FORMAT_VERSION}`);
13
+ const ids = new Set();
14
+ const id = (v, path) => {
15
+ const s = text(v, path);
16
+ if (ids.has(s))
17
+ throw new Error(`${path}: id repetido "${s}"`);
18
+ ids.add(s);
19
+ return s;
20
+ };
21
+ return {
22
+ version: FORMAT_VERSION,
23
+ // Valor desconhecido é ignorado (imagem aparece como sempre), não derruba o arquivo.
24
+ ...(obj.display === "s" || obj.display === "m" || obj.display === "full" ? { display: obj.display } : {}),
25
+ markers: list(obj.markers, "markers").map((m, i) => {
26
+ const r = record(m, `markers[${i}]`);
27
+ const n = r.n;
28
+ if (!Number.isInteger(n) || n < 1)
29
+ throw new Error(`markers[${i}].n: inteiro a partir de 1`);
30
+ return {
31
+ id: id(r.id, `markers[${i}].id`),
32
+ n: n,
33
+ ...point(r, `markers[${i}]`),
34
+ ...(r.label === undefined ? {} : { label: text(r.label, `markers[${i}].label`, true) }),
35
+ };
36
+ }),
37
+ highlights: list(obj.highlights, "highlights").map((h, i) => {
38
+ const r = record(h, `highlights[${i}]`);
39
+ return { id: id(r.id, `highlights[${i}].id`), ...rect(r, `highlights[${i}]`) };
40
+ }),
41
+ arrows: list(obj.arrows, "arrows").map((a, i) => {
42
+ const r = record(a, `arrows[${i}]`);
43
+ return {
44
+ id: id(r.id, `arrows[${i}].id`),
45
+ from: point(record(r.from, `arrows[${i}].from`), `arrows[${i}].from`),
46
+ to: point(record(r.to, `arrows[${i}].to`), `arrows[${i}].to`),
47
+ };
48
+ }),
49
+ hotspots: list(obj.hotspots, "hotspots").map((h, i) => {
50
+ const r = record(h, `hotspots[${i}]`);
51
+ return {
52
+ id: id(r.id, `hotspots[${i}].id`),
53
+ ...rect(r, `hotspots[${i}]`),
54
+ target: target(r.target, `hotspots[${i}].target`),
55
+ };
56
+ }),
57
+ };
58
+ }
59
+ function target(v, path) {
60
+ const r = record(v, path);
61
+ switch (r.type) {
62
+ case "page":
63
+ return { type: "page", pageId: text(r.pageId, `${path}.pageId`) };
64
+ case "anchor":
65
+ return { type: "anchor", pageId: text(r.pageId, `${path}.pageId`), anchor: text(r.anchor, `${path}.anchor`) };
66
+ case "url": {
67
+ const href = text(r.href, `${path}.href`);
68
+ // Só http(s) e mailto: um href "javascript:" viraria XSS no HTML exportado.
69
+ if (!/^(https?:|mailto:)/i.test(href))
70
+ throw new Error(`${path}.href: use http, https ou mailto`);
71
+ return { type: "url", href };
72
+ }
73
+ default:
74
+ throw new Error(`${path}.type: esperado "page", "anchor" ou "url"`);
75
+ }
76
+ }
77
+ function point(r, path) {
78
+ return { x: unit(r.x, `${path}.x`), y: unit(r.y, `${path}.y`) };
79
+ }
80
+ function rect(r, path) {
81
+ const box = { ...point(r, path), w: unit(r.w, `${path}.w`), h: unit(r.h, `${path}.h`) };
82
+ if (box.w === 0 || box.h === 0)
83
+ throw new Error(`${path}: largura e altura maiores que zero`);
84
+ if (box.x + box.w > 1 + 1e-9 || box.y + box.h > 1 + 1e-9)
85
+ throw new Error(`${path}: a área passa da borda da imagem`);
86
+ return box;
87
+ }
88
+ function unit(v, path) {
89
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1)
90
+ throw new Error(`${path}: número entre 0 e 1`);
91
+ return v;
92
+ }
93
+ function text(v, path, allowEmpty = false) {
94
+ if (typeof v !== "string" || (!allowEmpty && v.trim() === ""))
95
+ throw new Error(`${path}: texto obrigatório`);
96
+ return v;
97
+ }
98
+ function list(v, path) {
99
+ if (v === undefined)
100
+ return [];
101
+ if (!Array.isArray(v))
102
+ throw new Error(`${path}: esperado uma lista`);
103
+ return v;
104
+ }
105
+ function record(v, path) {
106
+ if (typeof v !== "object" || v === null || Array.isArray(v))
107
+ throw new Error(`${path}: esperado um objeto`);
108
+ return v;
109
+ }
110
+ //# sourceMappingURL=annotations.js.map
@@ -0,0 +1,17 @@
1
+ import type { Annotations } from "./types.js";
2
+ /** Grava a imagem em `assets/` com nome único e devolve o caminho relativo ao manual. */
3
+ export declare function saveAsset(dir: string, fileName: string, data: Uint8Array): Promise<string>;
4
+ export declare function readAnnotations(dir: string, assetPath: string): Promise<Annotations>;
5
+ export declare function writeAnnotations(dir: string, assetPath: string, input: unknown): Promise<Annotations>;
6
+ /** Caminho absoluto de uma imagem que está direto em assets/; qualquer outro lugar é recusado. */
7
+ export declare function assetFile(dir: string, assetPath: string): string;
8
+ /**
9
+ * Arquivos de assets/ que ninguém usa: imagens que nenhuma página, versão ou item da lixeira cita,
10
+ * e marcações (.json) cuja imagem não existe mais. Devolve caminhos como `assets/tela.png`.
11
+ */
12
+ export declare function findUnusedAssets(dir: string): Promise<string[]>;
13
+ /** Move o que `findUnusedAssets` achou para `.lixeira/imagens-<data>/`. Nada é apagado. */
14
+ export declare function cleanUnusedAssets(dir: string): Promise<{
15
+ moved: number;
16
+ folder: string | null;
17
+ }>;
@@ -0,0 +1,116 @@
1
+ import { mkdir, readdir, rename, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { emptyAnnotations, parseAnnotations } from "./annotations.js";
4
+ import { ExplicaiError } from "./errors.js";
5
+ import { atomicWrite, exists, readText, relativePosix, resolveInside, uniquePath } from "./fs.js";
6
+ import { ASSETS_DIR, readManual } from "./manuals.js";
7
+ import { decodeUri, slugify } from "./text.js";
8
+ const IMAGE_EXT = /^\.(png|jpe?g|gif|webp|svg)$/;
9
+ /** Grava a imagem em `assets/` com nome único e devolve o caminho relativo ao manual. */
10
+ export async function saveAsset(dir, fileName, data) {
11
+ const ext = path.extname(fileName).toLowerCase();
12
+ if (!IMAGE_EXT.test(ext))
13
+ throw new Error(`Formato de imagem não suportado: ${ext || "sem extensão"}`);
14
+ const base = slugify(path.basename(fileName).slice(0, -ext.length));
15
+ // Dois envios do mesmo nome ao mesmo tempo (duas abas) escolhem o mesmo caminho livre; quem perde tenta o próximo.
16
+ for (;;) {
17
+ const abs = await uniquePath(path.join(dir, ASSETS_DIR), base, ext);
18
+ try {
19
+ await writeFile(abs, data, { flag: "wx" });
20
+ return relativePosix(dir, abs);
21
+ }
22
+ catch (err) {
23
+ if (err.code !== "EEXIST")
24
+ throw err;
25
+ }
26
+ }
27
+ }
28
+ export async function readAnnotations(dir, assetPath) {
29
+ const file = annotationsFile(dir, assetPath);
30
+ if (!(await exists(file)))
31
+ return emptyAnnotations();
32
+ const raw = await readText(file);
33
+ try {
34
+ return parseAnnotations(JSON.parse(raw));
35
+ }
36
+ catch {
37
+ // JSON quebrado ou de forma errada: o mesmo aviso
38
+ const name = relativePosix(dir, file);
39
+ throw new ExplicaiError("annotations-invalid", `As marcações de ${name} estão com erro de sintaxe. Corrija ou apague o arquivo.`, { file: name });
40
+ }
41
+ }
42
+ export async function writeAnnotations(dir, assetPath, input) {
43
+ const annotations = parseAnnotations(input);
44
+ await atomicWrite(annotationsFile(dir, assetPath), `${JSON.stringify(annotations, null, 2)}\n`);
45
+ return annotations;
46
+ }
47
+ /** Caminho absoluto de uma imagem que está direto em assets/; qualquer outro lugar é recusado. */
48
+ export function assetFile(dir, assetPath) {
49
+ const abs = resolveInside(dir, assetPath);
50
+ if (path.dirname(abs) !== path.join(path.resolve(dir), ASSETS_DIR)) {
51
+ throw new Error("Anotações só existem para imagens em assets/.");
52
+ }
53
+ return abs;
54
+ }
55
+ /** `assets/foto.png` → `assets/foto.png.json`. Só imagens dentro de assets/ têm anotações. */
56
+ const annotationsFile = (dir, assetPath) => `${assetFile(dir, assetPath)}.json`;
57
+ /** Pastas cujos Markdown podem citar imagens: páginas, versões guardadas e lixeira. */
58
+ const MENTION_DIRS = ["pages", ".versoes", ".lixeira"];
59
+ const MENTION = /assets\/([^)\s"'#?]+)/g;
60
+ /**
61
+ * Arquivos de assets/ que ninguém usa: imagens que nenhuma página, versão ou item da lixeira cita,
62
+ * e marcações (.json) cuja imagem não existe mais. Devolve caminhos como `assets/tela.png`.
63
+ */
64
+ export async function findUnusedAssets(dir) {
65
+ const assetsDir = path.join(dir, ASSETS_DIR);
66
+ if (!(await exists(assetsDir)))
67
+ return [];
68
+ const used = new Set();
69
+ // O logo do tema não está em nenhuma página, mas o export precisa dele.
70
+ const logo = (await readManual(dir)).theme?.logo;
71
+ if (logo?.startsWith(`${ASSETS_DIR}/`))
72
+ used.add(logo.slice(ASSETS_DIR.length + 1));
73
+ for (const sub of MENTION_DIRS) {
74
+ const root = path.join(dir, sub);
75
+ if (!(await exists(root)))
76
+ continue;
77
+ for (const file of await readdir(root, { recursive: true })) {
78
+ if (!file.endsWith(".md"))
79
+ continue;
80
+ for (const [, name] of (await readText(path.join(root, file))).matchAll(MENTION)) {
81
+ if (name)
82
+ used.add(decodeUri(name));
83
+ }
84
+ }
85
+ }
86
+ const listed = await readdir(assetsDir);
87
+ const names = new Set(listed);
88
+ // Arquivo recém-gravado pode estar numa página que ainda não chegou ao disco: poupa (EXPLICAI_ASSET_GRACE_MS só para o e2e).
89
+ const envGrace = Number(process.env.EXPLICAI_ASSET_GRACE_MS ?? 60_000);
90
+ const grace = Number.isFinite(envGrace) && envGrace >= 0 ? envGrace : 60_000;
91
+ const stats = await Promise.all(listed.map((f) => stat(path.join(assetsDir, f)).catch((err) => {
92
+ if (err.code !== "ENOENT")
93
+ throw err;
94
+ return null; // sumiu desde a listagem
95
+ })));
96
+ const files = listed.filter((_, i) => stats[i]);
97
+ const recent = new Set(listed.filter((_, i) => stats[i] && Date.now() - stats[i].mtimeMs < grace));
98
+ return files
99
+ .filter((f) => !recent.has(f))
100
+ .filter((f) => (f.endsWith(".json") ? !names.has(f.slice(0, -".json".length)) : !used.has(f)))
101
+ .flatMap((f) => (!f.endsWith(".json") && names.has(`${f}.json`) ? [f, `${f}.json`] : [f]))
102
+ .filter((f, i, all) => all.indexOf(f) === i)
103
+ .map((f) => `${ASSETS_DIR}/${f}`);
104
+ }
105
+ /** Move o que `findUnusedAssets` achou para `.lixeira/imagens-<data>/`. Nada é apagado. */
106
+ export async function cleanUnusedAssets(dir) {
107
+ const unused = await findUnusedAssets(dir);
108
+ if (unused.length === 0)
109
+ return { moved: 0, folder: null };
110
+ const folder = path.join(".lixeira", `imagens-${new Date().toISOString().replace(/[:.]/g, "-")}`);
111
+ await mkdir(path.join(dir, folder, ASSETS_DIR), { recursive: true });
112
+ for (const asset of unused)
113
+ await rename(resolveInside(dir, asset), path.join(dir, folder, asset));
114
+ return { moved: unused.length, folder: folder.split(path.sep).join("/") };
115
+ }
116
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Erro do núcleo que a interface sabe traduzir: a mensagem fica em português (CLI e MCP leem ela),
3
+ * e `code` + `params` são estáveis para o Studio montar o texto no idioma escolhido.
4
+ */
5
+ export declare class ExplicaiError extends Error {
6
+ readonly code: string;
7
+ readonly params: Record<string, string | number>;
8
+ constructor(code: string, message: string, params?: Record<string, string | number>);
9
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Erro do núcleo que a interface sabe traduzir: a mensagem fica em português (CLI e MCP leem ela),
3
+ * e `code` + `params` são estáveis para o Studio montar o texto no idioma escolhido.
4
+ */
5
+ export class ExplicaiError extends Error {
6
+ code;
7
+ params;
8
+ constructor(code, message, params = {}) {
9
+ super(message);
10
+ this.code = code;
11
+ this.params = params;
12
+ }
13
+ }
14
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CSS das marcações no HTML exportado. O editor desenha o mesmo (AnnotationOverlay e AnnotationLegend):
3
+ * mexeu aqui, confira lá. Variáveis --anotacao e --sobre-anotacao vêm do :root do export.
4
+ */
5
+ export declare const ANNOTATION_CSS = "\n.shot{display:block;margin:.6em 0}\n.shot-s{--shot-w:40%}.shot-m{--shot-w:70%}.shot-full{--shot-w:100%}\n.frame{position:relative;display:block;max-width:var(--shot-w,100%);width:fit-content;isolation:isolate}\n.frame img{display:block;max-width:100%;height:auto;border:1px solid var(--linha);border-radius:8px}\n.hl{position:absolute;border:2px solid var(--marca);border-radius:3px}\n.hl::before{content:\"\";position:absolute;inset:0;background:var(--marca);mix-blend-mode:multiply}\n.hot{position:absolute;border:2px dashed var(--anotacao);background:var(--anotacao-fundo);border-radius:3px}\n.arrows{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}\n.arrows line{stroke:var(--anotacao);stroke-linecap:round}\n.arrows polygon{fill:var(--anotacao)}\n.arrows .halo{stroke:#fff;stroke-opacity:.9;stroke-linejoin:round}\n.arrows polygon.halo{fill:#fff}\n.mk{position:absolute;transform:translate(-50%,-50%);width:32px;height:32px;border:2px dashed var(--anotacao);border-radius:50%;box-shadow:0 0 0 2px #fff,inset 0 0 0 2px #fff}\n.mk::before{content:attr(data-n);position:absolute;left:-12px;top:-12px;display:grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--anotacao);color:var(--sobre-anotacao);font:600 11px var(--mono);box-shadow:0 0 0 2px #fff}\n.legend{display:grid;gap:2px;margin-top:12px;font-size:15px;line-height:1.4}\n.legend .row{display:flex;align-items:center;gap:4px}\n.legend .n{display:inline-grid;place-items:center;min-width:20px;height:20px;padding:0 4px;border-radius:50%;background:var(--anotacao);color:var(--sobre-anotacao);font:600 11px var(--mono)}\n.legend .n+span{margin-left:2px}\n.legend .none{color:var(--grafite);font-style:italic}\n";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * CSS das marcações no HTML exportado. O editor desenha o mesmo (AnnotationOverlay e AnnotationLegend):
3
+ * mexeu aqui, confira lá. Variáveis --anotacao e --sobre-anotacao vêm do :root do export.
4
+ */
5
+ export const ANNOTATION_CSS = `
6
+ .shot{display:block;margin:.6em 0}
7
+ .shot-s{--shot-w:40%}.shot-m{--shot-w:70%}.shot-full{--shot-w:100%}
8
+ .frame{position:relative;display:block;max-width:var(--shot-w,100%);width:fit-content;isolation:isolate}
9
+ .frame img{display:block;max-width:100%;height:auto;border:1px solid var(--linha);border-radius:8px}
10
+ .hl{position:absolute;border:2px solid var(--marca);border-radius:3px}
11
+ .hl::before{content:"";position:absolute;inset:0;background:var(--marca);mix-blend-mode:multiply}
12
+ .hot{position:absolute;border:2px dashed var(--anotacao);background:var(--anotacao-fundo);border-radius:3px}
13
+ .arrows{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}
14
+ .arrows line{stroke:var(--anotacao);stroke-linecap:round}
15
+ .arrows polygon{fill:var(--anotacao)}
16
+ .arrows .halo{stroke:#fff;stroke-opacity:.9;stroke-linejoin:round}
17
+ .arrows polygon.halo{fill:#fff}
18
+ .mk{position:absolute;transform:translate(-50%,-50%);width:32px;height:32px;border:2px dashed var(--anotacao);border-radius:50%;box-shadow:0 0 0 2px #fff,inset 0 0 0 2px #fff}
19
+ .mk::before{content:attr(data-n);position:absolute;left:-12px;top:-12px;display:grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--anotacao);color:var(--sobre-anotacao);font:600 11px var(--mono);box-shadow:0 0 0 2px #fff}
20
+ .legend{display:grid;gap:2px;margin-top:12px;font-size:15px;line-height:1.4}
21
+ .legend .row{display:flex;align-items:center;gap:4px}
22
+ .legend .n{display:inline-grid;place-items:center;min-width:20px;height:20px;padding:0 4px;border-radius:50%;background:var(--anotacao);color:var(--sobre-anotacao);font:600 11px var(--mono)}
23
+ .legend .n+span{margin-left:2px}
24
+ .legend .none{color:var(--grafite);font-style:italic}
25
+ `;
26
+ //# sourceMappingURL=annotationCss.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Desenho das marcações, em funções puras. O editor tem uma cópia em web/src/lib/annotationShapes.ts
3
+ * (o web não importa de src/); test/annotation-shapes.test.ts garante que as duas dão o mesmo resultado.
4
+ */
5
+ interface Pt {
6
+ x: number;
7
+ y: number;
8
+ }
9
+ export interface ArrowShape {
10
+ /** Haste: do começo até dentro da ponta, para não sobrar fresta. */
11
+ shaft: {
12
+ x1: number;
13
+ y1: number;
14
+ x2: number;
15
+ y2: number;
16
+ };
17
+ /** Vértices do triângulo da ponta: a ponta em `to`, a base perpendicular à haste. */
18
+ head: [Pt, Pt, Pt];
19
+ }
20
+ /**
21
+ * Seta com ponta fechada. O tamanho da ponta é proporcional à espessura e encolhe em setas curtas
22
+ * (no máximo 70% do comprimento), sem perder a forma. `thickness` na mesma unidade de `from` e `to`.
23
+ */
24
+ export declare function arrowShape(from: Pt, to: Pt, thickness: number): ArrowShape;
25
+ export interface LegendRow {
26
+ numbers: number[];
27
+ /** Selos a mostrar: um por número, ou um só intervalo ("1–8") para 3 ou mais sem descrição. */
28
+ badges: string[];
29
+ /** Vazio quando o marcador não tem descrição. */
30
+ label: string;
31
+ }
32
+ /** Linhas da legenda por número; marcadores sem descrição em sequência dividem uma linha só. */
33
+ export declare function legendRows(markers: {
34
+ n: number;
35
+ label?: string;
36
+ }[]): LegendRow[];
37
+ export {};