explicai 0.1.0 → 0.2.1
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.
- package/CHANGELOG.md +85 -0
- package/LICENSE +21 -21
- package/README.md +87 -27
- package/dist/cli.js +30 -11
- package/dist/core/annotations.js +2 -0
- package/dist/core/assets.d.ts +2 -0
- package/dist/core/assets.js +51 -16
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +14 -0
- package/dist/core/export/annotationCss.d.ts +5 -0
- package/dist/core/export/annotationCss.js +26 -0
- package/dist/core/export/annotationShapes.d.ts +37 -0
- package/dist/core/export/annotationShapes.js +38 -0
- package/dist/core/export/callout.d.ts +11 -0
- package/dist/core/export/callout.js +26 -0
- package/dist/core/export/collect.d.ts +2 -2
- package/dist/core/export/collect.js +2 -2
- package/dist/core/export/color.d.ts +11 -0
- package/dist/core/export/color.js +33 -0
- package/dist/core/export/fonts.d.ts +35 -0
- package/dist/core/export/fonts.js +40 -0
- package/dist/core/export/headings.d.ts +7 -0
- package/dist/core/export/headings.js +17 -0
- package/dist/core/export/html.d.ts +1 -3
- package/dist/core/export/html.js +86 -39
- package/dist/core/export/imageSize.js +20 -3
- package/dist/core/export/index.d.ts +12 -6
- package/dist/core/export/index.js +106 -26
- package/dist/core/export/labels.d.ts +1 -0
- package/dist/core/export/labels.js +2 -0
- package/dist/core/export/logo.d.ts +10 -0
- package/dist/core/export/logo.js +29 -0
- package/dist/core/export/markdown.js +20 -11
- package/dist/core/export/pdf.js +2 -1
- package/dist/core/export/refs.d.ts +2 -2
- package/dist/core/export/refs.js +22 -9
- package/dist/core/export/textColors.d.ts +7 -0
- package/dist/core/export/textColors.js +18 -0
- package/dist/core/export/theme.d.ts +28 -0
- package/dist/core/export/theme.js +140 -0
- package/dist/core/fs.d.ts +4 -0
- package/dist/core/fs.js +13 -1
- package/dist/core/index.d.ts +5 -2
- package/dist/core/index.js +5 -2
- package/dist/core/manuals.d.ts +7 -1
- package/dist/core/manuals.js +29 -9
- package/dist/core/pages.d.ts +5 -0
- package/dist/core/pages.js +56 -15
- package/dist/core/redact.d.ts +9 -0
- package/dist/core/redact.js +107 -0
- package/dist/core/text.d.ts +2 -0
- package/dist/core/text.js +9 -0
- package/dist/core/trash.js +13 -8
- package/dist/core/types.d.ts +26 -1
- package/dist/core/types.js +2 -0
- package/dist/core/versions.js +3 -3
- package/dist/server/errorPage.d.ts +9 -0
- package/dist/server/errorPage.js +122 -0
- package/dist/server/http.d.ts +11 -3
- package/dist/server/http.js +65 -11
- package/dist/server/index.js +35 -5
- package/dist/server/routes/assets.js +21 -2
- package/dist/server/routes/export.js +18 -8
- package/dist/server/routes/fonts.d.ts +3 -0
- package/dist/server/routes/fonts.js +24 -0
- package/dist/server/routes/pages.js +3 -1
- package/dist/server/routes/web.js +7 -1
- package/dist/server/security.d.ts +1 -1
- package/dist/server/security.js +7 -6
- package/dist/web/assets/index-BlWnNe8d.js +262 -0
- package/dist/web/assets/index-DFBrhkII.css +1 -0
- package/dist/web/index.html +2 -3
- package/fonts/OFL-archivo.txt +93 -0
- package/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
- package/fonts/OFL-inter.txt +93 -0
- package/fonts/OFL-jetbrains-mono.txt +93 -0
- package/fonts/OFL-lora.txt +93 -0
- package/fonts/OFL-montserrat.txt +93 -0
- package/fonts/OFL-playfair-display.txt +93 -0
- package/fonts/OFL-source-sans-3.txt +93 -0
- package/fonts/OFL-source-serif-4.txt +93 -0
- package/fonts/archivo-latin-wght-normal.woff2 +0 -0
- package/fonts/atkinson-hyperlegible-next-latin-400-italic.woff2 +0 -0
- package/fonts/atkinson-hyperlegible-next-latin-400-normal.woff2 +0 -0
- package/fonts/atkinson-hyperlegible-next-latin-700-normal.woff2 +0 -0
- package/fonts/inter-latin-wght-italic.woff2 +0 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/fonts/lora-latin-wght-italic.woff2 +0 -0
- package/fonts/lora-latin-wght-normal.woff2 +0 -0
- package/fonts/montserrat-latin-wght-normal.woff2 +0 -0
- package/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
- package/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- package/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- package/fonts/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/fonts/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/package.json +41 -7
- package/dist/cli.js.map +0 -1
- package/dist/core/annotations.js.map +0 -1
- package/dist/core/assets.js.map +0 -1
- package/dist/core/export/collect.js.map +0 -1
- package/dist/core/export/html.js.map +0 -1
- package/dist/core/export/imageSize.js.map +0 -1
- package/dist/core/export/index.js.map +0 -1
- package/dist/core/export/labels.js.map +0 -1
- package/dist/core/export/markdown.js.map +0 -1
- package/dist/core/export/pdf.js.map +0 -1
- package/dist/core/export/refs.js.map +0 -1
- package/dist/core/frontmatter.js.map +0 -1
- package/dist/core/fs.js.map +0 -1
- package/dist/core/index.js.map +0 -1
- package/dist/core/manuals.js.map +0 -1
- package/dist/core/pages.js.map +0 -1
- package/dist/core/text.js.map +0 -1
- package/dist/core/trash.js.map +0 -1
- package/dist/core/tree.js.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/core/versions.js.map +0 -1
- package/dist/server/http.js.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/server/router.js.map +0 -1
- package/dist/server/routes/assets.js.map +0 -1
- package/dist/server/routes/export.js.map +0 -1
- package/dist/server/routes/manuals.js.map +0 -1
- package/dist/server/routes/pages.js.map +0 -1
- package/dist/server/routes/versions.js.map +0 -1
- package/dist/server/routes/web.js.map +0 -1
- package/dist/server/security.js.map +0 -1
- package/dist/server/workspace.js.map +0 -1
- package/dist/web/assets/index-BXkBk-2Z.css +0 -1
- package/dist/web/assets/index-C02XKPHS.js +0 -260
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
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.1] - 2026-11-03
|
|
10
|
+
|
|
11
|
+
### Corrigido
|
|
12
|
+
|
|
13
|
+
- Aparência: com Sistema marcado, o painel diz se o tema valendo agora é claro ou escuro.
|
|
14
|
+
- Editor: imagem colada ou solta acompanha o texto digitado durante o envio e entra no ponto certo, sem erro quando a página encolhe.
|
|
15
|
+
- CLI: `--version` mostra a versão, e opção desconhecida mostra o uso em vez de um erro do Node.
|
|
16
|
+
|
|
17
|
+
### English summary
|
|
18
|
+
|
|
19
|
+
An image pasted while you type now lands in the right place. With System selected, the Appearance panel says which theme is active. `--version` works, and an unknown CLI option shows the usage instead of a Node error.
|
|
20
|
+
|
|
21
|
+
## [0.2.0] - 2026-11-03
|
|
22
|
+
|
|
23
|
+
A 0.1.0 foi a primeira versão do Studio; este bloco reúne o que entrou desde a tag `v0.1.0`.
|
|
24
|
+
|
|
25
|
+
### Adicionado
|
|
26
|
+
|
|
27
|
+
- 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.
|
|
28
|
+
- 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 1 MB).
|
|
29
|
+
- 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.
|
|
30
|
+
- 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.
|
|
31
|
+
- 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.
|
|
32
|
+
- 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.
|
|
33
|
+
- 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.
|
|
34
|
+
- 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.
|
|
35
|
+
- Layout: árvore em coluna estreita de 900 a 1023 px, marca na barra do topo, alvos de toque de 44 px no celular.
|
|
36
|
+
- 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.
|
|
37
|
+
- 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".
|
|
38
|
+
- 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.
|
|
39
|
+
- 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.
|
|
40
|
+
- 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.
|
|
41
|
+
- Pacote npm: CHANGELOG, descrição bilíngue, palavras-chave e README para quem chega pelo npm.
|
|
42
|
+
|
|
43
|
+
### Alterado
|
|
44
|
+
|
|
45
|
+
- 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.
|
|
46
|
+
- "Versão do manual" é o nome único da versão apontada para o manual final.
|
|
47
|
+
- Imagens do Studio carregam com `loading="lazy"` e o servidor entrega arquivos com ETag, 304 e streaming.
|
|
48
|
+
- O botão do balão da imagem e o título do editor dizem "Editar imagem" (era "Marcar imagem").
|
|
49
|
+
- Legenda da imagem: três ou mais marcadores seguidos sem descrição viram um selo só com intervalo ("1–8").
|
|
50
|
+
- 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.
|
|
51
|
+
- O pacote publicado não leva mais os mapas de código (`.map`).
|
|
52
|
+
|
|
53
|
+
### Corrigido
|
|
54
|
+
|
|
55
|
+
- 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.
|
|
56
|
+
- 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".
|
|
57
|
+
- 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.
|
|
58
|
+
- 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.
|
|
59
|
+
- 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.
|
|
60
|
+
- 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.
|
|
61
|
+
- 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.
|
|
62
|
+
- 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.
|
|
63
|
+
|
|
64
|
+
- 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.
|
|
65
|
+
- Desempenho: `imageSize` lê só o começo do arquivo.
|
|
66
|
+
|
|
67
|
+
### Segurança
|
|
68
|
+
|
|
69
|
+
- Recusa link simbólico que sai da pasta do manual ao servir e ao copiar arquivos (`realpath`).
|
|
70
|
+
- CSP em `/files/` (SVG não executa script) e `Origin` aceito só da mesma porta do `Host`.
|
|
71
|
+
- Erros de arquivo sem caminho absoluto na resposta.
|
|
72
|
+
- Testes de escape contra XSS na exportação e de HTML colado no texto.
|
|
73
|
+
|
|
74
|
+
### Interno
|
|
75
|
+
|
|
76
|
+
- 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.
|
|
77
|
+
- 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.
|
|
78
|
+
|
|
79
|
+
### English summary
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
[Unreleased]: https://github.com/gabriel-codesampaio/explicai-studio/compare/v0.2.1...HEAD
|
|
84
|
+
[0.2.1]: https://github.com/gabriel-codesampaio/explicai-studio/compare/v0.2.0...v0.2.1
|
|
85
|
+
[0.2.0]: https://github.com/gabriel-codesampaio/explicai-studio/compare/v0.1.0...v0.2.0
|
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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,54 +1,114 @@
|
|
|
1
1
|
<p>
|
|
2
|
-
<
|
|
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>
|
|
3
6
|
</p>
|
|
4
7
|
|
|
5
8
|
Construtor de manuais open source e gratuito. Do print ao manual pronto em PDF, HTML e Markdown.
|
|
6
9
|
|
|
7
|
-
> Tira o print
|
|
10
|
+
> Tira o print e explica. O Explicaí monta o manual.
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
[Português](#português) · [English](#english)
|
|
10
13
|
|
|
11
|
-
|
|
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.
|
|
12
19
|
|
|
13
20
|
```bash
|
|
14
21
|
npx explicai manuais
|
|
15
22
|
```
|
|
16
23
|
|
|
17
|
-
O Explicaí Studio abre no navegador, e os manuais ficam na pasta `manuais` (sem pasta, vale a pasta atual). Opções:
|
|
24
|
+
O Explicaí Studio abre no navegador, e os manuais ficam na pasta `manuais` (sem pasta, vale a pasta atual). Opções:
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
- `--port 4321` troca a porta;
|
|
27
|
+
- `--no-open` não abre o navegador;
|
|
28
|
+
- `--help` mostra o uso.
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
- cola prints e marca com números, destaques, setas e áreas clicáveis;
|
|
23
|
-
- escreve passos numerados com o print e o resultado esperado de cada um;
|
|
24
|
-
- guarda versões de cada página e escolhe qual vai para o manual final;
|
|
25
|
-
- marca cada página como rascunho, em revisão, aprovada ou publicada;
|
|
26
|
-
- exporta em HTML, PDF ou Markdown. O PDF usa o Chrome, o Edge ou o Chromium que já estiver na máquina.
|
|
30
|
+
### O que dá para fazer
|
|
27
31
|
|
|
28
|
-
|
|
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.
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
### Exportar
|
|
31
40
|
|
|
32
|
-
|
|
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:
|
|
33
54
|
|
|
34
|
-
```bash
|
|
35
|
-
npm install
|
|
36
|
-
npm run check # lint, tipos e testes
|
|
37
|
-
npm run test:e2e # testes de ponta a ponta no Chrome instalado
|
|
38
|
-
npm run build
|
|
39
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
|
|
40
68
|
|
|
41
|
-
|
|
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
|
+
```
|
|
42
78
|
|
|
43
|
-
|
|
79
|
+
### Contribuir
|
|
44
80
|
|
|
45
|
-
-
|
|
46
|
-
- Brand book: [gabriel-codesampaio.github.io/explicai-brandbook](https://gabriel-codesampaio.github.io/explicai-brandbook/)
|
|
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).
|
|
47
82
|
|
|
48
83
|
## English
|
|
49
84
|
|
|
50
|
-
|
|
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
|
|
51
111
|
|
|
52
|
-
|
|
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).
|
|
53
113
|
|
|
54
|
-
|
|
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.js
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import { mkdir } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { parseArgs } from "node:util";
|
|
7
8
|
import { createStudioServer } from "./server/index.js";
|
|
8
|
-
const
|
|
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]
|
|
9
|
+
const USAGE = `Uso: explicai [pasta] [--port 4321] [--no-open] [--version]
|
|
18
10
|
|
|
19
|
-
Abre o Explicaí Studio no navegador. Os manuais ficam em [pasta] (padrão: pasta atual)
|
|
11
|
+
Abre o Explicaí Studio no navegador. Os manuais ficam em [pasta] (padrão: pasta atual).`;
|
|
12
|
+
function parseCliArgs() {
|
|
13
|
+
try {
|
|
14
|
+
return parseArgs({
|
|
15
|
+
allowPositionals: true,
|
|
16
|
+
options: {
|
|
17
|
+
port: { type: "string", short: "p", default: "4321" },
|
|
18
|
+
"no-open": { type: "boolean", default: false },
|
|
19
|
+
help: { type: "boolean", short: "h", default: false },
|
|
20
|
+
version: { type: "boolean", short: "v", default: false },
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
console.error(`Opção inválida.
|
|
26
|
+
|
|
27
|
+
${USAGE}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const { values, positionals } = parseCliArgs();
|
|
32
|
+
if (values.help) {
|
|
33
|
+
console.log(USAGE);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
if (values.version) {
|
|
37
|
+
const pkg = new URL("../package.json", import.meta.url);
|
|
38
|
+
console.log(JSON.parse(readFileSync(pkg, "utf8")).version);
|
|
20
39
|
process.exit(0);
|
|
21
40
|
}
|
|
22
41
|
const workspace = path.resolve(positionals[0] ?? ".");
|
package/dist/core/annotations.js
CHANGED
|
@@ -20,6 +20,8 @@ export function parseAnnotations(input) {
|
|
|
20
20
|
};
|
|
21
21
|
return {
|
|
22
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 } : {}),
|
|
23
25
|
markers: list(obj.markers, "markers").map((m, i) => {
|
|
24
26
|
const r = record(m, `markers[${i}]`);
|
|
25
27
|
const n = r.n;
|
package/dist/core/assets.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { Annotations } from "./types.js";
|
|
|
3
3
|
export declare function saveAsset(dir: string, fileName: string, data: Uint8Array): Promise<string>;
|
|
4
4
|
export declare function readAnnotations(dir: string, assetPath: string): Promise<Annotations>;
|
|
5
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;
|
|
6
8
|
/**
|
|
7
9
|
* Arquivos de assets/ que ninguém usa: imagens que nenhuma página, versão ou item da lixeira cita,
|
|
8
10
|
* e marcações (.json) cuja imagem não existe mais. Devolve caminhos como `assets/tela.png`.
|
package/dist/core/assets.js
CHANGED
|
@@ -1,39 +1,59 @@
|
|
|
1
|
-
import { mkdir, readdir,
|
|
1
|
+
import { mkdir, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { emptyAnnotations, parseAnnotations } from "./annotations.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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";
|
|
7
8
|
const IMAGE_EXT = /^\.(png|jpe?g|gif|webp|svg)$/;
|
|
8
9
|
/** Grava a imagem em `assets/` com nome único e devolve o caminho relativo ao manual. */
|
|
9
10
|
export async function saveAsset(dir, fileName, data) {
|
|
10
11
|
const ext = path.extname(fileName).toLowerCase();
|
|
11
12
|
if (!IMAGE_EXT.test(ext))
|
|
12
13
|
throw new Error(`Formato de imagem não suportado: ${ext || "sem extensão"}`);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|
|
17
27
|
}
|
|
18
28
|
export async function readAnnotations(dir, assetPath) {
|
|
19
29
|
const file = annotationsFile(dir, assetPath);
|
|
20
30
|
if (!(await exists(file)))
|
|
21
31
|
return emptyAnnotations();
|
|
22
|
-
|
|
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
|
+
}
|
|
23
41
|
}
|
|
24
42
|
export async function writeAnnotations(dir, assetPath, input) {
|
|
25
43
|
const annotations = parseAnnotations(input);
|
|
26
44
|
await atomicWrite(annotationsFile(dir, assetPath), `${JSON.stringify(annotations, null, 2)}\n`);
|
|
27
45
|
return annotations;
|
|
28
46
|
}
|
|
29
|
-
/**
|
|
30
|
-
function
|
|
47
|
+
/** Caminho absoluto de uma imagem que está direto em assets/; qualquer outro lugar é recusado. */
|
|
48
|
+
export function assetFile(dir, assetPath) {
|
|
31
49
|
const abs = resolveInside(dir, assetPath);
|
|
32
50
|
if (path.dirname(abs) !== path.join(path.resolve(dir), ASSETS_DIR)) {
|
|
33
51
|
throw new Error("Anotações só existem para imagens em assets/.");
|
|
34
52
|
}
|
|
35
|
-
return
|
|
53
|
+
return abs;
|
|
36
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`;
|
|
37
57
|
/** Pastas cujos Markdown podem citar imagens: páginas, versões guardadas e lixeira. */
|
|
38
58
|
const MENTION_DIRS = ["pages", ".versoes", ".lixeira"];
|
|
39
59
|
const MENTION = /assets\/([^)\s"'#?]+)/g;
|
|
@@ -46,6 +66,10 @@ export async function findUnusedAssets(dir) {
|
|
|
46
66
|
if (!(await exists(assetsDir)))
|
|
47
67
|
return [];
|
|
48
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));
|
|
49
73
|
for (const sub of MENTION_DIRS) {
|
|
50
74
|
const root = path.join(dir, sub);
|
|
51
75
|
if (!(await exists(root)))
|
|
@@ -53,15 +77,26 @@ export async function findUnusedAssets(dir) {
|
|
|
53
77
|
for (const file of await readdir(root, { recursive: true })) {
|
|
54
78
|
if (!file.endsWith(".md"))
|
|
55
79
|
continue;
|
|
56
|
-
for (const [, name] of (await
|
|
80
|
+
for (const [, name] of (await readText(path.join(root, file))).matchAll(MENTION)) {
|
|
57
81
|
if (name)
|
|
58
|
-
used.add(
|
|
82
|
+
used.add(decodeUri(name));
|
|
59
83
|
}
|
|
60
84
|
}
|
|
61
85
|
}
|
|
62
|
-
const
|
|
63
|
-
const names = new Set(
|
|
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));
|
|
64
98
|
return files
|
|
99
|
+
.filter((f) => !recent.has(f))
|
|
65
100
|
.filter((f) => (f.endsWith(".json") ? !names.has(f.slice(0, -".json".length)) : !used.has(f)))
|
|
66
101
|
.flatMap((f) => (!f.endsWith(".json") && names.has(`${f}.json`) ? [f, `${f}.json`] : [f]))
|
|
67
102
|
.filter((f, i, all) => all.indexOf(f) === i)
|
|
@@ -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 {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seta com ponta fechada. O tamanho da ponta é proporcional à espessura e encolhe em setas curtas
|
|
3
|
+
* (no máximo 70% do comprimento), sem perder a forma. `thickness` na mesma unidade de `from` e `to`.
|
|
4
|
+
*/
|
|
5
|
+
export function arrowShape(from, to, thickness) {
|
|
6
|
+
const dx = to.x - from.x;
|
|
7
|
+
const dy = to.y - from.y;
|
|
8
|
+
const len = Math.hypot(dx, dy);
|
|
9
|
+
const ux = len === 0 ? 1 : dx / len;
|
|
10
|
+
const uy = len === 0 ? 0 : dy / len;
|
|
11
|
+
const headLen = Math.min(thickness * 4.5, len * 0.7);
|
|
12
|
+
const half = headLen * 0.45;
|
|
13
|
+
const bx = to.x - ux * headLen;
|
|
14
|
+
const by = to.y - uy * headLen;
|
|
15
|
+
const inside = headLen * 0.2;
|
|
16
|
+
return {
|
|
17
|
+
shaft: { x1: from.x, y1: from.y, x2: bx + ux * inside, y2: by + uy * inside },
|
|
18
|
+
head: [to, { x: bx - uy * half, y: by + ux * half }, { x: bx + uy * half, y: by - ux * half }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Linhas da legenda por número; marcadores sem descrição em sequência dividem uma linha só. */
|
|
22
|
+
export function legendRows(markers) {
|
|
23
|
+
const rows = [];
|
|
24
|
+
for (const m of [...markers].sort((a, b) => a.n - b.n)) {
|
|
25
|
+
const label = (m.label ?? "").trim();
|
|
26
|
+
const last = rows.at(-1);
|
|
27
|
+
if (!label && last && !last.label)
|
|
28
|
+
last.numbers.push(m.n);
|
|
29
|
+
else
|
|
30
|
+
rows.push({ numbers: [m.n], badges: [], label });
|
|
31
|
+
}
|
|
32
|
+
for (const r of rows) {
|
|
33
|
+
const [first, last] = [r.numbers[0], r.numbers.at(-1)];
|
|
34
|
+
r.badges = !r.label && r.numbers.length >= 3 ? [`${first}–${last}`] : r.numbers.map(String);
|
|
35
|
+
}
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=annotationShapes.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Token } from "marked";
|
|
2
|
+
/** Cor de cada tipo de alerta no manual exportado (fundo branco). O de `note` segue a cor de destaque do manual. */
|
|
3
|
+
export declare const CALLOUT_COLORS: Record<string, string>;
|
|
4
|
+
/**
|
|
5
|
+
* Alerta GFM (`> [!TIP]` na primeira linha da citação): devolve o tipo e o corpo sem o marcador.
|
|
6
|
+
* Qualquer outra citação, inclusive `[!IMPORTANT]`, volta null e sai como citação comum.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readCallout(tokens: Token[]): {
|
|
9
|
+
type: string;
|
|
10
|
+
body: Token[];
|
|
11
|
+
} | null;
|