@hed-hog/core 0.0.400 → 0.0.402

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 (274) hide show
  1. package/dist/access-log/access-log.interceptor.d.ts +10 -0
  2. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  3. package/dist/access-log/access-log.interceptor.js +15 -1
  4. package/dist/access-log/access-log.interceptor.js.map +1 -1
  5. package/dist/ai/ai.controller.d.ts +2 -0
  6. package/dist/ai/ai.controller.d.ts.map +1 -1
  7. package/dist/ai/ai.service.d.ts +86 -2
  8. package/dist/ai/ai.service.d.ts.map +1 -1
  9. package/dist/ai/ai.service.js +304 -23
  10. package/dist/ai/ai.service.js.map +1 -1
  11. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
  12. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
  13. package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
  14. package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
  15. package/dist/core.module.d.ts.map +1 -1
  16. package/dist/core.module.js +3 -0
  17. package/dist/core.module.js.map +1 -1
  18. package/dist/document-kit/document-kit.module.d.ts +3 -0
  19. package/dist/document-kit/document-kit.module.d.ts.map +1 -0
  20. package/dist/document-kit/document-kit.module.js +24 -0
  21. package/dist/document-kit/document-kit.module.js.map +1 -0
  22. package/dist/document-kit/document-kit.registry.d.ts +55 -0
  23. package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
  24. package/dist/document-kit/document-kit.registry.js +91 -0
  25. package/dist/document-kit/document-kit.registry.js.map +1 -0
  26. package/dist/document-kit/document-kit.service.d.ts +49 -0
  27. package/dist/document-kit/document-kit.service.d.ts.map +1 -0
  28. package/dist/document-kit/document-kit.service.js +135 -0
  29. package/dist/document-kit/document-kit.service.js.map +1 -0
  30. package/dist/file/file-upload-ticket.service.d.ts +1 -1
  31. package/dist/file/file.controller.d.ts +3 -3
  32. package/dist/file/file.service.d.ts +136 -13
  33. package/dist/file/file.service.d.ts.map +1 -1
  34. package/dist/file/file.service.js +297 -27
  35. package/dist/file/file.service.js.map +1 -1
  36. package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
  37. package/dist/file/mcp-file-source.schema.js +8 -5
  38. package/dist/file/mcp-file-source.schema.js.map +1 -1
  39. package/dist/file/provider/abstract.provider.d.ts +33 -0
  40. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  41. package/dist/file/provider/abstract.provider.js +9 -0
  42. package/dist/file/provider/abstract.provider.js.map +1 -1
  43. package/dist/file/provider/s3-multipart.util.d.ts +17 -0
  44. package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
  45. package/dist/file/provider/s3-multipart.util.js +88 -0
  46. package/dist/file/provider/s3-multipart.util.js.map +1 -0
  47. package/dist/file/provider/s3.provider.d.ts +9 -1
  48. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  49. package/dist/file/provider/s3.provider.js +16 -0
  50. package/dist/file/provider/s3.provider.js.map +1 -1
  51. package/dist/file/provider/spaces.provider.d.ts +10 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +17 -0
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +5 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +5 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
  60. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  61. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  62. package/dist/integration-profile/integration-profile.secrets.js +26 -0
  63. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  64. package/dist/integration-profile/integration-profile.service.d.ts +1 -0
  65. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  66. package/dist/integration-profile/integration-profile.service.js +3 -3
  67. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  68. package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
  69. package/dist/integration-profile/testers/ai.testers.js +46 -8
  70. package/dist/integration-profile/testers/ai.testers.js.map +1 -1
  71. package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
  72. package/dist/integration-profile/testers/http-probe.js +5 -3
  73. package/dist/integration-profile/testers/http-probe.js.map +1 -1
  74. package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
  75. package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
  76. package/dist/mail/mail.service.d.ts.map +1 -1
  77. package/dist/mail/mail.service.js +16 -1
  78. package/dist/mail/mail.service.js.map +1 -1
  79. package/dist/mcp/mcp-server.service.d.ts.map +1 -1
  80. package/dist/mcp/mcp-server.service.js +10 -1
  81. package/dist/mcp/mcp-server.service.js.map +1 -1
  82. package/dist/mcp/mcp.module.d.ts.map +1 -1
  83. package/dist/mcp/mcp.module.js +2 -0
  84. package/dist/mcp/mcp.module.js.map +1 -1
  85. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
  86. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
  87. package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
  88. package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
  89. package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
  90. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
  91. package/dist/meeting/meeting-provider.registry.d.ts +72 -0
  92. package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
  93. package/dist/meeting/meeting-provider.registry.js.map +1 -1
  94. package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
  95. package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
  96. package/dist/oauth/oauth-token-broker.service.js +105 -3
  97. package/dist/oauth/oauth-token-broker.service.js.map +1 -1
  98. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
  99. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  100. package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
  101. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  102. package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
  103. package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
  104. package/dist/pdf/document/charts/chart-bar.js +117 -0
  105. package/dist/pdf/document/charts/chart-bar.js.map +1 -0
  106. package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
  107. package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
  108. package/dist/pdf/document/charts/chart-donut.js +89 -0
  109. package/dist/pdf/document/charts/chart-donut.js.map +1 -0
  110. package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
  111. package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
  112. package/dist/pdf/document/charts/chart-gauge.js +60 -0
  113. package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
  114. package/dist/pdf/document/charts/chart-line.d.ts +29 -0
  115. package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
  116. package/dist/pdf/document/charts/chart-line.js +89 -0
  117. package/dist/pdf/document/charts/chart-line.js.map +1 -0
  118. package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
  119. package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
  120. package/dist/pdf/document/charts/chart-primitives.js +110 -0
  121. package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
  122. package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
  123. package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
  124. package/dist/pdf/document/charts/chart-radar.js +83 -0
  125. package/dist/pdf/document/charts/chart-radar.js.map +1 -0
  126. package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
  127. package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
  128. package/dist/pdf/document/charts/chart-scale.js +118 -0
  129. package/dist/pdf/document/charts/chart-scale.js.map +1 -0
  130. package/dist/pdf/document/charts/index.d.ts +8 -0
  131. package/dist/pdf/document/charts/index.d.ts.map +1 -0
  132. package/dist/pdf/document/charts/index.js +24 -0
  133. package/dist/pdf/document/charts/index.js.map +1 -0
  134. package/dist/pdf/document/index.d.ts +8 -0
  135. package/dist/pdf/document/index.d.ts.map +1 -0
  136. package/dist/pdf/document/index.js +24 -0
  137. package/dist/pdf/document/index.js.map +1 -0
  138. package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
  139. package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
  140. package/dist/pdf/document/pdf-document.chrome.js +36 -0
  141. package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
  142. package/dist/pdf/document/pdf-document.css.d.ts +20 -0
  143. package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
  144. package/dist/pdf/document/pdf-document.css.js +327 -0
  145. package/dist/pdf/document/pdf-document.css.js.map +1 -0
  146. package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
  147. package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
  148. package/dist/pdf/document/pdf-document.renderer.js +226 -0
  149. package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
  150. package/dist/pdf/document/pdf-document.service.d.ts +27 -0
  151. package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
  152. package/dist/pdf/document/pdf-document.service.js +81 -0
  153. package/dist/pdf/document/pdf-document.service.js.map +1 -0
  154. package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
  155. package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
  156. package/dist/pdf/document/pdf-document.theme.js +105 -0
  157. package/dist/pdf/document/pdf-document.theme.js.map +1 -0
  158. package/dist/pdf/document/pdf-document.types.d.ts +164 -0
  159. package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
  160. package/dist/pdf/document/pdf-document.types.js +18 -0
  161. package/dist/pdf/document/pdf-document.types.js.map +1 -0
  162. package/dist/pdf/pdf.module.d.ts.map +1 -1
  163. package/dist/pdf/pdf.module.js +3 -2
  164. package/dist/pdf/pdf.module.js.map +1 -1
  165. package/dist/pdf/pdf.service.d.ts +75 -1
  166. package/dist/pdf/pdf.service.d.ts.map +1 -1
  167. package/dist/pdf/pdf.service.js +167 -63
  168. package/dist/pdf/pdf.service.js.map +1 -1
  169. package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
  170. package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
  171. package/dist/pdf/templates/commercial-proposal.template.js +276 -0
  172. package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
  173. package/dist/setting/favicon.controller.d.ts +26 -0
  174. package/dist/setting/favicon.controller.d.ts.map +1 -0
  175. package/dist/setting/favicon.controller.js +130 -0
  176. package/dist/setting/favicon.controller.js.map +1 -0
  177. package/dist/setting/setting.module.d.ts.map +1 -1
  178. package/dist/setting/setting.module.js +2 -1
  179. package/dist/setting/setting.module.js.map +1 -1
  180. package/dist/setting/setting.service.d.ts.map +1 -1
  181. package/dist/setting/setting.service.js +3 -0
  182. package/dist/setting/setting.service.js.map +1 -1
  183. package/dist/task/task.service.d.ts +6 -0
  184. package/dist/task/task.service.d.ts.map +1 -1
  185. package/dist/task/task.service.js +44 -13
  186. package/dist/task/task.service.js.map +1 -1
  187. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
  188. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
  189. package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
  190. package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
  191. package/hedhog/data/integration_type.yaml +6 -0
  192. package/hedhog/data/route.yaml +8 -0
  193. package/hedhog/data/setting_group.yaml +52 -0
  194. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
  195. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
  196. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
  197. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
  198. package/hedhog/frontend/messages/en.json +81 -2
  199. package/hedhog/frontend/messages/pt.json +81 -2
  200. package/hedhog/table/file.yaml +3 -1
  201. package/package.json +5 -5
  202. package/src/access-log/access-log.interceptor.spec.ts +50 -0
  203. package/src/access-log/access-log.interceptor.ts +18 -0
  204. package/src/ai/ai-attachment.fragments.spec.ts +56 -3
  205. package/src/ai/ai.service.responses.spec.ts +429 -0
  206. package/src/ai/ai.service.ts +409 -27
  207. package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
  208. package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
  209. package/src/core.module.ts +3 -0
  210. package/src/document-kit/document-kit.module.ts +12 -0
  211. package/src/document-kit/document-kit.registry.ts +125 -0
  212. package/src/document-kit/document-kit.service.spec.ts +156 -0
  213. package/src/document-kit/document-kit.service.ts +173 -0
  214. package/src/file/file-direct-upload.spec.ts +344 -0
  215. package/src/file/file.service.spec.ts +168 -1
  216. package/src/file/file.service.ts +435 -36
  217. package/src/file/mcp-file-source.schema.ts +8 -5
  218. package/src/file/provider/abstract.provider.ts +57 -0
  219. package/src/file/provider/s3-multipart.spec.ts +123 -0
  220. package/src/file/provider/s3-multipart.util.ts +126 -0
  221. package/src/file/provider/s3.provider.ts +65 -1
  222. package/src/file/provider/spaces.provider.ts +67 -1
  223. package/src/index.ts +5 -0
  224. package/src/integration-profile/integration-profile.secrets.ts +26 -0
  225. package/src/integration-profile/integration-profile.service.ts +1 -0
  226. package/src/integration-profile/testers/ai.testers.ts +70 -5
  227. package/src/integration-profile/testers/http-probe.ts +5 -3
  228. package/src/integration-profile/testers/integration-tester.types.ts +5 -0
  229. package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
  230. package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
  231. package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
  232. package/src/mail/mail.service.spec.ts +25 -0
  233. package/src/mail/mail.service.ts +18 -1
  234. package/src/mcp/mcp-server.service.spec.ts +16 -0
  235. package/src/mcp/mcp-server.service.ts +10 -1
  236. package/src/mcp/mcp.module.ts +2 -0
  237. package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
  238. package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
  239. package/src/meeting/meeting-provider.registry.ts +80 -0
  240. package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
  241. package/src/oauth/oauth-token-broker.service.ts +129 -2
  242. package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
  243. package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
  244. package/src/pdf/document/charts/chart-bar.ts +217 -0
  245. package/src/pdf/document/charts/chart-donut.ts +163 -0
  246. package/src/pdf/document/charts/chart-gauge.ts +94 -0
  247. package/src/pdf/document/charts/chart-line.ts +159 -0
  248. package/src/pdf/document/charts/chart-primitives.ts +188 -0
  249. package/src/pdf/document/charts/chart-radar.ts +136 -0
  250. package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
  251. package/src/pdf/document/charts/chart-scale.ts +141 -0
  252. package/src/pdf/document/charts/charts.spec.ts +157 -0
  253. package/src/pdf/document/charts/index.ts +7 -0
  254. package/src/pdf/document/index.ts +7 -0
  255. package/src/pdf/document/pdf-document.chrome.ts +38 -0
  256. package/src/pdf/document/pdf-document.css.ts +326 -0
  257. package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
  258. package/src/pdf/document/pdf-document.renderer.ts +317 -0
  259. package/src/pdf/document/pdf-document.service.ts +77 -0
  260. package/src/pdf/document/pdf-document.theme.ts +146 -0
  261. package/src/pdf/document/pdf-document.types.ts +149 -0
  262. package/src/pdf/pdf.module.ts +3 -2
  263. package/src/pdf/pdf.service.spec.ts +131 -0
  264. package/src/pdf/pdf.service.ts +256 -82
  265. package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
  266. package/src/pdf/templates/commercial-proposal.template.ts +400 -0
  267. package/src/setting/favicon.controller.spec.ts +92 -0
  268. package/src/setting/favicon.controller.ts +135 -0
  269. package/src/setting/setting.module.ts +2 -1
  270. package/src/setting/setting.service.ts +3 -0
  271. package/src/task/task.service.spec.ts +77 -1
  272. package/src/task/task.service.ts +58 -14
  273. package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
  274. package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
@@ -0,0 +1,326 @@
1
+ import { PdfDocumentTheme } from './pdf-document.theme';
2
+
3
+ /**
4
+ * O CSS do documento padrão — o mesmo acabamento da proposta comercial do CRM
5
+ * (`libraries/crm/src/proposal/proposal.service.ts`), com três acréscimos que
6
+ * a proposta não precisava e um relatório precisa:
7
+ *
8
+ * 1. `thead { display: table-header-group }` — sem isso a tabela perde o
9
+ * cabeçalho a partir da segunda página, e um relatório de 15 páginas fica
10
+ * ilegível da página 2 em diante.
11
+ * 2. `tbody tr { break-inside: avoid }` — linha não parte no meio.
12
+ * 3. Nada de wrapper com `break-inside: avoid`, `overflow` ou borda arredondada
13
+ * em volta da tabela: foi o que fez a folha de impressão da tela sair com
14
+ * uma primeira página vazia e conteúdo cortado.
15
+ *
16
+ * A barra laranja do topo e o "X / N" do rodapé **não estão aqui** — são
17
+ * templates do Chromium (ver ./pdf-document.chrome.ts), porque só eles repetem
18
+ * em toda página e só eles sabem o total de páginas.
19
+ */
20
+ export function buildDocumentCss(theme: PdfDocumentTheme, landscape = false): string {
21
+ const { primary, mutedBg, mutedFg, ink, line, fontFamily } = theme;
22
+
23
+ return `
24
+ @page {
25
+ size: A4${landscape ? ' landscape' : ''};
26
+ margin: 72px 56px 56px 56px;
27
+ }
28
+ * { box-sizing: border-box; }
29
+ html, body {
30
+ -webkit-print-color-adjust: exact;
31
+ print-color-adjust: exact;
32
+ }
33
+ body {
34
+ color: ${ink};
35
+ font-family: ${fontFamily};
36
+ font-size: 11.5px;
37
+ line-height: 1.55;
38
+ margin: 0;
39
+ }
40
+
41
+ /* ── Letterhead ── */
42
+ header {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ align-items: flex-start;
46
+ gap: 24px;
47
+ margin-bottom: 24px;
48
+ padding-bottom: 20px;
49
+ border-bottom: 1px solid ${line};
50
+ }
51
+ .brand { display: flex; align-items: center; gap: 14px; }
52
+ .brand img { height: 44px; max-width: 150px; object-fit: contain; }
53
+ .brand-text .eyebrow {
54
+ color: ${primary};
55
+ font-size: 9px;
56
+ font-weight: 700;
57
+ letter-spacing: 0.14em;
58
+ text-transform: uppercase;
59
+ margin-bottom: 3px;
60
+ }
61
+ .brand-text h1 {
62
+ font-size: 20px;
63
+ font-weight: 700;
64
+ color: ${ink};
65
+ margin: 0 0 2px;
66
+ line-height: 1.2;
67
+ }
68
+ .brand-text .company-name { font-size: 11px; color: ${mutedFg}; }
69
+ .meta-block {
70
+ text-align: right;
71
+ font-size: 11px;
72
+ color: ${mutedFg};
73
+ line-height: 1.7;
74
+ white-space: nowrap;
75
+ }
76
+ .meta-block .meta-value { font-weight: 600; color: ${ink}; }
77
+
78
+ /* ── Cards ── */
79
+ .cards {
80
+ display: grid;
81
+ gap: 12px;
82
+ margin-bottom: 18px;
83
+ break-inside: avoid;
84
+ }
85
+ .cards-2 { grid-template-columns: repeat(2, 1fr); }
86
+ .cards-3 { grid-template-columns: repeat(3, 1fr); }
87
+ .cards-4 { grid-template-columns: repeat(4, 1fr); }
88
+ .cards-5 { grid-template-columns: repeat(5, 1fr); }
89
+ .card {
90
+ background: ${mutedBg};
91
+ border: 1px solid ${line};
92
+ border-top: 2px solid ${primary};
93
+ border-radius: 10px;
94
+ padding: 10px 12px;
95
+ }
96
+ .card .card-label {
97
+ font-size: 8.5px;
98
+ font-weight: 700;
99
+ letter-spacing: 0.12em;
100
+ text-transform: uppercase;
101
+ color: ${mutedFg};
102
+ margin-bottom: 4px;
103
+ }
104
+ .card .card-value { font-size: 12px; font-weight: 600; color: ${ink}; }
105
+ .cards-metric .card .card-value { font-size: 17px; font-weight: 700; }
106
+ .card .card-hint { font-size: 9px; color: ${mutedFg}; margin-top: 2px; }
107
+
108
+ /* ── Seções ── */
109
+ h2 {
110
+ font-size: 12.5px;
111
+ font-weight: 700;
112
+ color: ${ink};
113
+ margin: 22px 0 10px;
114
+ padding-bottom: 6px;
115
+ border-bottom: 1px solid ${line};
116
+ text-transform: uppercase;
117
+ letter-spacing: 0.06em;
118
+ break-after: avoid;
119
+ }
120
+ h2 .section-hint {
121
+ float: right;
122
+ font-size: 9.5px;
123
+ font-weight: 400;
124
+ letter-spacing: 0;
125
+ text-transform: none;
126
+ color: ${mutedFg};
127
+ }
128
+
129
+ /* ── Tabela ── */
130
+ .doc-table {
131
+ border-collapse: collapse;
132
+ width: 100%;
133
+ font-size: 10.5px;
134
+ }
135
+ .doc-table.compact { font-size: 9px; }
136
+ .doc-table thead { display: table-header-group; }
137
+ .doc-table thead tr { border-bottom: 1px solid ${line}; }
138
+ .doc-table th {
139
+ background: ${mutedBg};
140
+ color: ${mutedFg};
141
+ font-size: 9px;
142
+ font-weight: 700;
143
+ letter-spacing: 0.08em;
144
+ text-transform: uppercase;
145
+ padding: 7px 8px;
146
+ text-align: left;
147
+ vertical-align: middle;
148
+ }
149
+ .doc-table td {
150
+ border-bottom: 1px solid ${line};
151
+ padding: 6px 8px;
152
+ text-align: left;
153
+ vertical-align: top;
154
+ color: ${ink};
155
+ }
156
+ /*
157
+ * Os tons precisam vencer o color da propria celula: ".doc-table td" tem
158
+ * especificidade 0-1-1 e ".tone-accent" sozinho tem 0-1-0, entao a cor do
159
+ * tom era descartada em toda tabela do sistema — so o font-weight passava,
160
+ * porque a regra da celula nao mexe nele. Quem marca um valor como "warn"
161
+ * quer que ele salte aos olhos, nao que fique so em negrito igual ao resto.
162
+ */
163
+ .doc-table td.tone-muted { color: ${mutedFg}; }
164
+ .doc-table td.tone-accent { color: ${primary}; }
165
+ .doc-table td.tone-warn { color: #b45309; }
166
+ .doc-table tbody tr { break-inside: avoid; }
167
+ .doc-table tbody tr:last-child td { border-bottom: none; }
168
+ .doc-table.zebra tbody tr:nth-child(even) td { background: ${mutedBg}; }
169
+ .doc-table .align-right { text-align: right; }
170
+ .doc-table .align-center { text-align: center; }
171
+ /* Valor numérico não quebra: "1h 00min" partido em duas linhas dobra a
172
+ altura da linha e desalinha a leitura de cima a baixo. O cabeçalho pode
173
+ quebrar à vontade — é o que mantém "On demand" numa coluna estreita. */
174
+ .doc-table td.align-right,
175
+ .doc-table td.align-center { white-space: nowrap; }
176
+ /* A coluna "#" é numerada no servidor: contador de CSS reiniciaria a cada
177
+ repetição do <thead>. */
178
+ .doc-table .idx {
179
+ width: 30px;
180
+ color: ${mutedFg};
181
+ text-align: right;
182
+ font-variant-numeric: tabular-nums;
183
+ }
184
+ .cell-sub { font-size: 8.5px; color: ${mutedFg}; margin-top: 1px; }
185
+ .tone-muted { color: ${mutedFg}; }
186
+ .tone-bold { font-weight: 700; }
187
+ .tone-accent { color: ${primary}; font-weight: 600; }
188
+ .tone-warn { color: #b45309; font-weight: 600; }
189
+ .table-empty {
190
+ border: 1px dashed ${line};
191
+ border-radius: 10px;
192
+ padding: 18px;
193
+ text-align: center;
194
+ font-size: 10.5px;
195
+ color: ${mutedFg};
196
+ }
197
+
198
+ /* ── Gráficos ── */
199
+ .chart { margin: 4px 0 18px; break-inside: avoid; }
200
+ .chart-title {
201
+ font-size: 10.5px;
202
+ font-weight: 700;
203
+ color: ${ink};
204
+ margin-bottom: 6px;
205
+ }
206
+ .chart svg { width: 100%; height: auto; }
207
+ .chart-caption { font-size: 9px; color: ${mutedFg}; margin-top: 4px; }
208
+ .chart-grid {
209
+ display: grid;
210
+ grid-template-columns: repeat(2, 1fr);
211
+ gap: 16px;
212
+ margin-bottom: 18px;
213
+ }
214
+
215
+ /* ── Notas e legenda ── */
216
+ .note {
217
+ background: ${mutedBg};
218
+ border: 1px solid ${line};
219
+ border-radius: 8px;
220
+ padding: 9px 12px;
221
+ font-size: 10px;
222
+ color: ${mutedFg};
223
+ margin: 12px 0;
224
+ break-inside: avoid;
225
+ }
226
+ .note-accent { border-left: 3px solid ${primary}; }
227
+ .legend {
228
+ margin-top: 18px;
229
+ padding-top: 10px;
230
+ border-top: 1px solid ${line};
231
+ font-size: 9px;
232
+ line-height: 1.6;
233
+ color: ${mutedFg};
234
+ }
235
+ .legend p { margin: 0 0 3px; }
236
+ .page-break { break-before: page; }
237
+
238
+ /* ── Barra empilhada e cartões de progresso ── */
239
+ .seg-bar {
240
+ display: flex;
241
+ height: 6px;
242
+ border-radius: 999px;
243
+ overflow: hidden;
244
+ background: ${line};
245
+ margin: 8px 0;
246
+ }
247
+ .seg { display: block; height: 100%; }
248
+ .seg-ink, .dot-ink { background: ${ink}; }
249
+ .seg-muted, .dot-muted { background: #8f8f94; }
250
+ .seg-light, .dot-light { background: #cfcfd4; }
251
+ .seg-accent, .dot-accent { background: ${primary}; }
252
+ .seg-danger, .dot-danger { background: #dc2626; }
253
+ .dot {
254
+ display: inline-block;
255
+ width: 7px;
256
+ height: 7px;
257
+ border-radius: 50%;
258
+ margin-right: 5px;
259
+ vertical-align: middle;
260
+ }
261
+ .seg-block {
262
+ border: 1px solid ${line};
263
+ border-radius: 12px;
264
+ padding: 12px 16px;
265
+ margin-bottom: 18px;
266
+ break-inside: avoid;
267
+ }
268
+ .seg-title { font-size: 13px; font-weight: 700; color: ${ink}; }
269
+ .seg-legend { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 10px; color: ${ink}; }
270
+ .seg-legend-item { white-space: nowrap; }
271
+ .seg-caption { font-size: 10.5px; color: ${ink}; margin-top: 8px; }
272
+ .pc-legend {
273
+ display: flex;
274
+ flex-wrap: wrap;
275
+ gap: 6px 18px;
276
+ font-size: 10px;
277
+ color: ${mutedFg};
278
+ margin-bottom: 12px;
279
+ }
280
+ .pc-grid { display: grid; gap: 12px; align-items: start; }
281
+ .pc-grid-1 { grid-template-columns: 1fr; }
282
+ .pc-grid-2 { grid-template-columns: repeat(2, 1fr); }
283
+ .pc-card {
284
+ border: 1px solid ${line};
285
+ border-radius: 12px;
286
+ padding: 12px 14px;
287
+ break-inside: avoid;
288
+ }
289
+ .pc-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
290
+ .pc-title { font-size: 13px; font-weight: 700; color: ${ink}; line-height: 1.25; }
291
+ .pc-subtitle { font-size: 9.5px; color: ${mutedFg}; margin-top: 2px; }
292
+ .pc-value { text-align: right; white-space: nowrap; }
293
+ .pc-value-num { font-size: 22px; font-weight: 700; color: ${ink}; line-height: 1; }
294
+ .pc-value-hint { font-size: 9px; color: ${mutedFg}; margin-top: 2px; }
295
+ .pc-list-title { font-size: 10px; font-weight: 700; color: ${ink}; margin: 4px 0 6px; }
296
+ .pc-entry {
297
+ border: 1px solid ${line};
298
+ border-radius: 8px;
299
+ padding: 6px 10px;
300
+ margin-bottom: 6px;
301
+ break-inside: avoid;
302
+ }
303
+ .pc-entry-hl { border-color: transparent; }
304
+ .pc-entry-hl-accent { background: color-mix(in srgb, ${primary} 16%, white); }
305
+ .pc-entry-hl-danger { background: #fee2e2; }
306
+ .pc-entry-text { font-size: 10.5px; color: ${ink}; }
307
+ .pc-entry-meta {
308
+ display: flex;
309
+ justify-content: space-between;
310
+ gap: 8px;
311
+ font-size: 9.5px;
312
+ margin-top: 1px;
313
+ }
314
+ .pc-entry-status { font-weight: 700; color: ${ink}; }
315
+ .pc-entry-aside { color: ${mutedFg}; white-space: nowrap; }
316
+ .pc-entry-sub { font-size: 9px; color: ${mutedFg}; margin-top: 2px; }
317
+ .pc-entry-sub-danger { color: #b91c1c; font-weight: 700; }
318
+ .pc-empty {
319
+ background: ${mutedBg};
320
+ border-radius: 8px;
321
+ padding: 8px 10px;
322
+ font-size: 10px;
323
+ color: ${ink};
324
+ }
325
+ `;
326
+ }
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+
3
+ import { escapeHtml, renderBlock, renderDocumentHtml } from './pdf-document.renderer';
4
+ import { PdfDocumentTheme } from './pdf-document.theme';
5
+ import { PdfDocumentBlock, PdfDocumentRow } from './pdf-document.types';
6
+
7
+ const theme: PdfDocumentTheme = {
8
+ primary: '#ff760c',
9
+ mutedBg: '#f4f4f5',
10
+ mutedFg: '#71717a',
11
+ ink: '#18181b',
12
+ line: '#e4e4e7',
13
+ companyName: 'HedHog',
14
+ logoUrl: 'data:image/svg+xml;charset=UTF-8,%3Csvg%3E%3C/svg%3E',
15
+ fontFamily: 'Arial, Helvetica, sans-serif',
16
+ chartPalette: ['#ff760c'],
17
+ };
18
+
19
+ const table = (rows: PdfDocumentRow[]): PdfDocumentBlock => ({
20
+ kind: 'table',
21
+ numbered: true,
22
+ columns: [{ header: 'Aluno' }, { header: 'Horas', align: 'right' }],
23
+ rows,
24
+ });
25
+
26
+ describe('escapeHtml', () => {
27
+ it('neutraliza os cinco caracteres que viram markup', () => {
28
+ expect(escapeHtml(`<script>alert("1" & '2')</script>`)).toBe(
29
+ '&lt;script&gt;alert(&quot;1&quot; &amp; &#39;2&#39;)&lt;/script&gt;',
30
+ );
31
+ });
32
+
33
+ it('trata nulo e indefinido como texto vazio', () => {
34
+ expect(escapeHtml(null)).toBe('');
35
+ expect(escapeHtml(undefined)).toBe('');
36
+ });
37
+ });
38
+
39
+ describe('tabela', () => {
40
+ // O pedido que originou o kit: cada linha numerada a partir de 1.
41
+ it('numera as linhas de 1 a N, na ordem recebida', () => {
42
+ const html = renderBlock(table([['Ana', '10h'], ['Bruno', '9h'], ['Carla', '8h']]));
43
+ const indices = [...html.matchAll(/<td class="idx">(\d+)<\/td>/g)].map((m) => m[1]);
44
+
45
+ expect(indices).toEqual(['1', '2', '3']);
46
+ });
47
+
48
+ // Sem `display: table-header-group` o cabeçalho some da segunda página em
49
+ // diante; o `<thead>` tem que existir de verdade para a regra pegar.
50
+ it('emite thead próprio, que o CSS manda repetir por página', () => {
51
+ const html = renderBlock(table([['Ana', '10h']]));
52
+
53
+ expect(html).toContain('<thead><tr>');
54
+ expect(html).toContain('<th class="idx">#</th>');
55
+ });
56
+
57
+ it('escapa o conteúdo das células e da segunda linha', () => {
58
+ const html = renderBlock({
59
+ kind: 'table',
60
+ columns: [{ header: 'Aluno' }],
61
+ rows: [[{ text: '<b>Ana</b>', sub: 'a&b@x.com' }]],
62
+ });
63
+
64
+ expect(html).toContain('&lt;b&gt;Ana&lt;/b&gt;');
65
+ expect(html).toContain('a&amp;b@x.com');
66
+ expect(html).not.toContain('<b>Ana</b>');
67
+ });
68
+
69
+ it('mostra um aviso no lugar da tabela quando não há linha nenhuma', () => {
70
+ const html = renderBlock({ kind: 'table', columns: [{ header: 'Aluno' }], rows: [] });
71
+
72
+ expect(html).toContain('table-empty');
73
+ expect(html).not.toContain('<table');
74
+ });
75
+
76
+ it('preenche célula vazia com travessão em vez de deixar buraco', () => {
77
+ const html = renderBlock(table([['Ana', null]]));
78
+
79
+ expect(html).toContain('—');
80
+ });
81
+ });
82
+
83
+ describe('documento', () => {
84
+ it('monta o letterhead com sobretítulo, título e metadados', () => {
85
+ const html = renderDocumentHtml(
86
+ {
87
+ title: 'Alunos',
88
+ eyebrow: 'Relatório de Alunos',
89
+ subtitle: 'BRClick',
90
+ meta: [{ label: 'Status', value: 'Todos os status' }],
91
+ generatedAt: new Date('2026-09-22T13:32:00Z'),
92
+ blocks: [{ kind: 'section', title: 'Resumo' }],
93
+ },
94
+ theme,
95
+ );
96
+
97
+ expect(html).toContain('<div class="eyebrow">Relatório de Alunos</div>');
98
+ expect(html).toContain('<h1>Alunos</h1>');
99
+ expect(html).toContain('Status: <span class="meta-value">Todos os status</span>');
100
+ expect(html).toContain('Gerado em:');
101
+ });
102
+
103
+ it('escapa o título também no <title> da página', () => {
104
+ const html = renderDocumentHtml(
105
+ { title: 'Turma <A>', blocks: [] },
106
+ theme,
107
+ );
108
+
109
+ expect(html).toContain('<title>Turma &lt;A&gt;</title>');
110
+ });
111
+
112
+ // O SVG é a única string injetada crua — e só os desenhistas do kit produzem.
113
+ it('injeta o SVG do gráfico sem escapar, mas escapa o título dele', () => {
114
+ const html = renderBlock({
115
+ kind: 'chart',
116
+ title: 'Matrículas & conclusões',
117
+ svg: '<svg><rect /></svg>' as never,
118
+ });
119
+
120
+ expect(html).toContain('<svg><rect /></svg>');
121
+ expect(html).toContain('Matrículas &amp; conclusões');
122
+ });
123
+
124
+ it('vira paisagem quando o documento pede', () => {
125
+ const html = renderDocumentHtml({ title: 'Frequência', landscape: true, blocks: [] }, theme);
126
+
127
+ expect(html).toContain('size: A4 landscape');
128
+ });
129
+ });
130
+
131
+ describe('barra e cartões de progresso', () => {
132
+ it('reparte a barra pelo total e mostra a legenda com a porcentagem', () => {
133
+ const html = renderBlock({
134
+ kind: 'segmentBar',
135
+ title: 'Com quem estão',
136
+ segments: [
137
+ { label: 'Com o aluno', value: 1, tone: 'muted' },
138
+ { label: 'Aguardando', value: 3, tone: 'accent' },
139
+ { label: 'Em revisão', value: 0, tone: 'light' },
140
+ ],
141
+ });
142
+
143
+ expect(html).toContain('seg-muted" style="width:25.00%"');
144
+ expect(html).toContain('seg-accent" style="width:75.00%"');
145
+ expect(html).not.toContain('seg-light" style');
146
+ expect(html).toContain('<strong>3 (75%)</strong>');
147
+ });
148
+
149
+ it('escapa o texto de cada cartão e cai no texto de vazio sem itens', () => {
150
+ const html = renderBlock({
151
+ kind: 'progressCards',
152
+ items: [
153
+ {
154
+ title: '<b>Ana</b>',
155
+ value: '25%',
156
+ segments: [{ value: 1, tone: 'ink' }],
157
+ listTitle: 'Tarefas em aberto (1)',
158
+ entries: [
159
+ { text: 'Acesso & API', status: 'Aguardando', tone: 'accent', highlight: true, sub: 'Atrasada', subTone: 'danger' },
160
+ ],
161
+ },
162
+ { title: 'Bruno', value: '100%', segments: [], entries: [], emptyText: 'Tudo concluído.' },
163
+ ],
164
+ });
165
+
166
+ expect(html).toContain('&lt;b&gt;Ana&lt;/b&gt;');
167
+ expect(html).toContain('Acesso &amp; API');
168
+ expect(html).toContain('pc-entry-hl-accent');
169
+ expect(html).toContain('pc-entry-sub-danger');
170
+ expect(html).toContain('<div class="pc-empty">Tudo concluído.</div>');
171
+ });
172
+ });