redmine-context 1.0.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 (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/dist/bundle/index.d.ts +5 -0
  4. package/dist/bundle/index.js +5 -0
  5. package/dist/bundle/json.d.ts +90 -0
  6. package/dist/bundle/json.js +266 -0
  7. package/dist/bundle/markdown.d.ts +75 -0
  8. package/dist/bundle/markdown.js +294 -0
  9. package/dist/bundle/search-list.d.ts +43 -0
  10. package/dist/bundle/search-list.js +53 -0
  11. package/dist/bundle/stable-stringify.d.ts +26 -0
  12. package/dist/bundle/stable-stringify.js +50 -0
  13. package/dist/cache/contract.d.ts +157 -0
  14. package/dist/cache/contract.js +0 -0
  15. package/dist/cache/disk-index.d.ts +82 -0
  16. package/dist/cache/disk-index.js +220 -0
  17. package/dist/cache/disk.d.ts +133 -0
  18. package/dist/cache/disk.js +313 -0
  19. package/dist/cache/gc.d.ts +78 -0
  20. package/dist/cache/gc.js +123 -0
  21. package/dist/cache/get-or-compute.d.ts +36 -0
  22. package/dist/cache/get-or-compute.js +52 -0
  23. package/dist/cache/index.d.ts +9 -0
  24. package/dist/cache/index.js +8 -0
  25. package/dist/cache/keys.d.ts +76 -0
  26. package/dist/cache/keys.js +78 -0
  27. package/dist/cache/memory.d.ts +48 -0
  28. package/dist/cache/memory.js +110 -0
  29. package/dist/cache-first.d.ts +127 -0
  30. package/dist/cache-first.js +227 -0
  31. package/dist/client/errors.d.ts +33 -0
  32. package/dist/client/errors.js +49 -0
  33. package/dist/client/http.d.ts +110 -0
  34. package/dist/client/http.js +207 -0
  35. package/dist/client/index.d.ts +5 -0
  36. package/dist/client/index.js +5 -0
  37. package/dist/client/issues.d.ts +71 -0
  38. package/dist/client/issues.js +100 -0
  39. package/dist/client/search.d.ts +58 -0
  40. package/dist/client/search.js +81 -0
  41. package/dist/config/credentials.d.ts +247 -0
  42. package/dist/config/credentials.js +427 -0
  43. package/dist/config/doctor.d.ts +123 -0
  44. package/dist/config/doctor.js +260 -0
  45. package/dist/config/index.d.ts +6 -0
  46. package/dist/config/index.js +6 -0
  47. package/dist/config/keyring.d.ts +96 -0
  48. package/dist/config/keyring.js +158 -0
  49. package/dist/config/login.d.ts +97 -0
  50. package/dist/config/login.js +189 -0
  51. package/dist/config/settings.d.ts +94 -0
  52. package/dist/config/settings.js +140 -0
  53. package/dist/contract.d.ts +173 -0
  54. package/dist/contract.js +27 -0
  55. package/dist/core.d.ts +1 -0
  56. package/dist/core.js +8 -0
  57. package/dist/extract/audio-extractor.d.ts +105 -0
  58. package/dist/extract/audio-extractor.js +156 -0
  59. package/dist/extract/audio.d.ts +126 -0
  60. package/dist/extract/audio.js +184 -0
  61. package/dist/extract/dispatcher.d.ts +132 -0
  62. package/dist/extract/dispatcher.js +115 -0
  63. package/dist/extract/download.d.ts +111 -0
  64. package/dist/extract/download.js +261 -0
  65. package/dist/extract/duration.d.ts +106 -0
  66. package/dist/extract/duration.js +148 -0
  67. package/dist/extract/ffmpeg.d.ts +56 -0
  68. package/dist/extract/ffmpeg.js +95 -0
  69. package/dist/extract/gguf.d.ts +137 -0
  70. package/dist/extract/gguf.js +215 -0
  71. package/dist/extract/index.d.ts +19 -0
  72. package/dist/extract/index.js +19 -0
  73. package/dist/extract/magic.d.ts +80 -0
  74. package/dist/extract/magic.js +282 -0
  75. package/dist/extract/ooxml.d.ts +131 -0
  76. package/dist/extract/ooxml.js +336 -0
  77. package/dist/extract/pdf.d.ts +147 -0
  78. package/dist/extract/pdf.js +322 -0
  79. package/dist/extract/queue.d.ts +167 -0
  80. package/dist/extract/queue.js +217 -0
  81. package/dist/extract/subprocess.d.ts +145 -0
  82. package/dist/extract/subprocess.js +181 -0
  83. package/dist/extract/tesseract.d.ts +153 -0
  84. package/dist/extract/tesseract.js +321 -0
  85. package/dist/extract/video-extractor.d.ts +84 -0
  86. package/dist/extract/video-extractor.js +89 -0
  87. package/dist/extract/video.d.ts +198 -0
  88. package/dist/extract/video.js +418 -0
  89. package/dist/extract/which.d.ts +63 -0
  90. package/dist/extract/which.js +72 -0
  91. package/dist/extract/whisper-extract.d.ts +211 -0
  92. package/dist/extract/whisper-extract.js +323 -0
  93. package/dist/extract/whisper.d.ts +50 -0
  94. package/dist/extract/whisper.js +67 -0
  95. package/dist/extract/zip.d.ts +50 -0
  96. package/dist/extract/zip.js +165 -0
  97. package/dist/extract-issue-attachments.d.ts +82 -0
  98. package/dist/extract-issue-attachments.js +156 -0
  99. package/dist/fetch-attachment-text.d.ts +113 -0
  100. package/dist/fetch-attachment-text.js +155 -0
  101. package/dist/fetch-issue-bundle.d.ts +81 -0
  102. package/dist/fetch-issue-bundle.js +93 -0
  103. package/dist/fetch-issue-search.d.ts +74 -0
  104. package/dist/fetch-issue-search.js +120 -0
  105. package/dist/index.d.ts +21 -0
  106. package/dist/index.js +67 -0
  107. package/dist/normalize/collections.d.ts +52 -0
  108. package/dist/normalize/collections.js +170 -0
  109. package/dist/normalize/helpers.d.ts +35 -0
  110. package/dist/normalize/helpers.js +59 -0
  111. package/dist/normalize/index.d.ts +2 -0
  112. package/dist/normalize/index.js +2 -0
  113. package/dist/normalize/issue.d.ts +34 -0
  114. package/dist/normalize/issue.js +154 -0
  115. package/dist/surfaces/cli/commands.d.ts +61 -0
  116. package/dist/surfaces/cli/commands.js +262 -0
  117. package/dist/surfaces/cli/main.d.ts +32 -0
  118. package/dist/surfaces/cli/main.js +210 -0
  119. package/dist/surfaces/cli/prompts.d.ts +66 -0
  120. package/dist/surfaces/cli/prompts.js +147 -0
  121. package/dist/surfaces/cli/tty.d.ts +27 -0
  122. package/dist/surfaces/cli/tty.js +35 -0
  123. package/dist/surfaces/cli/types.d.ts +39 -0
  124. package/dist/surfaces/cli/types.js +7 -0
  125. package/dist/surfaces/mcp/server.d.ts +171 -0
  126. package/dist/surfaces/mcp/server.js +427 -0
  127. package/dist/surfaces/tui/app.d.ts +55 -0
  128. package/dist/surfaces/tui/app.js +180 -0
  129. package/dist/surfaces/tui/attachment-status.d.ts +79 -0
  130. package/dist/surfaces/tui/attachment-status.js +113 -0
  131. package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
  132. package/dist/surfaces/tui/components/breadcrumb.js +31 -0
  133. package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
  134. package/dist/surfaces/tui/components/gradient-text.js +75 -0
  135. package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
  136. package/dist/surfaces/tui/components/scroll-view.js +77 -0
  137. package/dist/surfaces/tui/components/spinner.d.ts +12 -0
  138. package/dist/surfaces/tui/components/spinner.js +39 -0
  139. package/dist/surfaces/tui/components/text-input.d.ts +45 -0
  140. package/dist/surfaces/tui/components/text-input.js +114 -0
  141. package/dist/surfaces/tui/format-file-size.d.ts +24 -0
  142. package/dist/surfaces/tui/format-file-size.js +43 -0
  143. package/dist/surfaces/tui/glyphs.d.ts +47 -0
  144. package/dist/surfaces/tui/glyphs.js +84 -0
  145. package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
  146. package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
  147. package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
  148. package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
  149. package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
  150. package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
  151. package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
  152. package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
  153. package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
  154. package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
  155. package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
  156. package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
  157. package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
  158. package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
  159. package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
  160. package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
  161. package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
  162. package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
  163. package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
  164. package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
  165. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
  166. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
  167. package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
  168. package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
  169. package/dist/surfaces/tui/index.d.ts +50 -0
  170. package/dist/surfaces/tui/index.js +158 -0
  171. package/dist/surfaces/tui/instance.d.ts +37 -0
  172. package/dist/surfaces/tui/instance.js +36 -0
  173. package/dist/surfaces/tui/job-registry.d.ts +113 -0
  174. package/dist/surfaces/tui/job-registry.js +123 -0
  175. package/dist/surfaces/tui/job-status.d.ts +45 -0
  176. package/dist/surfaces/tui/job-status.js +81 -0
  177. package/dist/surfaces/tui/navigation.d.ts +74 -0
  178. package/dist/surfaces/tui/navigation.js +87 -0
  179. package/dist/surfaces/tui/palettes.d.ts +38 -0
  180. package/dist/surfaces/tui/palettes.js +244 -0
  181. package/dist/surfaces/tui/screen.d.ts +30 -0
  182. package/dist/surfaces/tui/screen.js +51 -0
  183. package/dist/surfaces/tui/screens/about.d.ts +2 -0
  184. package/dist/surfaces/tui/screens/about.js +35 -0
  185. package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
  186. package/dist/surfaces/tui/screens/appearance.js +74 -0
  187. package/dist/surfaces/tui/screens/config.d.ts +2 -0
  188. package/dist/surfaces/tui/screens/config.js +82 -0
  189. package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
  190. package/dist/surfaces/tui/screens/doctor.js +109 -0
  191. package/dist/surfaces/tui/screens/export.d.ts +2 -0
  192. package/dist/surfaces/tui/screens/export.js +168 -0
  193. package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
  194. package/dist/surfaces/tui/screens/home-selection.js +80 -0
  195. package/dist/surfaces/tui/screens/home.d.ts +2 -0
  196. package/dist/surfaces/tui/screens/home.js +200 -0
  197. package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
  198. package/dist/surfaces/tui/screens/issue-detail.js +182 -0
  199. package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
  200. package/dist/surfaces/tui/screens/jobs.js +89 -0
  201. package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
  202. package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
  203. package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
  204. package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
  205. package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
  206. package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
  207. package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
  208. package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
  209. package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
  210. package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
  211. package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
  212. package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
  213. package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
  214. package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
  215. package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
  216. package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
  217. package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
  218. package/dist/surfaces/tui/screens/welcome.js +95 -0
  219. package/dist/surfaces/tui/status-color.d.ts +21 -0
  220. package/dist/surfaces/tui/status-color.js +23 -0
  221. package/dist/surfaces/tui/symbols.d.ts +231 -0
  222. package/dist/surfaces/tui/symbols.js +14 -0
  223. package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
  224. package/dist/surfaces/tui/terminal-colors.js +39 -0
  225. package/dist/surfaces/tui/theme.d.ts +156 -0
  226. package/dist/surfaces/tui/theme.js +86 -0
  227. package/dist/surfaces/tui/truncate.d.ts +31 -0
  228. package/dist/surfaces/tui/truncate.js +81 -0
  229. package/package.json +93 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Mapeamento de status de extração de anexo para a apresentação na TUI (#32).
3
+ *
4
+ * MIGRAÇÃO (M3-08): desde a #50, `src/contract.ts` — a fronteira única que as
5
+ * superfícies consomem do core (ADR-005) — PASSA a exportar o vocabulário
6
+ * canônico {@link ExtractionStatus} do pipeline de extração real. Este módulo
7
+ * foi então REALINHADO: {@link AttachmentExtractionStatus} agora é um ALIAS do
8
+ * tipo do contrato (não mais uma união local paralela), de modo que a TUI e o
9
+ * core compartilham exatamente os mesmos estados — incluindo o novo `skipped`
10
+ * (anexo pulado por exceder o limite de tamanho, ADR-002).
11
+ *
12
+ * O que AINDA é TUI-only e provisório é apenas a HEURÍSTICA
13
+ * {@link deriveAttachmentExtractionStatus}: enquanto a fila de jobs de extração
14
+ * não roda ligada à TUI, o status exibido é derivado só de
15
+ * `Attachment.content_type` (apresentação, sem extração de fato). Quando o core
16
+ * passar a entregar o `ExtractionResult` pronto por anexo (M3/M4), esta derivação
17
+ * deixa de existir e o status vem direto do `ExtractionResult['status']`;
18
+ * `attachmentStatusColor`/`attachmentStatusLabel` já operam sobre o tipo do
19
+ * contrato e permanecem.
20
+ *
21
+ * Valores que {@link deriveAttachmentExtractionStatus} produz HOJE:
22
+ * - `text` — anexo textual (content_type `text/*`), referenciado como texto-only.
23
+ * - `pending` — ainda não é possível classificar (ex.: `content_type` ausente
24
+ * no payload do Redmine) — aguarda uma análise futura, não é erro.
25
+ * - `unsupported` — anexo binário conhecido (imagem/PDF/áudio/vídeo) para o
26
+ * qual não há extrator disponível na TUI ainda.
27
+ *
28
+ * Valores restantes do vocabulário (produzidos pelo core real em M3/M4; o
29
+ * layout de badge já os suporta): `processing` (extração em andamento), `done`
30
+ * (texto extraído com sucesso), `failed` (extração falhou) e `skipped` (pulado
31
+ * deliberadamente, ex.: limite de tamanho).
32
+ */
33
+ import type { Attachment, ExtractionStatus } from '../../index.js';
34
+ import type { Theme } from './theme.js';
35
+ /**
36
+ * Vocabulário de status de extração consumido pela TUI — ALIAS de
37
+ * {@link ExtractionStatus} do contrato (ADR-005), garantindo alinhamento total
38
+ * entre a apresentação e o core. Ver o JSDoc do módulo para a migração.
39
+ */
40
+ export type AttachmentExtractionStatus = ExtractionStatus;
41
+ /**
42
+ * Deriva o {@link AttachmentExtractionStatus} de um anexo — heurística de
43
+ * apresentação a partir SOMENTE do `content_type` (sem extração de fato na tela de
44
+ * detalhe). Produz `text` (texto puro), `pending` (extraível: imagem/PDF/áudio/
45
+ * vídeo/OOXML — o texto sai com `--extract`) ou `unsupported` (sem extrator: ex.:
46
+ * Office binário antigo `.doc`, executáveis, tipos desconhecidos).
47
+ *
48
+ * @param attachment - Recorte do anexo com o único campo relevante aqui.
49
+ * @returns O status derivado.
50
+ * @example
51
+ * deriveAttachmentExtractionStatus({ content_type: 'text/plain' }) // 'text'
52
+ * deriveAttachmentExtractionStatus({ content_type: 'application/pdf' }) // 'pending'
53
+ * deriveAttachmentExtractionStatus({ content_type: 'image/png' }) // 'pending'
54
+ * deriveAttachmentExtractionStatus({ content_type: 'application/x-msdownload' }) // 'unsupported'
55
+ */
56
+ export declare function deriveAttachmentExtractionStatus(attachment: Pick<Attachment, 'content_type'>): AttachmentExtractionStatus;
57
+ /**
58
+ * Cor (token do tema) do badge de status — nunca uma cor literal (varredura
59
+ * anti-hardcode, `tests/surfaces/tui/no-hardcoded-colors.test.ts`).
60
+ *
61
+ * Guideline fixado nesta issue: `muted` = pending/processing (neutro, nada
62
+ * acionável ainda), `primary` = text (já disponível, identidade), `warning` =
63
+ * unsupported (limitação atual, não é erro do usuário), `success`/`danger` =
64
+ * reservados para done/failed (M3/M4) — nunca usados por
65
+ * {@link deriveAttachmentExtractionStatus} hoje.
66
+ *
67
+ * @param theme - Tema ativo (via `useTheme()` no chamador).
68
+ * @param status - Status de extração do anexo.
69
+ * @returns O token de cor do tema.
70
+ */
71
+ export declare function attachmentStatusColor(theme: Theme, status: AttachmentExtractionStatus): string;
72
+ /**
73
+ * Rótulo textual (pt-BR) do badge de status, exibido ao lado do nome/tamanho
74
+ * do anexo.
75
+ *
76
+ * @param status - Status de extração do anexo.
77
+ * @returns O rótulo a exibir dentro do badge (ex.: `[texto]`).
78
+ */
79
+ export declare function attachmentStatusLabel(status: AttachmentExtractionStatus): string;
@@ -0,0 +1,113 @@
1
+ /** Prefixo de `content_type` considerado texto puro (extraível sem OCR/ASR). */
2
+ const TEXT_CONTENT_TYPE_PREFIX = 'text/';
3
+ /**
4
+ * `content_type` (ou prefixo) com EXTRATOR disponível no pipeline hoje (imagem via
5
+ * tesseract; PDF via pdftotext; áudio/vídeo via whisper+ffmpeg; OOXML docx/xlsx/pptx
6
+ * puro-JS). Esses anexos são `pending` (extraíveis; o texto sai com `--extract` /
7
+ * `get_attachment_text`), NÃO `unsupported`. A decisão FINAL é por magic bytes na
8
+ * extração — aqui é só a apresentação a partir do `content_type` do Redmine, então
9
+ * `application/zip` e `application/octet-stream` (genéricos) entram como pending e o
10
+ * magic decide de verdade.
11
+ */
12
+ function isExtractableContentType(contentType) {
13
+ return (contentType.startsWith('image/') ||
14
+ contentType.startsWith('audio/') ||
15
+ contentType.startsWith('video/') ||
16
+ contentType === 'application/pdf' ||
17
+ contentType === 'application/zip' ||
18
+ contentType.startsWith('application/vnd.openxmlformats-officedocument.') ||
19
+ contentType === 'application/octet-stream');
20
+ }
21
+ /**
22
+ * Deriva o {@link AttachmentExtractionStatus} de um anexo — heurística de
23
+ * apresentação a partir SOMENTE do `content_type` (sem extração de fato na tela de
24
+ * detalhe). Produz `text` (texto puro), `pending` (extraível: imagem/PDF/áudio/
25
+ * vídeo/OOXML — o texto sai com `--extract`) ou `unsupported` (sem extrator: ex.:
26
+ * Office binário antigo `.doc`, executáveis, tipos desconhecidos).
27
+ *
28
+ * @param attachment - Recorte do anexo com o único campo relevante aqui.
29
+ * @returns O status derivado.
30
+ * @example
31
+ * deriveAttachmentExtractionStatus({ content_type: 'text/plain' }) // 'text'
32
+ * deriveAttachmentExtractionStatus({ content_type: 'application/pdf' }) // 'pending'
33
+ * deriveAttachmentExtractionStatus({ content_type: 'image/png' }) // 'pending'
34
+ * deriveAttachmentExtractionStatus({ content_type: 'application/x-msdownload' }) // 'unsupported'
35
+ */
36
+ export function deriveAttachmentExtractionStatus(attachment) {
37
+ const { content_type: contentType } = attachment;
38
+ if (contentType === undefined)
39
+ return 'pending';
40
+ if (contentType.startsWith(TEXT_CONTENT_TYPE_PREFIX))
41
+ return 'text';
42
+ if (isExtractableContentType(contentType))
43
+ return 'pending';
44
+ return 'unsupported';
45
+ }
46
+ /**
47
+ * Cor (token do tema) do badge de status — nunca uma cor literal (varredura
48
+ * anti-hardcode, `tests/surfaces/tui/no-hardcoded-colors.test.ts`).
49
+ *
50
+ * Guideline fixado nesta issue: `muted` = pending/processing (neutro, nada
51
+ * acionável ainda), `primary` = text (já disponível, identidade), `warning` =
52
+ * unsupported (limitação atual, não é erro do usuário), `success`/`danger` =
53
+ * reservados para done/failed (M3/M4) — nunca usados por
54
+ * {@link deriveAttachmentExtractionStatus} hoje.
55
+ *
56
+ * @param theme - Tema ativo (via `useTheme()` no chamador).
57
+ * @param status - Status de extração do anexo.
58
+ * @returns O token de cor do tema.
59
+ */
60
+ export function attachmentStatusColor(theme, status) {
61
+ switch (status) {
62
+ case 'text':
63
+ return theme.primary;
64
+ case 'unsupported':
65
+ return theme.warning;
66
+ case 'done':
67
+ return theme.success;
68
+ case 'failed':
69
+ return theme.danger;
70
+ case 'pending':
71
+ case 'processing':
72
+ case 'skipped':
73
+ case 'cancelled':
74
+ return theme.muted;
75
+ default: {
76
+ // Exaustividade: se a união ganhar um novo membro sem atualizar este
77
+ // switch, o typecheck falha aqui (nunca em runtime).
78
+ const exhaustiveCheck = status;
79
+ return exhaustiveCheck;
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * Rótulo textual (pt-BR) do badge de status, exibido ao lado do nome/tamanho
85
+ * do anexo.
86
+ *
87
+ * @param status - Status de extração do anexo.
88
+ * @returns O rótulo a exibir dentro do badge (ex.: `[texto]`).
89
+ */
90
+ export function attachmentStatusLabel(status) {
91
+ switch (status) {
92
+ case 'text':
93
+ return 'texto';
94
+ case 'pending':
95
+ return 'pendente';
96
+ case 'unsupported':
97
+ return 'não suportado';
98
+ case 'processing':
99
+ return 'processando';
100
+ case 'done':
101
+ return 'concluído';
102
+ case 'failed':
103
+ return 'falhou';
104
+ case 'skipped':
105
+ return 'pulado';
106
+ case 'cancelled':
107
+ return 'cancelado';
108
+ default: {
109
+ const exhaustiveCheck = status;
110
+ return exhaustiveCheck;
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,7 @@
1
+ import { type ScreenName } from '../screen.js';
2
+ export interface BreadcrumbProps {
3
+ /** Pilha de telas navegadas (base -> topo) — o topo é a tela atual. */
4
+ stack: readonly ScreenName[];
5
+ }
6
+ /** Breadcrumb fixo no topo do app: título da tela atual em destaque, caminho anterior discreto. */
7
+ export declare function Breadcrumb({ stack }: BreadcrumbProps): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Breadcrumb fixo da TUI (M2-04) — reflete a PILHA de telas do roteador
4
+ * (`../navigation.tsx`) como "Início › Atalhos". Primeiro componente
5
+ * compartilhado da TUI (`components/`): puramente apresentacional — recebe
6
+ * a pilha via prop em vez de ler o contexto de navegação diretamente, para
7
+ * poder ser testado isolado (sem `NavigationProvider`, só `ThemeProvider`).
8
+ *
9
+ * Estética: tela atual em `primary` (negrito), telas anteriores em `muted`,
10
+ * separadas pelo símbolo de navegação de `../symbols.ts` (nunca hardcoded);
11
+ * uma borda inferior discreta em `theme.border` fecha a barra sem competir
12
+ * com o conteúdo da tela abaixo.
13
+ */
14
+ import { Box, Text } from 'ink';
15
+ import { SCREENS } from '../screen.js';
16
+ import { symbols } from '../symbols.js';
17
+ import { useTheme } from '../theme.js';
18
+ /** Breadcrumb fixo no topo do app: título da tela atual em destaque, caminho anterior discreto. */
19
+ export function Breadcrumb({ stack }) {
20
+ const theme = useTheme();
21
+ const lastIndex = stack.length - 1;
22
+ const segments = [];
23
+ stack.forEach((name, index) => {
24
+ const isCurrent = index === lastIndex;
25
+ if (index > 0) {
26
+ segments.push(_jsxs(Text, { color: theme.muted, children: [' ', symbols.pointerSmall, ' '] }, `separator-${index}`));
27
+ }
28
+ segments.push(_jsx(Text, { color: isCurrent ? theme.primary : theme.muted, children: SCREENS[name].title }, `title-${index}`));
29
+ });
30
+ return (_jsx(Box, { paddingX: 1, marginBottom: 1, borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottomColor: theme.border, children: segments }));
31
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Gera `steps` cores interpolando linearmente ao longo da ramp `colors`.
3
+ *
4
+ * @param colors - Paradas de cor (hex).
5
+ * @param steps - Quantidade de cores a produzir (ex.: nº de caracteres).
6
+ * @returns Uma cor hex por passo (vazio se `steps <= 0`).
7
+ */
8
+ export declare function rampColors(colors: readonly string[], steps: number): string[];
9
+ /**
10
+ * Renderiza `children` (texto) com um gradiente ao longo de `colors`.
11
+ *
12
+ * @param props.children - Texto a colorir.
13
+ * @param props.colors - Ramp de cores (hex); vazia ⇒ sem cor.
14
+ * @param props.bold - Negrito (default `false`, #190). Em alguns terminais
15
+ * (ex.: Terminal.app do macOS) o atributo BOLD ignora o fg truecolor e
16
+ * renderiza "preto", ilegível sobre o bg escuro do OSC — por isso o padrão
17
+ * é sem negrito; o destaque do gradiente já vem da própria ramp de cores.
18
+ */
19
+ export declare function GradientText({ children, colors, bold, }: {
20
+ children: string;
21
+ colors: readonly string[];
22
+ bold?: boolean;
23
+ }): import("react").JSX.Element;
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Texto com GRADIENTE de cores (#190) — zero-dependência (não usa `ink-gradient`).
4
+ *
5
+ * Recebe um texto e uma ramp de cores (hex) e pinta cada caractere interpolando
6
+ * linearmente entre as paradas da ramp. Usado em títulos/destaques para dar o
7
+ * "muitas cores maneiras" sem depender de biblioteca nova. As cores vêm do tema
8
+ * (`theme.gradient`), então nenhuma cor literal aparece nas telas (varredura
9
+ * anti-hardcode) — este componente recebe a ramp como prop (variável).
10
+ *
11
+ * Degrada com elegância: ramp vazia ⇒ texto sem cor; 1 cor ⇒ texto sólido.
12
+ */
13
+ import { Text } from 'ink';
14
+ /** Converte `#rgb`/`#rrggbb` em `[r,g,b]` (0-255); `undefined` se inválido. */
15
+ function hexToRgb(hex) {
16
+ const m = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(hex.trim());
17
+ if (m === null)
18
+ return undefined;
19
+ let h = m[1];
20
+ if (h.length === 3)
21
+ h = h.replace(/(.)/g, '$1$1'); // #abc → #aabbcc
22
+ const n = Number.parseInt(h, 16);
23
+ return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
24
+ }
25
+ /** Converte `[r,g,b]` em `#rrggbb`. */
26
+ function rgbToHex([r, g, b]) {
27
+ const to2 = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0');
28
+ return `#${to2(r)}${to2(g)}${to2(b)}`;
29
+ }
30
+ /**
31
+ * Gera `steps` cores interpolando linearmente ao longo da ramp `colors`.
32
+ *
33
+ * @param colors - Paradas de cor (hex).
34
+ * @param steps - Quantidade de cores a produzir (ex.: nº de caracteres).
35
+ * @returns Uma cor hex por passo (vazio se `steps <= 0`).
36
+ */
37
+ export function rampColors(colors, steps) {
38
+ if (steps <= 0)
39
+ return [];
40
+ const stops = colors.map(hexToRgb).filter((c) => c !== undefined);
41
+ if (stops.length === 0)
42
+ return [];
43
+ if (stops.length === 1 || steps === 1) {
44
+ return Array.from({ length: steps }, () => rgbToHex(stops[0]));
45
+ }
46
+ const out = [];
47
+ for (let i = 0; i < steps; i += 1) {
48
+ const t = (i / (steps - 1)) * (stops.length - 1); // posição na ramp [0, stops-1]
49
+ const lo = Math.floor(t);
50
+ const hi = Math.min(lo + 1, stops.length - 1);
51
+ const f = t - lo;
52
+ const a = stops[lo];
53
+ const b = stops[hi];
54
+ out.push(rgbToHex([a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f]));
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Renderiza `children` (texto) com um gradiente ao longo de `colors`.
60
+ *
61
+ * @param props.children - Texto a colorir.
62
+ * @param props.colors - Ramp de cores (hex); vazia ⇒ sem cor.
63
+ * @param props.bold - Negrito (default `false`, #190). Em alguns terminais
64
+ * (ex.: Terminal.app do macOS) o atributo BOLD ignora o fg truecolor e
65
+ * renderiza "preto", ilegível sobre o bg escuro do OSC — por isso o padrão
66
+ * é sem negrito; o destaque do gradiente já vem da própria ramp de cores.
67
+ */
68
+ export function GradientText({ children, colors, bold = false, }) {
69
+ const chars = [...children];
70
+ const ramp = rampColors(colors, chars.length);
71
+ if (ramp.length === 0) {
72
+ return _jsx(Text, { bold: bold, children: children });
73
+ }
74
+ return (_jsx(Text, { bold: bold, children: chars.map((ch, i) => (_jsx(Text, { color: ramp[i], children: ch }, i))) }));
75
+ }
@@ -0,0 +1,25 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Props do `ScrollView`. */
3
+ export interface ScrollViewProps {
4
+ /**
5
+ * Linhas de conteúdo, uma por posição — cada item já é o elemento completo
6
+ * da linha (ex.: `<Text key={...}>...</Text>`), COM `key` próprio (o
7
+ * componente as renderiza diretamente, sem reatribuir chaves).
8
+ */
9
+ lines: ReactNode[];
10
+ /** Altura da janela visível, em linhas. */
11
+ height: number;
12
+ /**
13
+ * Desativa a captura de teclado desta viewport (ex.: um overlay/modal por
14
+ * cima está ativo). Default `true`.
15
+ */
16
+ isActive?: boolean;
17
+ }
18
+ /**
19
+ * Viewport rolável de `height` linhas sobre `lines`. Ver o JSDoc do módulo
20
+ * para o contrato completo de teclado/props.
21
+ *
22
+ * @example
23
+ * <ScrollView lines={rows} height={10} />
24
+ */
25
+ export declare function ScrollView({ lines, height, isActive }: ScrollViewProps): import("react").JSX.Element;
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Viewport rolável genérico (#31): janela de `height` linhas sobre um array
4
+ * `lines` potencialmente maior, sem quebrar o layout da tela que o usa (a
5
+ * altura ocupada na tela é sempre `height`, mesmo perto do fim do conteúdo).
6
+ *
7
+ * Extraído como componente próprio (em vez de embutido em
8
+ * `../screens/issue-detail.tsx`) porque a mecânica de rolagem — deslocar um
9
+ * `offset` sobre um array já "achatado" em linhas, com wrap NÃO permitido
10
+ * (ao contrário de `../hooks/use-list-navigation.ts`, que dá a volta nas
11
+ * pontas) — é genérica o suficiente para qualquer tela futura com conteúdo
12
+ * maior que a viewport (a #31 é a primeira consumidora: descrição + journals
13
+ * da issue).
14
+ *
15
+ * Teclado: `j`/seta para baixo desce 1 linha, `k`/seta para cima sobe 1
16
+ * linha, `PageDown`/`PageUp` deslocam uma página inteira (`height` linhas) de
17
+ * uma vez. Nas duas pontas o `offset` apenas CLAMPA (não há wrap — rolar
18
+ * "além" do topo/fundo não faz sentido para uma leitura linear de texto).
19
+ *
20
+ * `lines` já deve conter os elementos PRONTOS (tipicamente `<Text key=.../>`
21
+ * ou `<Box key=.../>`) — o componente só decide QUAIS `height` delas mostrar
22
+ * a cada momento; formatação/estilo de cada linha é responsabilidade de quem
23
+ * monta o array (ver `../screens/issue-detail.tsx`).
24
+ */
25
+ import { Box, Text, useInput } from 'ink';
26
+ import { useEffect, useState } from 'react';
27
+ import { symbols } from '../symbols.js';
28
+ import { useTheme } from '../theme.js';
29
+ /** Clampa `offset` para `[0, maxOffset]` — sem wrap (ver o JSDoc do módulo). */
30
+ function clampOffset(offset, maxOffset) {
31
+ if (maxOffset <= 0)
32
+ return 0;
33
+ if (offset < 0)
34
+ return 0;
35
+ if (offset > maxOffset)
36
+ return maxOffset;
37
+ return offset;
38
+ }
39
+ /**
40
+ * Viewport rolável de `height` linhas sobre `lines`. Ver o JSDoc do módulo
41
+ * para o contrato completo de teclado/props.
42
+ *
43
+ * @example
44
+ * <ScrollView lines={rows} height={10} />
45
+ */
46
+ export function ScrollView({ lines, height, isActive = true }) {
47
+ const theme = useTheme();
48
+ const maxOffset = Math.max(lines.length - height, 0);
49
+ const [offset, setOffset] = useState(0);
50
+ // Reclampa quando o conteúdo muda de tamanho (ex.: issue recarregada via
51
+ // retry) para não deixar a janela apontando além do novo fim.
52
+ useEffect(() => {
53
+ setOffset((current) => clampOffset(current, maxOffset));
54
+ }, [maxOffset]);
55
+ useInput((input, key) => {
56
+ if (key.pageDown) {
57
+ setOffset((current) => clampOffset(current + height, maxOffset));
58
+ return;
59
+ }
60
+ if (key.pageUp) {
61
+ setOffset((current) => clampOffset(current - height, maxOffset));
62
+ return;
63
+ }
64
+ if (key.downArrow || input === 'j') {
65
+ setOffset((current) => clampOffset(current + 1, maxOffset));
66
+ return;
67
+ }
68
+ if (key.upArrow || input === 'k') {
69
+ setOffset((current) => clampOffset(current - 1, maxOffset));
70
+ }
71
+ }, { isActive });
72
+ const visible = lines.slice(offset, offset + height);
73
+ const canScroll = lines.length > height;
74
+ const atTop = offset <= 0;
75
+ const atBottom = offset >= maxOffset;
76
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { flexDirection: "column", height: height, children: visible }), canScroll ? (_jsx(Box, { children: _jsxs(Text, { color: theme.muted, children: [atTop ? ' ' : symbols.arrowUp, " ", offset + 1, "-", offset + visible.length, "/", lines.length, ' ', atBottom ? ' ' : symbols.arrowDown] }) })) : null] }));
77
+ }
@@ -0,0 +1,12 @@
1
+ /** Sequência de frames do spinner — um giro completo por volta do array (ASCII no Windows legado). */
2
+ export declare const SPINNER_FRAMES: readonly string[];
3
+ /** Intervalo (ms) entre frames — rápido o bastante para parecer fluido, sem sobrecarregar o terminal. */
4
+ export declare const SPINNER_INTERVAL_MS = 80;
5
+ /**
6
+ * Spinner animado em `theme.primary` — usado inline junto de um rótulo (ex.:
7
+ * `<Text><Spinner /> Validando credenciais...</Text>`).
8
+ *
9
+ * @example
10
+ * <Text color={theme.primary}><Spinner /> Validando...</Text>
11
+ */
12
+ export declare function Spinner(): import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Spinner de terminal (#28) — indicador de "operação em andamento" para as
4
+ * telas assíncronas do onboarding (`../screens/onboarding/validating.tsx`,
5
+ * `../screens/onboarding/api-key.tsx`). Implementação própria, sem depender
6
+ * de `@inkjs/ui` (ver `../theme.tsx`, mapeamento previsto): frames braille
7
+ * (mesmo conjunto usado por spinners de terminal populares), avançados por um
8
+ * `setInterval` simples.
9
+ *
10
+ * HARDENING WINDOWS LEGADO (M5-09, #84): os frames vêm de `../glyphs.ts`, que
11
+ * degrada os braille para um spinner ASCII (`| / - \`) no terminal legado do
12
+ * Windows — do contrário apareceriam como mojibake.
13
+ */
14
+ import { useEffect, useState } from 'react';
15
+ import { Text } from 'ink';
16
+ import { glyphs } from '../glyphs.js';
17
+ import { useTheme } from '../theme.js';
18
+ /** Sequência de frames do spinner — um giro completo por volta do array (ASCII no Windows legado). */
19
+ export const SPINNER_FRAMES = glyphs.spinnerFrames;
20
+ /** Intervalo (ms) entre frames — rápido o bastante para parecer fluido, sem sobrecarregar o terminal. */
21
+ export const SPINNER_INTERVAL_MS = 80;
22
+ /**
23
+ * Spinner animado em `theme.primary` — usado inline junto de um rótulo (ex.:
24
+ * `<Text><Spinner /> Validando credenciais...</Text>`).
25
+ *
26
+ * @example
27
+ * <Text color={theme.primary}><Spinner /> Validando...</Text>
28
+ */
29
+ export function Spinner() {
30
+ const theme = useTheme();
31
+ const [frameIndex, setFrameIndex] = useState(0);
32
+ useEffect(() => {
33
+ const timer = setInterval(() => {
34
+ setFrameIndex((current) => (current + 1) % SPINNER_FRAMES.length);
35
+ }, SPINNER_INTERVAL_MS);
36
+ return () => clearInterval(timer);
37
+ }, []);
38
+ return _jsx(Text, { color: theme.primary, children: SPINNER_FRAMES[frameIndex] });
39
+ }
@@ -0,0 +1,45 @@
1
+ /** Props do `TextInput` — ver guideline de uso na documentação do arquivo. */
2
+ export interface TextInputProps {
3
+ /** Valor atual — controlado pelo componente pai (ex.: `useOnboarding().url`). */
4
+ value: string;
5
+ /** Chamado a cada tecla que altera o valor (digitação, backspace/delete). */
6
+ onChange: (value: string) => void;
7
+ /** Chamado quando o usuário confirma com Enter. Opcional. */
8
+ onSubmit?: (value: string) => void;
9
+ /**
10
+ * Caractere de máscara — quando definido, o valor real NUNCA é
11
+ * renderizado; a tela mostra `mask` repetido pelo comprimento do valor.
12
+ * Omitido: eco normal (comportamento padrão).
13
+ */
14
+ mask?: string;
15
+ /** Texto exibido (em `theme.muted`) quando `value` está vazio. */
16
+ placeholder?: string;
17
+ /** Captura teclado quando `true` (default). Ver guideline de foco acima. */
18
+ isActive?: boolean;
19
+ /**
20
+ * Caracteres reservados por uma tecla de CONTROLE da tela pai (ex.: `f`
21
+ * cicla o filtro de status na busca da home, M2-07/#30) — quando o `input`
22
+ * recebido for EXATAMENTE um desses (tecla única, não uma sequência
23
+ * colada com mais de um caractere), o `TextInput` ignora e NÃO chama
24
+ * `onChange`. A tela pai continua recebendo o mesmo evento normalmente
25
+ * (Ink despacha para todos os `useInput()` registrados, não só este) e
26
+ * decide o que fazer com ele — este campo só evita que a tecla de controle
27
+ * também vire texto digitado. Default: nenhum caractere reservado.
28
+ */
29
+ reservedChars?: readonly string[];
30
+ /**
31
+ * Largura máxima (colunas) do texto EXIBIDO — trunca `value`/`placeholder`
32
+ * quando excedida (ver o JSDoc do módulo). Omitido: sem truncamento (uso
33
+ * padrão, campo sozinho na própria linha).
34
+ */
35
+ maxWidth?: number;
36
+ }
37
+ /**
38
+ * Input de texto de linha única com eco normal ou mascarado.
39
+ *
40
+ * @example
41
+ * <TextInput value={url} onChange={setUrl} onSubmit={handleSubmit} placeholder="https://redmine.example" />
42
+ * @example
43
+ * <TextInput value={password} onChange={setPassword} mask="•" isActive={field === 'password'} />
44
+ */
45
+ export declare function TextInput({ value, onChange, onSubmit, mask, placeholder, isActive, reservedChars, maxWidth, }: TextInputProps): import("react").JSX.Element;
@@ -0,0 +1,114 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Input de texto de linha única, Ink puro (M2-05.1, issue #27) — sem
4
+ * depender do `@inkjs/ui` ainda (ver `../theme.tsx`, mapeamento previsto
5
+ * para quando a primeira tela adotar a lib). Cobre o necessário para as
6
+ * telas de onboarding: eco normal (URL, usuário) e eco mascarado (senha,
7
+ * `mask="•"`) — com `mask` definido, o valor real NUNCA é ecoado na tela,
8
+ * só o caractere de máscara repetido pelo comprimento do valor.
9
+ *
10
+ * Suporta apenas edição no fim da string (sem mover o cursor pelo meio) —
11
+ * suficiente para os campos de onboarding (URL, usuário, senha); um cursor
12
+ * navegável fica para quando alguma tela precisar editar no meio do texto.
13
+ *
14
+ * `isActive` (default `true`) desliga a captura de teclado deste input
15
+ * específico — necessário quando mais de um campo divide a mesma tela (ex.:
16
+ * `../screens/onboarding/login.tsx`, usuário + senha): o `useInput()` do
17
+ * Ink não tem noção de foco, então cada `TextInput` "inativo" precisa
18
+ * ignorar o teclado, senão a mesma tecla digitaria nos dois campos ao mesmo
19
+ * tempo.
20
+ *
21
+ * `maxWidth` (M2-16, #39, opcional) trunca o TEXTO EXIBIDO (nunca o `value`
22
+ * de verdade, que segue completo para `onChange`/`onSubmit`) quando este
23
+ * campo divide uma linha com outro elemento (ex.: um rótulo, ver
24
+ * `../screens/export.tsx`) — sem isso, um valor comprido (ex.: um caminho de
25
+ * destino) sobrepõe o rótulo vizinho (o Ink não reflui `<Text>` IRMÃS dentro
26
+ * de um `<Box>` em linha; ver o JSDoc de `../truncate.ts`). O corte é a
27
+ * partir do INÍCIO (`truncateStart`) — o usuário digita/edita no FIM da
28
+ * string (ver o JSDoc do módulo acima), então é o fim que precisa continuar
29
+ * visível.
30
+ */
31
+ import { useCallback, useRef } from 'react';
32
+ import { Text, useInput } from 'ink';
33
+ import { useTheme } from '../theme.js';
34
+ import { truncate, truncateStart } from '../truncate.js';
35
+ /**
36
+ * Input de texto de linha única com eco normal ou mascarado.
37
+ *
38
+ * @example
39
+ * <TextInput value={url} onChange={setUrl} onSubmit={handleSubmit} placeholder="https://redmine.example" />
40
+ * @example
41
+ * <TextInput value={password} onChange={setPassword} mask="•" isActive={field === 'password'} />
42
+ */
43
+ export function TextInput({ value, onChange, onSubmit, mask, placeholder, isActive = true, reservedChars = [], maxWidth, }) {
44
+ const theme = useTheme();
45
+ // O handler do useInput pode disparar antes do resubscribe pós-commit do
46
+ // Ink (ex.: colar "url\n" entrega texto e Enter no mesmo lote) — closures
47
+ // capturariam o value da renderização anterior e o submit sairia vazio.
48
+ // Refs sempre apontam para a versão corrente.
49
+ const valueRef = useRef(value);
50
+ valueRef.current = value;
51
+ const onChangeRef = useRef(onChange);
52
+ onChangeRef.current = onChange;
53
+ const onSubmitRef = useRef(onSubmit);
54
+ onSubmitRef.current = onSubmit;
55
+ const reservedCharsRef = useRef(reservedChars);
56
+ reservedCharsRef.current = reservedChars;
57
+ // Handler ESTÁVEL (useCallback + refs): identidade nova a cada render faz o
58
+ // useInput des/re-subscrever no efeito pós-commit, abrindo janelas sem
59
+ // listener onde teclas rápidas (ou lote colado) se perdem.
60
+ const handleInput = useCallback((input, key) => {
61
+ if (key.return) {
62
+ onSubmitRef.current?.(valueRef.current);
63
+ return;
64
+ }
65
+ // Reason: teclas de navegação/controle não devem virar caracteres no
66
+ // valor — Esc/Tab/setas são atalhos de outras camadas (roteador
67
+ // global, troca de campo do formulário pai); Ctrl/Meta evitam
68
+ // capturar sequências de atalho (ex.: Ctrl+C) como texto digitado.
69
+ if (key.escape ||
70
+ key.tab ||
71
+ key.upArrow ||
72
+ key.downArrow ||
73
+ key.leftArrow ||
74
+ key.rightArrow ||
75
+ key.pageUp ||
76
+ key.pageDown ||
77
+ key.ctrl ||
78
+ key.meta) {
79
+ return;
80
+ }
81
+ if (key.backspace || key.delete) {
82
+ onChangeRef.current(valueRef.current.slice(0, -1));
83
+ return;
84
+ }
85
+ // Reason: tecla ÚNICA reservada por um controle da tela pai (ex.: "f"
86
+ // cicla o filtro de status na busca da home, M2-07/#30) — ignorada
87
+ // aqui para não virar texto digitado; a tela pai trata o mesmo evento
88
+ // via seu próprio `useInput()`. Só bloqueia o caractere isolado, não
89
+ // uma sequência colada maior que o contenha.
90
+ if (reservedCharsRef.current.includes(input)) {
91
+ return;
92
+ }
93
+ if (input.length > 0) {
94
+ onChangeRef.current(valueRef.current + input);
95
+ }
96
+ }, []);
97
+ useInput(handleInput, { isActive });
98
+ // Reason: cursor em bloco simples (inversão de cor, sem literal — não
99
+ // conta como cor hardcoded) só quando o campo está ativo, reforçando
100
+ // visualmente qual campo recebe o próximo caractere digitado.
101
+ const cursor = isActive ? _jsx(Text, { inverse: true, children: " " }) : null;
102
+ // Reserva 1 coluna para o cursor quando ativo — sem isso, o texto truncado
103
+ // + cursor poderia exceder `maxWidth` por 1 caractere.
104
+ const budget = maxWidth !== undefined ? Math.max(maxWidth - (isActive ? 1 : 0), 1) : undefined;
105
+ if (value.length === 0 && placeholder !== undefined) {
106
+ const shownPlaceholder = budget !== undefined ? truncate(placeholder, budget) : placeholder;
107
+ return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: shownPlaceholder }), cursor] }));
108
+ }
109
+ const displayValue = mask !== undefined ? mask.repeat(value.length) : value;
110
+ // truncateStart (não truncate): o usuário edita no FIM da string — é o
111
+ // fim que precisa continuar visível, ver o JSDoc do módulo.
112
+ const shownValue = budget !== undefined ? truncateStart(displayValue, budget) : displayValue;
113
+ return (_jsxs(Text, { children: [shownValue, cursor] }));
114
+ }