tempest-react-sdk 0.73.0 → 0.75.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 (246) hide show
  1. package/README.md +6 -4
  2. package/dist/br/BrazilMap.module.cjs.map +1 -1
  3. package/dist/br/BrazilMap.module.js.map +1 -1
  4. package/dist/br/MapMarkers.cjs +1 -1
  5. package/dist/br/MapMarkers.cjs.map +1 -1
  6. package/dist/br/MapMarkers.js +19 -22
  7. package/dist/br/MapMarkers.js.map +1 -1
  8. package/dist/br/MunicipalitySearch.cjs +1 -1
  9. package/dist/br/MunicipalitySearch.cjs.map +1 -1
  10. package/dist/br/MunicipalitySearch.js +66 -48
  11. package/dist/br/MunicipalitySearch.js.map +1 -1
  12. package/dist/br/MunicipalitySearch.module.cjs +1 -1
  13. package/dist/br/MunicipalitySearch.module.cjs.map +1 -1
  14. package/dist/br/MunicipalitySearch.module.js +4 -3
  15. package/dist/br/MunicipalitySearch.module.js.map +1 -1
  16. package/dist/br.d.ts +12 -0
  17. package/dist/components/AIChat/AIChat.module.cjs.map +1 -1
  18. package/dist/components/AIChat/AIChat.module.js.map +1 -1
  19. package/dist/components/AIChat/AIChatTurn.cjs +1 -1
  20. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  21. package/dist/components/AIChat/AIChatTurn.js +1 -1
  22. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  23. package/dist/components/AppBar/AppBar.cjs +1 -1
  24. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  25. package/dist/components/AppBar/AppBar.js +1 -0
  26. package/dist/components/AppBar/AppBar.js.map +1 -1
  27. package/dist/components/AppBar/AppBar.module.cjs +1 -1
  28. package/dist/components/AppBar/AppBar.module.cjs.map +1 -1
  29. package/dist/components/AppBar/AppBar.module.js +12 -11
  30. package/dist/components/AppBar/AppBar.module.js.map +1 -1
  31. package/dist/components/AppShell/AppShell.module.cjs.map +1 -1
  32. package/dist/components/AppShell/AppShell.module.js.map +1 -1
  33. package/dist/components/Button/Button.cjs +1 -1
  34. package/dist/components/Button/Button.cjs.map +1 -1
  35. package/dist/components/Button/Button.js +41 -36
  36. package/dist/components/Button/Button.js.map +1 -1
  37. package/dist/components/Button/Button.module.cjs.map +1 -1
  38. package/dist/components/Button/Button.module.js.map +1 -1
  39. package/dist/components/Button/ButtonContent.cjs +2 -0
  40. package/dist/components/Button/ButtonContent.cjs.map +1 -0
  41. package/dist/components/Button/ButtonContent.js +34 -0
  42. package/dist/components/Button/ButtonContent.js.map +1 -0
  43. package/dist/components/Button/ButtonSlot.cjs +2 -0
  44. package/dist/components/Button/ButtonSlot.cjs.map +1 -0
  45. package/dist/components/Button/ButtonSlot.js +36 -0
  46. package/dist/components/Button/ButtonSlot.js.map +1 -0
  47. package/dist/components/Button/button-class.cjs +2 -0
  48. package/dist/components/Button/button-class.cjs.map +1 -0
  49. package/dist/components/Button/button-class.js +13 -0
  50. package/dist/components/Button/button-class.js.map +1 -0
  51. package/dist/components/Button/link-rel.cjs +2 -0
  52. package/dist/components/Button/link-rel.cjs.map +1 -0
  53. package/dist/components/Button/link-rel.js +11 -0
  54. package/dist/components/Button/link-rel.js.map +1 -0
  55. package/dist/components/Button/slot.cjs +2 -0
  56. package/dist/components/Button/slot.cjs.map +1 -0
  57. package/dist/components/Button/slot.js +41 -0
  58. package/dist/components/Button/slot.js.map +1 -0
  59. package/dist/components/Carousel/Carousel.module.cjs.map +1 -1
  60. package/dist/components/Carousel/Carousel.module.js.map +1 -1
  61. package/dist/components/Chat/Chat.module.cjs.map +1 -1
  62. package/dist/components/Chat/Chat.module.js.map +1 -1
  63. package/dist/components/Collapsible/Collapsible.cjs +1 -1
  64. package/dist/components/Collapsible/Collapsible.cjs.map +1 -1
  65. package/dist/components/Collapsible/Collapsible.js +1 -1
  66. package/dist/components/Collapsible/Collapsible.js.map +1 -1
  67. package/dist/components/Combobox/Combobox.cjs +1 -1
  68. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  69. package/dist/components/Combobox/Combobox.js +18 -23
  70. package/dist/components/Combobox/Combobox.js.map +1 -1
  71. package/dist/components/Combobox/use-listbox-dismiss.cjs +2 -0
  72. package/dist/components/Combobox/use-listbox-dismiss.cjs.map +1 -0
  73. package/dist/components/Combobox/use-listbox-dismiss.js +34 -0
  74. package/dist/components/Combobox/use-listbox-dismiss.js.map +1 -0
  75. package/dist/components/Dropzone/Dropzone.module.cjs.map +1 -1
  76. package/dist/components/Dropzone/Dropzone.module.js.map +1 -1
  77. package/dist/components/FilterBar/FilterBar.module.cjs.map +1 -1
  78. package/dist/components/FilterBar/FilterBar.module.js.map +1 -1
  79. package/dist/components/FloatingActionButton/FloatingActionButton.module.cjs.map +1 -1
  80. package/dist/components/FloatingActionButton/FloatingActionButton.module.js.map +1 -1
  81. package/dist/components/Form/Form.cjs.map +1 -1
  82. package/dist/components/Form/Form.js.map +1 -1
  83. package/dist/components/ImageCropper/ImageCropper.module.cjs.map +1 -1
  84. package/dist/components/ImageCropper/ImageCropper.module.js.map +1 -1
  85. package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
  86. package/dist/components/InstallButton/InstallButton.js.map +1 -1
  87. package/dist/components/ListTile/ListTile.module.cjs.map +1 -1
  88. package/dist/components/ListTile/ListTile.module.js.map +1 -1
  89. package/dist/components/Markdown/Markdown.module.cjs.map +1 -1
  90. package/dist/components/Markdown/Markdown.module.js.map +1 -1
  91. package/dist/components/Menubar/Menubar.module.cjs.map +1 -1
  92. package/dist/components/Menubar/Menubar.module.js.map +1 -1
  93. package/dist/components/Modal/Modal.cjs +1 -1
  94. package/dist/components/Modal/Modal.cjs.map +1 -1
  95. package/dist/components/Modal/Modal.js +1 -1
  96. package/dist/components/Modal/Modal.js.map +1 -1
  97. package/dist/components/Modal/Modal.module.cjs +1 -1
  98. package/dist/components/Modal/Modal.module.cjs.map +1 -1
  99. package/dist/components/Modal/Modal.module.js +1 -1
  100. package/dist/components/Modal/Modal.module.js.map +1 -1
  101. package/dist/components/MultiSelect/MultiSelect.cjs +1 -1
  102. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  103. package/dist/components/MultiSelect/MultiSelect.js +55 -54
  104. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  105. package/dist/components/Navbar/Navbar.cjs +1 -1
  106. package/dist/components/Navbar/Navbar.cjs.map +1 -1
  107. package/dist/components/Navbar/Navbar.js +1 -0
  108. package/dist/components/Navbar/Navbar.js.map +1 -1
  109. package/dist/components/Navbar/Navbar.module.cjs +1 -1
  110. package/dist/components/Navbar/Navbar.module.cjs.map +1 -1
  111. package/dist/components/Navbar/Navbar.module.js +9 -8
  112. package/dist/components/Navbar/Navbar.module.js.map +1 -1
  113. package/dist/components/NavigationMenu/NavigationMenu.module.cjs.map +1 -1
  114. package/dist/components/NavigationMenu/NavigationMenu.module.js.map +1 -1
  115. package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
  116. package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
  117. package/dist/components/NotificationCenter/NotificationCenter.module.cjs.map +1 -1
  118. package/dist/components/NotificationCenter/NotificationCenter.module.js.map +1 -1
  119. package/dist/components/Page/Page.cjs +1 -1
  120. package/dist/components/Page/Page.cjs.map +1 -1
  121. package/dist/components/Page/Page.js +23 -39
  122. package/dist/components/Page/Page.js.map +1 -1
  123. package/dist/components/Page/Page.module.cjs +1 -1
  124. package/dist/components/Page/Page.module.cjs.map +1 -1
  125. package/dist/components/Page/Page.module.js +5 -11
  126. package/dist/components/Page/Page.module.js.map +1 -1
  127. package/dist/components/PasswordInput/PasswordInput.module.cjs.map +1 -1
  128. package/dist/components/PasswordInput/PasswordInput.module.js.map +1 -1
  129. package/dist/components/PinInput/PinInput.module.cjs.map +1 -1
  130. package/dist/components/PinInput/PinInput.module.js.map +1 -1
  131. package/dist/components/Reveal/Reveal.cjs +2 -0
  132. package/dist/components/Reveal/Reveal.cjs.map +1 -0
  133. package/dist/components/Reveal/Reveal.js +56 -0
  134. package/dist/components/Reveal/Reveal.js.map +1 -0
  135. package/dist/components/Reveal/Reveal.module.cjs +3 -0
  136. package/dist/components/Reveal/Reveal.module.cjs.map +1 -0
  137. package/dist/components/Reveal/Reveal.module.js +7 -0
  138. package/dist/components/Reveal/Reveal.module.js.map +1 -0
  139. package/dist/components/Reveal/reveal-threshold.cjs +2 -0
  140. package/dist/components/Reveal/reveal-threshold.cjs.map +1 -0
  141. package/dist/components/Reveal/reveal-threshold.js +13 -0
  142. package/dist/components/Reveal/reveal-threshold.js.map +1 -0
  143. package/dist/components/SectionHeader/SectionHeader.cjs +2 -0
  144. package/dist/components/SectionHeader/SectionHeader.cjs.map +1 -0
  145. package/dist/components/SectionHeader/SectionHeader.js +66 -0
  146. package/dist/components/SectionHeader/SectionHeader.js.map +1 -0
  147. package/dist/components/SectionHeader/SectionHeader.module.cjs +3 -0
  148. package/dist/components/SectionHeader/SectionHeader.module.cjs.map +1 -0
  149. package/dist/components/SectionHeader/SectionHeader.module.js +15 -0
  150. package/dist/components/SectionHeader/SectionHeader.module.js.map +1 -0
  151. package/dist/components/Timeline/Timeline.cjs +1 -1
  152. package/dist/components/Timeline/Timeline.cjs.map +1 -1
  153. package/dist/components/Timeline/Timeline.js +22 -15
  154. package/dist/components/Timeline/Timeline.js.map +1 -1
  155. package/dist/components/Timeline/Timeline.module.cjs +1 -1
  156. package/dist/components/Timeline/Timeline.module.cjs.map +1 -1
  157. package/dist/components/Timeline/Timeline.module.js +1 -1
  158. package/dist/components/Timeline/Timeline.module.js.map +1 -1
  159. package/dist/components/Tour/Tour.module.cjs.map +1 -1
  160. package/dist/components/Tour/Tour.module.js.map +1 -1
  161. package/dist/components/Transfer/Transfer.module.cjs.map +1 -1
  162. package/dist/components/Transfer/Transfer.module.js.map +1 -1
  163. package/dist/components/VirtualTable/VirtualTable.module.cjs.map +1 -1
  164. package/dist/components/VirtualTable/VirtualTable.module.js.map +1 -1
  165. package/dist/geo/TrajectoryMap.module.cjs.map +1 -1
  166. package/dist/geo/TrajectoryMap.module.js.map +1 -1
  167. package/dist/icons/IconPicker.module.cjs.map +1 -1
  168. package/dist/icons/IconPicker.module.js.map +1 -1
  169. package/dist/styles/Reveal.css +2 -0
  170. package/dist/styles/SectionHeader.css +2 -0
  171. package/dist/styles/actions.css +26 -24
  172. package/dist/styles/base.css +3 -1
  173. package/dist/styles/br.css +3 -3
  174. package/dist/styles/chat.css +5 -5
  175. package/dist/styles/component/AIChat.css +3 -3
  176. package/dist/styles/component/AppBar.css +1 -0
  177. package/dist/styles/component/AppShell.css +1 -1
  178. package/dist/styles/component/BrazilMap.css +2 -2
  179. package/dist/styles/component/Button.css +25 -23
  180. package/dist/styles/component/Carousel.css +3 -3
  181. package/dist/styles/component/Chat.css +2 -2
  182. package/dist/styles/component/Dropzone.css +2 -1
  183. package/dist/styles/component/FilterBar.css +2 -2
  184. package/dist/styles/component/FloatingActionButton.css +1 -1
  185. package/dist/styles/component/IconPicker.css +1 -1
  186. package/dist/styles/component/ImageCropper.css +2 -2
  187. package/dist/styles/component/ListTile.css +1 -1
  188. package/dist/styles/component/Markdown.css +1 -1
  189. package/dist/styles/component/Menubar.css +1 -1
  190. package/dist/styles/component/Modal.css +2 -2
  191. package/dist/styles/component/MunicipalitySearch.css +1 -1
  192. package/dist/styles/component/Navbar.css +1 -0
  193. package/dist/styles/component/NavigationMenu.css +1 -1
  194. package/dist/styles/component/NavigationRail.css +1 -1
  195. package/dist/styles/component/NotificationCenter.css +3 -3
  196. package/dist/styles/component/Page.css +0 -6
  197. package/dist/styles/component/PasswordInput.css +1 -1
  198. package/dist/styles/component/PinInput.css +1 -1
  199. package/dist/styles/component/Reveal.css +9 -0
  200. package/dist/styles/component/SectionHeader.css +9 -0
  201. package/dist/styles/component/Timeline.css +5 -5
  202. package/dist/styles/component/Tour.css +2 -2
  203. package/dist/styles/component/TrajectoryMap.css +1 -1
  204. package/dist/styles/component/Transfer.css +2 -2
  205. package/dist/styles/component/VirtualTable.css +2 -2
  206. package/dist/styles/core.css +3 -1
  207. package/dist/styles/data.css +15 -15
  208. package/dist/styles/feedback.css +3 -3
  209. package/dist/styles/forms.css +6 -5
  210. package/dist/styles/geo.css +1 -1
  211. package/dist/styles/icons.css +1 -1
  212. package/dist/styles/inverse.css +372 -0
  213. package/dist/styles/layout.css +9 -7
  214. package/dist/styles/manifest.json +14 -1
  215. package/dist/styles/navigation.css +8 -6
  216. package/dist/styles/overlay.css +2 -2
  217. package/dist/styles/utility.css +8 -0
  218. package/dist/styles.css +1 -1
  219. package/dist/tempest-react-sdk.cjs +1 -1
  220. package/dist/tempest-react-sdk.d.ts +473 -11
  221. package/dist/tempest-react-sdk.js +350 -346
  222. package/dist/theme/create-theme.cjs +3 -3
  223. package/dist/theme/create-theme.cjs.map +1 -1
  224. package/dist/theme/create-theme.js +72 -55
  225. package/dist/theme/create-theme.js.map +1 -1
  226. package/dist/theme/inverse-render.cjs +3 -0
  227. package/dist/theme/inverse-render.cjs.map +1 -0
  228. package/dist/theme/inverse-render.js +10 -0
  229. package/dist/theme/inverse-render.js.map +1 -0
  230. package/dist/theme/inverse-restore.cjs +5 -0
  231. package/dist/theme/inverse-restore.cjs.map +1 -0
  232. package/dist/theme/inverse-restore.js +30 -0
  233. package/dist/theme/inverse-restore.js.map +1 -0
  234. package/dist/theme/inverse-surface.cjs +2 -0
  235. package/dist/theme/inverse-surface.cjs.map +1 -0
  236. package/dist/theme/inverse-surface.js +255 -0
  237. package/dist/theme/inverse-surface.js.map +1 -0
  238. package/dist/theme/status-tokens.cjs +2 -0
  239. package/dist/theme/status-tokens.cjs.map +1 -0
  240. package/dist/theme/status-tokens.js +15 -0
  241. package/dist/theme/status-tokens.js.map +1 -0
  242. package/dist/vite/create-vite-config.cjs +1 -1
  243. package/dist/vite/create-vite-config.cjs.map +1 -1
  244. package/dist/vite/create-vite-config.js +6 -4
  245. package/dist/vite/create-vite-config.js.map +1 -1
  246. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"AIChat.module.cjs","names":[],"sources":["../../../src/components/AIChat/AIChat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-ai-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n/*\n * The wrapper exists only so the jump-to-latest button can be positioned against\n * the scroll area without being scrolled away with it.\n */\n.threadWrapper {\n position: relative;\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n}\n\n/*\n * `min-width: 0` is load-bearing, not defensive.\n *\n * A flex item defaults to `min-width: auto`, which means \"at least as wide as my\n * content wants\". An answer carrying a wide table or a long line of SQL would make\n * the scroll area itself grow past the panel — and the table's own `overflow-x`\n * inside `Markdown` never gets a chance to kick in, because it is already as wide as\n * it asked to be. Zeroing the floor here is what pushes the overflow back down to\n * the table and the code block, where it scrolls in its own box.\n */\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-5);\n min-width: 0;\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -2px;\n}\n\n.jump {\n position: absolute;\n right: var(--tempest-space-4);\n bottom: var(--tempest-space-3);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: 0;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n box-shadow: var(--tempest-shadow-md);\n cursor: pointer;\n}\n\n.jump:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* Same 44×44 hit-slop as the turn actions: 2rem is a small target for a thumb. */\n@media (pointer: coarse) {\n .jump::after {\n content: \"\";\n position: absolute;\n inset: -6px;\n }\n}\n\n/* --- turn ------------------------------------------------------------------ */\n\n/*\n * Turns, the thinking row, the suggestions and the composer all share one reading\n * column — `--tempest-ai-chat-width`, 48rem by default — centred in whatever width\n * the panel got.\n *\n * A capped column is the right answer from a phone up to a 1920 desktop: prose past\n * roughly 90 characters per line is measurably harder to track back to the start of\n * the next line, and letting an answer run the full width of a wide monitor makes it\n * worse, not more useful. It is deliberately a **knob** rather than a constant\n * because at 2560 and above the trade flips — a 768px ribbon in the middle of a\n * living-room screen is mostly empty room, and only the app knows how far away its\n * user is sitting. Widen it at the root: `--tempest-ai-chat-width: 72rem`.\n *\n * Type size is **not** solved here. Scaling the font for a TV is a decision for\n * `typography.css` and `density.css` — a component-local font ramp would fight the\n * tokens every app themes through.\n */\n.turn {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n min-width: 0;\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n}\n\n/*\n * An assistant turn is full width with no bubble, a user turn is a narrow bubble\n * pushed to the end of the row.\n *\n * That asymmetry is the whole reading model of an LLM transcript: the answer is the\n * document being read, and wrapping it in a bubble would cap its width, fight the\n * code blocks and tables inside it, and make a long answer look like a shouted\n * message. The prompt is short and needs to be told apart at a glance, which a\n * bubble does better than anything else.\n */\n.userTurn {\n align-items: flex-end;\n}\n\n.systemTurn {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.turnHeader {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n min-width: 0;\n}\n\n.avatar {\n display: inline-flex;\n flex: 0 0 auto;\n}\n\n.role {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n}\n\n/*\n * The role of a user turn is announced but not drawn: the bubble on the other side\n * already says who wrote it, and a \"Você\" label over every prompt is noise. Screen\n * readers still get it, because side and colour are not information for them.\n */\n.visuallyHiddenRole {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n border: 0;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n/*\n * The meta row uses the muted token, not the subtle one.\n *\n * `--tempest-text-subtle` at `--tempest-text-2xs` measures **4.37:1** against\n * `--tempest-bg` in the dark theme — under the 4.5:1 floor for text. Measured in a\n * real browser; jsdom's `axe` turns `color-contrast` off because nothing is painted.\n * Size is what de-emphasises this row; colour still has to pass.\n */\n.model,\n.time {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.body {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.userTurn .body {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n max-width: min(85%, 36rem);\n}\n\n.plain {\n white-space: pre-wrap;\n}\n\n.markdownRoot {\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-relaxed);\n}\n\n/*\n * The caret rides the last block of the answer, so it sits at the end of the text\n * instead of on a line of its own.\n *\n * `::after` on the final child is inline content, which is exactly the behaviour\n * wanted: it flows after the last word and moves with it as more text arrives. When\n * the answer starts with a fenced block the caret lands inside the code area — which\n * is where the text is actually being appended, so it is still telling the truth.\n */\n.streamingBody > .markdownRoot > :last-child::after {\n content: \"▍\";\n margin-left: 0.125em;\n color: var(--tempest-primary);\n animation: caretBlink 1s steps(2, jump-none) infinite;\n}\n\n@keyframes caretBlink {\n to {\n opacity: 0.2;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .streamingBody > .markdownRoot > :last-child::after {\n animation: none;\n }\n}\n\n.dots {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n}\n\n.dots > span {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background-color: var(--tempest-text-subtle);\n animation: dotPulse 1.2s var(--tempest-ease-in-out) infinite;\n}\n\n.dots > span:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.dots > span:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes dotPulse {\n 0%,\n 60%,\n 100% {\n opacity: 0.25;\n }\n\n 30% {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dots > span {\n animation: none;\n opacity: 0.6;\n }\n}\n\n.thinking {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n/* --- reasoning ------------------------------------------------------------- */\n\n.reasoning {\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n}\n\n/*\n * Class plus attribute, so this beats `Collapsible`'s own single-class `.trigger`\n * rule whatever order the two modules land in the built stylesheet.\n */\n.reasoning button[aria-expanded] {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.reasoning button[aria-expanded=\"true\"] .reasoningChevron {\n transform: rotate(90deg);\n}\n\n.reasoningChevron {\n flex: 0 0 auto;\n transition: transform var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .reasoningChevron {\n transition: none;\n }\n}\n\n.reasoningBody {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-relaxed);\n white-space: pre-wrap;\n}\n\n/* --- attachments ----------------------------------------------------------- */\n\n.attachments {\n display: flex;\n flex-wrap: wrap;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.attachment {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: 16rem;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-xs);\n}\n\n.attachmentName {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.attachmentMeta {\n flex: 0 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.thumb {\n display: block;\n width: 4rem;\n height: 4rem;\n border-radius: var(--tempest-radius-sm);\n object-fit: cover;\n}\n\n/* --- error ----------------------------------------------------------------- */\n\n.error {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-danger);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-weight: var(--tempest-weight-medium);\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* --- actions --------------------------------------------------------------- */\n\n.actions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n opacity: 0;\n transition: opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n/*\n * The row is revealed on hover and on keyboard focus, and is **always** visible\n * where there is no hover at all.\n *\n * Hiding a control behind `:hover` on a touch screen hides it for good — there is no\n * hover event to trigger it, and the first tap would land on whatever is underneath.\n */\n.turn:hover .actions,\n.actions:focus-within {\n opacity: 1;\n}\n\n@media (hover: none) {\n .actions {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions {\n transition: none;\n }\n}\n\n/*\n * Two classes so the ghost styling wins over `CopyButton`'s own `.button` rule\n * regardless of the order the two CSS modules land in `styles.css`.\n */\n.actions .action {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-sm);\n background: none;\n color: var(--tempest-text-muted);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n/*\n * Touch hit-slop — grows the tap target to 44×44 on a coarse pointer without moving\n * a pixel of the visible button, the same trick `Button` uses for its icon-only\n * sizes.\n *\n * 28×28 clears the 24×24 floor of WCAG 2.5.8 but sits well under the 44×44 of 2.5.5,\n * and four of these sit side by side: on a phone, \"copy\" and \"thumbs down\" are two\n * thumb-widths apart. Padding the button instead would space the row out on desktop,\n * where the pointer is precise and the row should stay quiet.\n */\n@media (pointer: coarse) {\n .actions .action::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n }\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actions .action:hover {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n }\n}\n\n.actions .action:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 1px;\n}\n\n.actions .action[data-copied],\n.actions .actionActive {\n background: none;\n color: var(--tempest-primary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions .action {\n transition: none;\n }\n}\n\n/* --- edit ------------------------------------------------------------------ */\n\n.editor {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n width: 100%;\n max-width: min(85%, 36rem);\n}\n\n.editField {\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: vertical;\n}\n\n.editField:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -1px;\n}\n\n.editActions {\n display: flex;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n}\n\n.ghost {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: transparent;\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n}\n\n.primary {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.primary:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/* --- suggestions ----------------------------------------------------------- */\n\n.suggestions {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: auto auto 0;\n}\n\n.suggestionsTitle {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.suggestion {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .suggestion:hover {\n background-color: var(--tempest-surface-2);\n }\n}\n\n.suggestion:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* --- composer -------------------------------------------------------------- */\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n flex-direction: column;\n gap: var(--tempest-space-1);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n/*\n * The field and the buttons share one bordered box, and the border is drawn here\n * rather than on the textarea: that is what makes the attach control and the send\n * button read as part of the prompt instead of a toolbar next to it.\n */\n.composerBox {\n display: flex;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n}\n\n.composerBox:focus-within {\n border-color: var(--tempest-primary);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: none;\n}\n\n.field:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.composerFooter {\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding-inline: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/*\n * Stop replaces send in the same slot, so the one button under the thumb is always\n * the one that does the thing you want next — send while idle, abort while the answer\n * is coming. Two buttons side by side would mean aiming at the right one mid-stream.\n */\n.stop {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.stopGlyph {\n display: inline-block;\n width: 0.625rem;\n height: 0.625rem;\n border-radius: 2px;\n background-color: currentColor;\n}\n\n@media (max-width: 480px) {\n .thread {\n gap: var(--tempest-space-4);\n padding: var(--tempest-space-3);\n }\n\n .userTurn .body,\n .editor {\n max-width: 100%;\n }\n\n .composer {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n }\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"AIChat.module.cjs","names":[],"sources":["../../../src/components/AIChat/AIChat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-ai-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n/*\n * The wrapper exists only so the jump-to-latest button can be positioned against\n * the scroll area without being scrolled away with it.\n */\n.threadWrapper {\n position: relative;\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n}\n\n/*\n * `min-width: 0` is load-bearing, not defensive.\n *\n * A flex item defaults to `min-width: auto`, which means \"at least as wide as my\n * content wants\". An answer carrying a wide table or a long line of SQL would make\n * the scroll area itself grow past the panel — and the table's own `overflow-x`\n * inside `Markdown` never gets a chance to kick in, because it is already as wide as\n * it asked to be. Zeroing the floor here is what pushes the overflow back down to\n * the table and the code block, where it scrolls in its own box.\n */\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-5);\n min-width: 0;\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: calc(var(--tempest-focus-ring-width) * -1);\n}\n\n.jump {\n position: absolute;\n right: var(--tempest-space-4);\n bottom: var(--tempest-space-3);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: 0;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n box-shadow: var(--tempest-shadow-md);\n cursor: pointer;\n}\n\n.jump:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* Same 44×44 hit-slop as the turn actions: 2rem is a small target for a thumb. */\n@media (pointer: coarse) {\n .jump::after {\n content: \"\";\n position: absolute;\n inset: -6px;\n }\n}\n\n/* --- turn ------------------------------------------------------------------ */\n\n/*\n * Turns, the thinking row, the suggestions and the composer all share one reading\n * column — `--tempest-ai-chat-width`, 48rem by default — centred in whatever width\n * the panel got.\n *\n * A capped column is the right answer from a phone up to a 1920 desktop: prose past\n * roughly 90 characters per line is measurably harder to track back to the start of\n * the next line, and letting an answer run the full width of a wide monitor makes it\n * worse, not more useful. It is deliberately a **knob** rather than a constant\n * because at 2560 and above the trade flips — a 768px ribbon in the middle of a\n * living-room screen is mostly empty room, and only the app knows how far away its\n * user is sitting. Widen it at the root: `--tempest-ai-chat-width: 72rem`.\n *\n * Type size is **not** solved here. Scaling the font for a TV is a decision for\n * `typography.css` and `density.css` — a component-local font ramp would fight the\n * tokens every app themes through.\n */\n.turn {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n min-width: 0;\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n}\n\n/*\n * An assistant turn is full width with no bubble, a user turn is a narrow bubble\n * pushed to the end of the row.\n *\n * That asymmetry is the whole reading model of an LLM transcript: the answer is the\n * document being read, and wrapping it in a bubble would cap its width, fight the\n * code blocks and tables inside it, and make a long answer look like a shouted\n * message. The prompt is short and needs to be told apart at a glance, which a\n * bubble does better than anything else.\n */\n.userTurn {\n align-items: flex-end;\n}\n\n.systemTurn {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.turnHeader {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n min-width: 0;\n}\n\n.avatar {\n display: inline-flex;\n flex: 0 0 auto;\n}\n\n.role {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n}\n\n/*\n * The role of a user turn is announced but not drawn: the bubble on the other side\n * already says who wrote it, and a \"Você\" label over every prompt is noise. Screen\n * readers still get it, because side and colour are not information for them.\n */\n.visuallyHiddenRole {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n border: 0;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n/*\n * The meta row uses the muted token, not the subtle one.\n *\n * `--tempest-text-subtle` at `--tempest-text-2xs` measures **4.37:1** against\n * `--tempest-bg` in the dark theme — under the 4.5:1 floor for text. Measured in a\n * real browser; jsdom's `axe` turns `color-contrast` off because nothing is painted.\n * Size is what de-emphasises this row; colour still has to pass.\n */\n.model,\n.time {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.body {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.userTurn .body {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n max-width: min(85%, 36rem);\n}\n\n.plain {\n white-space: pre-wrap;\n}\n\n.markdownRoot {\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-relaxed);\n}\n\n/*\n * The caret rides the last block of the answer, so it sits at the end of the text\n * instead of on a line of its own.\n *\n * `::after` on the final child is inline content, which is exactly the behaviour\n * wanted: it flows after the last word and moves with it as more text arrives. When\n * the answer starts with a fenced block the caret lands inside the code area — which\n * is where the text is actually being appended, so it is still telling the truth.\n */\n.streamingBody > .markdownRoot > :last-child::after {\n content: \"▍\";\n margin-left: 0.125em;\n color: var(--tempest-primary);\n animation: caretBlink 1s steps(2, jump-none) infinite;\n}\n\n@keyframes caretBlink {\n to {\n opacity: 0.2;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .streamingBody > .markdownRoot > :last-child::after {\n animation: none;\n }\n}\n\n.dots {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n}\n\n.dots > span {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background-color: var(--tempest-text-subtle);\n animation: dotPulse 1.2s var(--tempest-ease-in-out) infinite;\n}\n\n.dots > span:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.dots > span:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes dotPulse {\n 0%,\n 60%,\n 100% {\n opacity: 0.25;\n }\n\n 30% {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dots > span {\n animation: none;\n opacity: 0.6;\n }\n}\n\n.thinking {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n/* --- reasoning ------------------------------------------------------------- */\n\n.reasoning {\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n}\n\n/*\n * Class plus attribute, so this beats `Collapsible`'s own single-class `.trigger`\n * rule whatever order the two modules land in the built stylesheet.\n */\n.reasoning button[aria-expanded] {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.reasoning button[aria-expanded=\"true\"] .reasoningChevron {\n transform: rotate(90deg);\n}\n\n.reasoningChevron {\n flex: 0 0 auto;\n transition: transform var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .reasoningChevron {\n transition: none;\n }\n}\n\n.reasoningBody {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-relaxed);\n white-space: pre-wrap;\n}\n\n/* --- attachments ----------------------------------------------------------- */\n\n.attachments {\n display: flex;\n flex-wrap: wrap;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.attachment {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: 16rem;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-xs);\n}\n\n.attachmentName {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.attachmentMeta {\n flex: 0 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.thumb {\n display: block;\n width: 4rem;\n height: 4rem;\n border-radius: var(--tempest-radius-sm);\n object-fit: cover;\n}\n\n/* --- error ----------------------------------------------------------------- */\n\n.error {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-danger);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-weight: var(--tempest-weight-medium);\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* --- actions --------------------------------------------------------------- */\n\n.actions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n opacity: 0;\n transition: opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n/*\n * The row is revealed on hover and on keyboard focus, and is **always** visible\n * where there is no hover at all.\n *\n * Hiding a control behind `:hover` on a touch screen hides it for good — there is no\n * hover event to trigger it, and the first tap would land on whatever is underneath.\n */\n.turn:hover .actions,\n.actions:focus-within {\n opacity: 1;\n}\n\n@media (hover: none) {\n .actions {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions {\n transition: none;\n }\n}\n\n/*\n * Two classes so the ghost styling wins over `CopyButton`'s own `.button` rule\n * regardless of the order the two CSS modules land in `styles.css`.\n */\n.actions .action {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-sm);\n background: none;\n color: var(--tempest-text-muted);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n/*\n * Touch hit-slop — grows the tap target to 44×44 on a coarse pointer without moving\n * a pixel of the visible button, the same trick `Button` uses for its icon-only\n * sizes.\n *\n * 28×28 clears the 24×24 floor of WCAG 2.5.8 but sits well under the 44×44 of 2.5.5,\n * and four of these sit side by side: on a phone, \"copy\" and \"thumbs down\" are two\n * thumb-widths apart. Padding the button instead would space the row out on desktop,\n * where the pointer is precise and the row should stay quiet.\n */\n@media (pointer: coarse) {\n .actions .action::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n }\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actions .action:hover {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n }\n}\n\n.actions .action:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 1px;\n}\n\n.actions .action[data-copied],\n.actions .actionActive {\n background: none;\n color: var(--tempest-primary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions .action {\n transition: none;\n }\n}\n\n/* --- edit ------------------------------------------------------------------ */\n\n.editor {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n width: 100%;\n max-width: min(85%, 36rem);\n}\n\n.editField {\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: vertical;\n}\n\n.editField:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: calc(var(--tempest-focus-ring-width) * -1);\n}\n\n.editActions {\n display: flex;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n}\n\n.ghost {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: transparent;\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n}\n\n.primary {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.primary:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/* --- suggestions ----------------------------------------------------------- */\n\n.suggestions {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: auto auto 0;\n}\n\n.suggestionsTitle {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.suggestion {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .suggestion:hover {\n background-color: var(--tempest-surface-2);\n }\n}\n\n.suggestion:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* --- composer -------------------------------------------------------------- */\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n flex-direction: column;\n gap: var(--tempest-space-1);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n/*\n * The field and the buttons share one bordered box, and the border is drawn here\n * rather than on the textarea: that is what makes the attach control and the send\n * button read as part of the prompt instead of a toolbar next to it.\n */\n.composerBox {\n display: flex;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n}\n\n.composerBox:focus-within {\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: none;\n}\n\n.field:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.composerFooter {\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding-inline: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/*\n * Stop replaces send in the same slot, so the one button under the thumb is always\n * the one that does the thing you want next — send while idle, abort while the answer\n * is coming. Two buttons side by side would mean aiming at the right one mid-stream.\n */\n.stop {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.stopGlyph {\n display: inline-block;\n width: 0.625rem;\n height: 0.625rem;\n border-radius: 2px;\n background-color: currentColor;\n}\n\n@media (max-width: 480px) {\n .thread {\n gap: var(--tempest-space-4);\n padding: var(--tempest-space-3);\n }\n\n .userTurn .body,\n .editor {\n max-width: 100%;\n }\n\n .composer {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n }\n}\n"],"mappings":";"}
@@ -1 +1 @@
1
- {"version":3,"file":"AIChat.module.js","names":[],"sources":["../../../src/components/AIChat/AIChat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-ai-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n/*\n * The wrapper exists only so the jump-to-latest button can be positioned against\n * the scroll area without being scrolled away with it.\n */\n.threadWrapper {\n position: relative;\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n}\n\n/*\n * `min-width: 0` is load-bearing, not defensive.\n *\n * A flex item defaults to `min-width: auto`, which means \"at least as wide as my\n * content wants\". An answer carrying a wide table or a long line of SQL would make\n * the scroll area itself grow past the panel — and the table's own `overflow-x`\n * inside `Markdown` never gets a chance to kick in, because it is already as wide as\n * it asked to be. Zeroing the floor here is what pushes the overflow back down to\n * the table and the code block, where it scrolls in its own box.\n */\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-5);\n min-width: 0;\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -2px;\n}\n\n.jump {\n position: absolute;\n right: var(--tempest-space-4);\n bottom: var(--tempest-space-3);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: 0;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n box-shadow: var(--tempest-shadow-md);\n cursor: pointer;\n}\n\n.jump:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* Same 44×44 hit-slop as the turn actions: 2rem is a small target for a thumb. */\n@media (pointer: coarse) {\n .jump::after {\n content: \"\";\n position: absolute;\n inset: -6px;\n }\n}\n\n/* --- turn ------------------------------------------------------------------ */\n\n/*\n * Turns, the thinking row, the suggestions and the composer all share one reading\n * column — `--tempest-ai-chat-width`, 48rem by default — centred in whatever width\n * the panel got.\n *\n * A capped column is the right answer from a phone up to a 1920 desktop: prose past\n * roughly 90 characters per line is measurably harder to track back to the start of\n * the next line, and letting an answer run the full width of a wide monitor makes it\n * worse, not more useful. It is deliberately a **knob** rather than a constant\n * because at 2560 and above the trade flips — a 768px ribbon in the middle of a\n * living-room screen is mostly empty room, and only the app knows how far away its\n * user is sitting. Widen it at the root: `--tempest-ai-chat-width: 72rem`.\n *\n * Type size is **not** solved here. Scaling the font for a TV is a decision for\n * `typography.css` and `density.css` — a component-local font ramp would fight the\n * tokens every app themes through.\n */\n.turn {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n min-width: 0;\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n}\n\n/*\n * An assistant turn is full width with no bubble, a user turn is a narrow bubble\n * pushed to the end of the row.\n *\n * That asymmetry is the whole reading model of an LLM transcript: the answer is the\n * document being read, and wrapping it in a bubble would cap its width, fight the\n * code blocks and tables inside it, and make a long answer look like a shouted\n * message. The prompt is short and needs to be told apart at a glance, which a\n * bubble does better than anything else.\n */\n.userTurn {\n align-items: flex-end;\n}\n\n.systemTurn {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.turnHeader {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n min-width: 0;\n}\n\n.avatar {\n display: inline-flex;\n flex: 0 0 auto;\n}\n\n.role {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n}\n\n/*\n * The role of a user turn is announced but not drawn: the bubble on the other side\n * already says who wrote it, and a \"Você\" label over every prompt is noise. Screen\n * readers still get it, because side and colour are not information for them.\n */\n.visuallyHiddenRole {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n border: 0;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n/*\n * The meta row uses the muted token, not the subtle one.\n *\n * `--tempest-text-subtle` at `--tempest-text-2xs` measures **4.37:1** against\n * `--tempest-bg` in the dark theme — under the 4.5:1 floor for text. Measured in a\n * real browser; jsdom's `axe` turns `color-contrast` off because nothing is painted.\n * Size is what de-emphasises this row; colour still has to pass.\n */\n.model,\n.time {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.body {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.userTurn .body {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n max-width: min(85%, 36rem);\n}\n\n.plain {\n white-space: pre-wrap;\n}\n\n.markdownRoot {\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-relaxed);\n}\n\n/*\n * The caret rides the last block of the answer, so it sits at the end of the text\n * instead of on a line of its own.\n *\n * `::after` on the final child is inline content, which is exactly the behaviour\n * wanted: it flows after the last word and moves with it as more text arrives. When\n * the answer starts with a fenced block the caret lands inside the code area — which\n * is where the text is actually being appended, so it is still telling the truth.\n */\n.streamingBody > .markdownRoot > :last-child::after {\n content: \"▍\";\n margin-left: 0.125em;\n color: var(--tempest-primary);\n animation: caretBlink 1s steps(2, jump-none) infinite;\n}\n\n@keyframes caretBlink {\n to {\n opacity: 0.2;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .streamingBody > .markdownRoot > :last-child::after {\n animation: none;\n }\n}\n\n.dots {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n}\n\n.dots > span {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background-color: var(--tempest-text-subtle);\n animation: dotPulse 1.2s var(--tempest-ease-in-out) infinite;\n}\n\n.dots > span:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.dots > span:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes dotPulse {\n 0%,\n 60%,\n 100% {\n opacity: 0.25;\n }\n\n 30% {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dots > span {\n animation: none;\n opacity: 0.6;\n }\n}\n\n.thinking {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n/* --- reasoning ------------------------------------------------------------- */\n\n.reasoning {\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n}\n\n/*\n * Class plus attribute, so this beats `Collapsible`'s own single-class `.trigger`\n * rule whatever order the two modules land in the built stylesheet.\n */\n.reasoning button[aria-expanded] {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.reasoning button[aria-expanded=\"true\"] .reasoningChevron {\n transform: rotate(90deg);\n}\n\n.reasoningChevron {\n flex: 0 0 auto;\n transition: transform var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .reasoningChevron {\n transition: none;\n }\n}\n\n.reasoningBody {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-relaxed);\n white-space: pre-wrap;\n}\n\n/* --- attachments ----------------------------------------------------------- */\n\n.attachments {\n display: flex;\n flex-wrap: wrap;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.attachment {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: 16rem;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-xs);\n}\n\n.attachmentName {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.attachmentMeta {\n flex: 0 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.thumb {\n display: block;\n width: 4rem;\n height: 4rem;\n border-radius: var(--tempest-radius-sm);\n object-fit: cover;\n}\n\n/* --- error ----------------------------------------------------------------- */\n\n.error {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-danger);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-weight: var(--tempest-weight-medium);\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* --- actions --------------------------------------------------------------- */\n\n.actions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n opacity: 0;\n transition: opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n/*\n * The row is revealed on hover and on keyboard focus, and is **always** visible\n * where there is no hover at all.\n *\n * Hiding a control behind `:hover` on a touch screen hides it for good — there is no\n * hover event to trigger it, and the first tap would land on whatever is underneath.\n */\n.turn:hover .actions,\n.actions:focus-within {\n opacity: 1;\n}\n\n@media (hover: none) {\n .actions {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions {\n transition: none;\n }\n}\n\n/*\n * Two classes so the ghost styling wins over `CopyButton`'s own `.button` rule\n * regardless of the order the two CSS modules land in `styles.css`.\n */\n.actions .action {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-sm);\n background: none;\n color: var(--tempest-text-muted);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n/*\n * Touch hit-slop — grows the tap target to 44×44 on a coarse pointer without moving\n * a pixel of the visible button, the same trick `Button` uses for its icon-only\n * sizes.\n *\n * 28×28 clears the 24×24 floor of WCAG 2.5.8 but sits well under the 44×44 of 2.5.5,\n * and four of these sit side by side: on a phone, \"copy\" and \"thumbs down\" are two\n * thumb-widths apart. Padding the button instead would space the row out on desktop,\n * where the pointer is precise and the row should stay quiet.\n */\n@media (pointer: coarse) {\n .actions .action::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n }\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actions .action:hover {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n }\n}\n\n.actions .action:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 1px;\n}\n\n.actions .action[data-copied],\n.actions .actionActive {\n background: none;\n color: var(--tempest-primary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions .action {\n transition: none;\n }\n}\n\n/* --- edit ------------------------------------------------------------------ */\n\n.editor {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n width: 100%;\n max-width: min(85%, 36rem);\n}\n\n.editField {\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: vertical;\n}\n\n.editField:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -1px;\n}\n\n.editActions {\n display: flex;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n}\n\n.ghost {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: transparent;\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n}\n\n.primary {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.primary:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/* --- suggestions ----------------------------------------------------------- */\n\n.suggestions {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: auto auto 0;\n}\n\n.suggestionsTitle {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.suggestion {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .suggestion:hover {\n background-color: var(--tempest-surface-2);\n }\n}\n\n.suggestion:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* --- composer -------------------------------------------------------------- */\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n flex-direction: column;\n gap: var(--tempest-space-1);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n/*\n * The field and the buttons share one bordered box, and the border is drawn here\n * rather than on the textarea: that is what makes the attach control and the send\n * button read as part of the prompt instead of a toolbar next to it.\n */\n.composerBox {\n display: flex;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n}\n\n.composerBox:focus-within {\n border-color: var(--tempest-primary);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: none;\n}\n\n.field:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.composerFooter {\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding-inline: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/*\n * Stop replaces send in the same slot, so the one button under the thumb is always\n * the one that does the thing you want next — send while idle, abort while the answer\n * is coming. Two buttons side by side would mean aiming at the right one mid-stream.\n */\n.stop {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.stopGlyph {\n display: inline-block;\n width: 0.625rem;\n height: 0.625rem;\n border-radius: 2px;\n background-color: currentColor;\n}\n\n@media (max-width: 480px) {\n .thread {\n gap: var(--tempest-space-4);\n padding: var(--tempest-space-3);\n }\n\n .userTurn .body,\n .editor {\n max-width: 100%;\n }\n\n .composer {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n }\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"AIChat.module.js","names":[],"sources":["../../../src/components/AIChat/AIChat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-ai-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n/*\n * The wrapper exists only so the jump-to-latest button can be positioned against\n * the scroll area without being scrolled away with it.\n */\n.threadWrapper {\n position: relative;\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n}\n\n/*\n * `min-width: 0` is load-bearing, not defensive.\n *\n * A flex item defaults to `min-width: auto`, which means \"at least as wide as my\n * content wants\". An answer carrying a wide table or a long line of SQL would make\n * the scroll area itself grow past the panel — and the table's own `overflow-x`\n * inside `Markdown` never gets a chance to kick in, because it is already as wide as\n * it asked to be. Zeroing the floor here is what pushes the overflow back down to\n * the table and the code block, where it scrolls in its own box.\n */\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-5);\n min-width: 0;\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: calc(var(--tempest-focus-ring-width) * -1);\n}\n\n.jump {\n position: absolute;\n right: var(--tempest-space-4);\n bottom: var(--tempest-space-3);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: 0;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n box-shadow: var(--tempest-shadow-md);\n cursor: pointer;\n}\n\n.jump:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* Same 44×44 hit-slop as the turn actions: 2rem is a small target for a thumb. */\n@media (pointer: coarse) {\n .jump::after {\n content: \"\";\n position: absolute;\n inset: -6px;\n }\n}\n\n/* --- turn ------------------------------------------------------------------ */\n\n/*\n * Turns, the thinking row, the suggestions and the composer all share one reading\n * column — `--tempest-ai-chat-width`, 48rem by default — centred in whatever width\n * the panel got.\n *\n * A capped column is the right answer from a phone up to a 1920 desktop: prose past\n * roughly 90 characters per line is measurably harder to track back to the start of\n * the next line, and letting an answer run the full width of a wide monitor makes it\n * worse, not more useful. It is deliberately a **knob** rather than a constant\n * because at 2560 and above the trade flips — a 768px ribbon in the middle of a\n * living-room screen is mostly empty room, and only the app knows how far away its\n * user is sitting. Widen it at the root: `--tempest-ai-chat-width: 72rem`.\n *\n * Type size is **not** solved here. Scaling the font for a TV is a decision for\n * `typography.css` and `density.css` — a component-local font ramp would fight the\n * tokens every app themes through.\n */\n.turn {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n min-width: 0;\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n}\n\n/*\n * An assistant turn is full width with no bubble, a user turn is a narrow bubble\n * pushed to the end of the row.\n *\n * That asymmetry is the whole reading model of an LLM transcript: the answer is the\n * document being read, and wrapping it in a bubble would cap its width, fight the\n * code blocks and tables inside it, and make a long answer look like a shouted\n * message. The prompt is short and needs to be told apart at a glance, which a\n * bubble does better than anything else.\n */\n.userTurn {\n align-items: flex-end;\n}\n\n.systemTurn {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.turnHeader {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n min-width: 0;\n}\n\n.avatar {\n display: inline-flex;\n flex: 0 0 auto;\n}\n\n.role {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n}\n\n/*\n * The role of a user turn is announced but not drawn: the bubble on the other side\n * already says who wrote it, and a \"Você\" label over every prompt is noise. Screen\n * readers still get it, because side and colour are not information for them.\n */\n.visuallyHiddenRole {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n border: 0;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n/*\n * The meta row uses the muted token, not the subtle one.\n *\n * `--tempest-text-subtle` at `--tempest-text-2xs` measures **4.37:1** against\n * `--tempest-bg` in the dark theme — under the 4.5:1 floor for text. Measured in a\n * real browser; jsdom's `axe` turns `color-contrast` off because nothing is painted.\n * Size is what de-emphasises this row; colour still has to pass.\n */\n.model,\n.time {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.body {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.userTurn .body {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n max-width: min(85%, 36rem);\n}\n\n.plain {\n white-space: pre-wrap;\n}\n\n.markdownRoot {\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-relaxed);\n}\n\n/*\n * The caret rides the last block of the answer, so it sits at the end of the text\n * instead of on a line of its own.\n *\n * `::after` on the final child is inline content, which is exactly the behaviour\n * wanted: it flows after the last word and moves with it as more text arrives. When\n * the answer starts with a fenced block the caret lands inside the code area — which\n * is where the text is actually being appended, so it is still telling the truth.\n */\n.streamingBody > .markdownRoot > :last-child::after {\n content: \"▍\";\n margin-left: 0.125em;\n color: var(--tempest-primary);\n animation: caretBlink 1s steps(2, jump-none) infinite;\n}\n\n@keyframes caretBlink {\n to {\n opacity: 0.2;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .streamingBody > .markdownRoot > :last-child::after {\n animation: none;\n }\n}\n\n.dots {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n}\n\n.dots > span {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background-color: var(--tempest-text-subtle);\n animation: dotPulse 1.2s var(--tempest-ease-in-out) infinite;\n}\n\n.dots > span:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.dots > span:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes dotPulse {\n 0%,\n 60%,\n 100% {\n opacity: 0.25;\n }\n\n 30% {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dots > span {\n animation: none;\n opacity: 0.6;\n }\n}\n\n.thinking {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n/* --- reasoning ------------------------------------------------------------- */\n\n.reasoning {\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n}\n\n/*\n * Class plus attribute, so this beats `Collapsible`'s own single-class `.trigger`\n * rule whatever order the two modules land in the built stylesheet.\n */\n.reasoning button[aria-expanded] {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.reasoning button[aria-expanded=\"true\"] .reasoningChevron {\n transform: rotate(90deg);\n}\n\n.reasoningChevron {\n flex: 0 0 auto;\n transition: transform var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .reasoningChevron {\n transition: none;\n }\n}\n\n.reasoningBody {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-relaxed);\n white-space: pre-wrap;\n}\n\n/* --- attachments ----------------------------------------------------------- */\n\n.attachments {\n display: flex;\n flex-wrap: wrap;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.attachment {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n max-width: 16rem;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-xs);\n}\n\n.attachmentName {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.attachmentMeta {\n flex: 0 0 auto;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.thumb {\n display: block;\n width: 4rem;\n height: 4rem;\n border-radius: var(--tempest-radius-sm);\n object-fit: cover;\n}\n\n/* --- error ----------------------------------------------------------------- */\n\n.error {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n margin: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-danger);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-weight: var(--tempest-weight-medium);\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* --- actions --------------------------------------------------------------- */\n\n.actions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n opacity: 0;\n transition: opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n/*\n * The row is revealed on hover and on keyboard focus, and is **always** visible\n * where there is no hover at all.\n *\n * Hiding a control behind `:hover` on a touch screen hides it for good — there is no\n * hover event to trigger it, and the first tap would land on whatever is underneath.\n */\n.turn:hover .actions,\n.actions:focus-within {\n opacity: 1;\n}\n\n@media (hover: none) {\n .actions {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions {\n transition: none;\n }\n}\n\n/*\n * Two classes so the ghost styling wins over `CopyButton`'s own `.button` rule\n * regardless of the order the two CSS modules land in `styles.css`.\n */\n.actions .action {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-sm);\n background: none;\n color: var(--tempest-text-muted);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n/*\n * Touch hit-slop — grows the tap target to 44×44 on a coarse pointer without moving\n * a pixel of the visible button, the same trick `Button` uses for its icon-only\n * sizes.\n *\n * 28×28 clears the 24×24 floor of WCAG 2.5.8 but sits well under the 44×44 of 2.5.5,\n * and four of these sit side by side: on a phone, \"copy\" and \"thumbs down\" are two\n * thumb-widths apart. Padding the button instead would space the row out on desktop,\n * where the pointer is precise and the row should stay quiet.\n */\n@media (pointer: coarse) {\n .actions .action::after {\n content: \"\";\n position: absolute;\n inset: -8px;\n }\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actions .action:hover {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n }\n}\n\n.actions .action:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 1px;\n}\n\n.actions .action[data-copied],\n.actions .actionActive {\n background: none;\n color: var(--tempest-primary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .actions .action {\n transition: none;\n }\n}\n\n/* --- edit ------------------------------------------------------------------ */\n\n.editor {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-2);\n width: 100%;\n max-width: min(85%, 36rem);\n}\n\n.editField {\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: vertical;\n}\n\n.editField:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: calc(var(--tempest-focus-ring-width) * -1);\n}\n\n.editActions {\n display: flex;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n}\n\n.ghost {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: transparent;\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n}\n\n.primary {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.primary:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/* --- suggestions ----------------------------------------------------------- */\n\n.suggestions {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin: auto auto 0;\n}\n\n.suggestionsTitle {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.suggestion {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .suggestion:hover {\n background-color: var(--tempest-surface-2);\n }\n}\n\n.suggestion:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n/* --- composer -------------------------------------------------------------- */\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n flex-direction: column;\n gap: var(--tempest-space-1);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n/*\n * The field and the buttons share one bordered box, and the border is drawn here\n * rather than on the textarea: that is what makes the attach control and the send\n * button read as part of the prompt instead of a toolbar next to it.\n */\n.composerBox {\n display: flex;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n}\n\n.composerBox:focus-within {\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: none;\n}\n\n.field:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.composerFooter {\n max-width: var(--tempest-ai-chat-width, 48rem);\n width: 100%;\n margin-inline: auto;\n padding-inline: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/*\n * Stop replaces send in the same slot, so the one button under the thumb is always\n * the one that does the thing you want next — send while idle, abort while the answer\n * is coming. Two buttons side by side would mean aiming at the right one mid-stream.\n */\n.stop {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.stopGlyph {\n display: inline-block;\n width: 0.625rem;\n height: 0.625rem;\n border-radius: 2px;\n background-color: currentColor;\n}\n\n@media (max-width: 480px) {\n .thread {\n gap: var(--tempest-space-4);\n padding: var(--tempest-space-3);\n }\n\n .userTurn .body,\n .editor {\n max-width: 100%;\n }\n\n .composer {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n }\n}\n"],"mappings":";"}
@@ -1,3 +1,3 @@
1
- const e=require("../../utils/cn.cjs"),t=require("../VisuallyHidden/VisuallyHidden.cjs"),n=require("../../utils/numbers.cjs"),r=require("../CopyButton/CopyButton.cjs"),i=require("../Collapsible/Collapsible.cjs"),a=require("./ai-chat-turns.cjs"),o=require("./AIChat.module.cjs"),s=require("../Markdown/Markdown.cjs");let c=require("react"),l=require("react/jsx-runtime"),u=require("lucide-react");function d({message:d,locale:f=`pt-BR`,canRegenerate:p=!1,onRegenerate:m,onFeedback:h,onEditSubmit:g,onEditError:_,onRetry:v,renderAvatar:y,renderContent:b,vote:x,defaultReasoningOpen:S=!1,showLineNumbers:C=!1}){let w=a.aiChatStrings(f),{role:T,content:E,reasoning:D,streaming:O,error:k,model:A,createdAt:j,attachments:M}=d,[N,P]=(0,c.useState)(null),[F,I]=(0,c.useState)(!1),[L,R]=(0,c.useState)(E),[z,B]=(0,c.useState)(!1),V=x??N,H=T===`user`,U=(0,c.useMemo)(()=>(0,l.jsx)(s.Markdown,{source:E,showLineNumbers:C,className:o.default.markdownRoot}),[E,C]),W=b?b(d):H?(0,l.jsx)(`div`,{className:o.default.plain,children:E}):E===``?null:U,G=async()=>{let e=L.trim();if(e&&!z&&g){B(!0);try{await g(d,e),I(!1)}catch(e){_?.(e)}finally{B(!1)}}},K=e=>{V!==e&&(P(e),h?.(d,e))},q=!O&&!F,J=h!==void 0||p&&m!==void 0;return(0,l.jsxs)(`article`,{className:e.cn(o.default.turn,H&&o.default.userTurn,T===`system`&&o.default.systemTurn,O&&o.default.streamingTurn),"aria-busy":O||void 0,"data-role":T,children:[(0,l.jsxs)(`header`,{className:o.default.turnHeader,children:[y&&(0,l.jsx)(`span`,{className:o.default.avatar,"aria-hidden":`true`,children:y(d)}),(0,l.jsx)(`span`,{className:e.cn(o.default.role,H&&o.default.visuallyHiddenRole),children:a.roleLabel(T,w)}),A&&!H&&(0,l.jsx)(`span`,{className:o.default.model,children:A}),j!==void 0&&(0,l.jsx)(`time`,{className:o.default.time,dateTime:new Date(j).toISOString(),children:a.turnTime(j,f)})]}),M&&M.length>0&&(0,l.jsx)(`ul`,{className:o.default.attachments,children:M.map(e=>(0,l.jsx)(`li`,{className:o.default.attachment,children:e.url?(0,l.jsx)(`img`,{className:o.default.thumb,src:e.url,alt:e.name}):(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(`span`,{className:o.default.attachmentName,children:e.name}),(0,l.jsx)(`span`,{className:o.default.attachmentMeta,children:e.size===void 0?e.mimeType:n.formatBytes(e.size)})]})},e.id))}),D&&(0,l.jsx)(i.Collapsible,{className:o.default.reasoning,defaultOpen:S||O===!0&&E===``,trigger:(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(u.ChevronRight,{className:o.default.reasoningChevron,size:14,"aria-hidden":!0}),w.reasoning]}),children:(0,l.jsx)(`div`,{className:o.default.reasoningBody,children:D})}),F?(0,l.jsxs)(`div`,{className:o.default.editor,children:[(0,l.jsx)(t.VisuallyHidden,{children:(0,l.jsx)(`label`,{htmlFor:`${d.id}-edit`,children:w.editing})}),(0,l.jsx)(`textarea`,{id:`${d.id}-edit`,className:o.default.editField,value:L,rows:Math.min(L.split(`
1
+ const e=require("../../utils/cn.cjs"),t=require("../VisuallyHidden/VisuallyHidden.cjs"),n=require("../../utils/numbers.cjs"),r=require("../CopyButton/CopyButton.cjs"),i=require("../Collapsible/Collapsible.cjs"),a=require("./ai-chat-turns.cjs"),o=require("./AIChat.module.cjs"),s=require("../Markdown/Markdown.cjs");let c=require("react"),l=require("react/jsx-runtime"),u=require("lucide-react");function d({message:d,locale:f=`pt-BR`,canRegenerate:p=!1,onRegenerate:m,onFeedback:h,onEditSubmit:g,onEditError:_,onRetry:v,renderAvatar:y,renderContent:b,vote:x,defaultReasoningOpen:S=!1,showLineNumbers:C=!1}){let w=a.aiChatStrings(f),{role:T,content:E,reasoning:D,streaming:O,error:k,model:A,createdAt:j,attachments:M}=d,[N,P]=(0,c.useState)(null),[F,I]=(0,c.useState)(!1),[L,R]=(0,c.useState)(E),[z,B]=(0,c.useState)(!1),V=x??N,H=T===`user`,U=(0,c.useMemo)(()=>(0,l.jsx)(s.Markdown,{source:E,showLineNumbers:C,className:o.default.markdownRoot}),[E,C]),W=b?b(d):H?(0,l.jsx)(`div`,{className:o.default.plain,children:E}):E===``?null:U,G=async()=>{let e=L.trim();if(e&&!z&&g){B(!0);try{await g(d,e),I(!1)}catch(e){_?.(e)}finally{B(!1)}}},K=e=>{V!==e&&(P(e),h?.(d,e))},q=!O&&!F,J=h!==void 0||p&&m!==void 0;return(0,l.jsxs)(`article`,{className:e.cn(o.default.turn,H&&o.default.userTurn,T===`system`&&o.default.systemTurn),"aria-busy":O||void 0,"data-role":T,children:[(0,l.jsxs)(`header`,{className:o.default.turnHeader,children:[y&&(0,l.jsx)(`span`,{className:o.default.avatar,"aria-hidden":`true`,children:y(d)}),(0,l.jsx)(`span`,{className:e.cn(o.default.role,H&&o.default.visuallyHiddenRole),children:a.roleLabel(T,w)}),A&&!H&&(0,l.jsx)(`span`,{className:o.default.model,children:A}),j!==void 0&&(0,l.jsx)(`time`,{className:o.default.time,dateTime:new Date(j).toISOString(),children:a.turnTime(j,f)})]}),M&&M.length>0&&(0,l.jsx)(`ul`,{className:o.default.attachments,children:M.map(e=>(0,l.jsx)(`li`,{className:o.default.attachment,children:e.url?(0,l.jsx)(`img`,{className:o.default.thumb,src:e.url,alt:e.name}):(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(`span`,{className:o.default.attachmentName,children:e.name}),(0,l.jsx)(`span`,{className:o.default.attachmentMeta,children:e.size===void 0?e.mimeType:n.formatBytes(e.size)})]})},e.id))}),D&&(0,l.jsx)(i.Collapsible,{className:o.default.reasoning,defaultOpen:S||O===!0&&E===``,trigger:(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(u.ChevronRight,{className:o.default.reasoningChevron,size:14,"aria-hidden":!0}),w.reasoning]}),children:(0,l.jsx)(`div`,{className:o.default.reasoningBody,children:D})}),F?(0,l.jsxs)(`div`,{className:o.default.editor,children:[(0,l.jsx)(t.VisuallyHidden,{children:(0,l.jsx)(`label`,{htmlFor:`${d.id}-edit`,children:w.editing})}),(0,l.jsx)(`textarea`,{id:`${d.id}-edit`,className:o.default.editField,value:L,rows:Math.min(L.split(`
2
2
  `).length+1,10),onChange:e=>R(e.target.value)}),(0,l.jsxs)(`div`,{className:o.default.editActions,children:[(0,l.jsx)(`button`,{type:`button`,className:o.default.ghost,onClick:()=>{R(E),I(!1)},children:w.cancel}),(0,l.jsx)(`button`,{type:`button`,className:o.default.primary,disabled:z||L.trim()===``,onClick:()=>void G(),children:w.save})]})]}):(0,l.jsxs)(`div`,{className:e.cn(o.default.body,O&&o.default.streamingBody),children:[W,O===!0&&E===``&&(0,l.jsxs)(`span`,{className:o.default.dots,"aria-hidden":`true`,children:[(0,l.jsx)(`span`,{}),(0,l.jsx)(`span`,{}),(0,l.jsx)(`span`,{})]})]}),k&&(0,l.jsxs)(`p`,{className:o.default.error,role:`alert`,children:[(0,l.jsx)(`span`,{children:k}),v&&(0,l.jsx)(`button`,{type:`button`,className:o.default.retry,onClick:()=>v(d),children:w.retry})]}),q&&(0,l.jsxs)(`div`,{className:o.default.actions,"aria-label":w.turnActions,role:`group`,children:[(0,l.jsx)(r.CopyButton,{className:o.default.action,value:E,"aria-label":w.copy,title:w.copy,children:(0,l.jsx)(u.Copy,{size:14,"aria-hidden":!0})}),H&&g&&(0,l.jsx)(`button`,{type:`button`,className:o.default.action,"aria-label":w.edit,title:w.edit,onClick:()=>{R(E),I(!0)},children:(0,l.jsx)(u.Pencil,{size:14,"aria-hidden":!0})}),!H&&J&&(0,l.jsxs)(l.Fragment,{children:[p&&m&&(0,l.jsx)(`button`,{type:`button`,className:o.default.action,"aria-label":w.regenerate,title:w.regenerate,onClick:()=>m(d),children:(0,l.jsx)(u.RefreshCw,{size:14,"aria-hidden":!0})}),h&&(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(`button`,{type:`button`,className:e.cn(o.default.action,V===`up`&&o.default.actionActive),"aria-label":w.good,title:w.good,"aria-pressed":V===`up`,onClick:()=>K(`up`),children:(0,l.jsx)(u.ThumbsUp,{size:14,"aria-hidden":!0})}),(0,l.jsx)(`button`,{type:`button`,className:e.cn(o.default.action,V===`down`&&o.default.actionActive),"aria-label":w.bad,title:w.bad,"aria-pressed":V===`down`,onClick:()=>K(`down`),children:(0,l.jsx)(u.ThumbsDown,{size:14,"aria-hidden":!0})})]})]})]})]})}exports.AIChatTurn=d;
3
3
  //# sourceMappingURL=AIChatTurn.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"AIChatTurn.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — one turn renders six\n * things that each have their own state: Markdown body, collapsible reasoning,\n * attachments, streaming caret, inline edit form and the action row. The 13 props\n * are the callbacks those actions need plus the render slots, and the body is long\n * because the edit form and the action row share the turn's message and its pending\n * state.\n */\nimport { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n streaming && styles.streamingTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":"2YAkFA,SAAgB,EAAW,CACvB,UACA,SAAS,QACT,gBAAgB,GAChB,eACA,aACA,eACA,cACA,UACA,eACA,gBACA,OACA,uBAAuB,GACvB,kBAAkB,IACF,CAChB,IAAM,EAAU,EAAA,cAAc,CAAM,EAC9B,CAAE,OAAM,UAAS,YAAW,YAAW,QAAO,QAAO,YAAW,eAAgB,EAChF,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAA4B,IAAI,EAC5D,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAS,EAAK,EACtC,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAS,CAAO,EACpC,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAAS,EAAK,EAEpC,EAAc,GAAQ,EACtB,EAAS,IAAS,OAUlB,GAAA,EAAW,EAAA,QAAA,MAET,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CACI,OAAQ,EACS,kBACjB,UAAW,EAAA,QAAO,YACrB,CAAA,EAEL,CAAC,EAAS,CAAe,CAC7B,EASM,EACE,EAAsB,EAAc,CAAO,EAC3C,GAAe,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAa,CAAA,EAC3D,IAAY,GAAW,KACpB,EAGL,EAAa,SAA2B,CAC1C,IAAM,EAAO,EAAM,KAAK,EACpB,GAAC,GAAQ,IAAW,EACxB,GAAU,EAAI,EACd,GAAI,CACA,MAAM,EAAa,EAAS,CAAI,EAChC,EAAW,EAAK,CACpB,OAAS,EAAO,CACZ,IAAc,CAAK,CACvB,QAAU,CACN,EAAU,EAAK,CACnB,CARc,CASlB,EAEM,EAAQ,GAA2B,CACjC,IAAgB,IACpB,EAAa,CAAI,EACjB,IAAa,EAAS,CAAI,EAC9B,EAEM,EAAc,CAAC,GAAa,CAAC,EAC7B,EACF,IAAe,IAAA,IAAc,GAAiB,IAAiB,IAAA,GAEnE,OACI,EAAA,EAAA,KAAA,CAAC,UAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,KACP,GAAU,EAAA,QAAO,SACjB,IAAS,UAAY,EAAA,QAAO,WAC5B,GAAa,EAAA,QAAO,aACxB,EACA,YAAW,GAAa,IAAA,GACxB,YAAW,EARf,SAAA,EAUI,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,WAA1B,SAAA,CACK,IACG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAQ,cAAY,OACvC,SAAA,EAAa,CAAO,CACnB,CAAA,GAEV,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,GAAU,EAAA,QAAO,kBAAkB,EAC/D,SAAA,EAAA,UAAU,EAAM,CAAO,CACtB,CAAA,EACL,GAAS,CAAC,IAAU,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAY,CAAA,EAChE,IAAc,IAAA,KACX,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAM,SAAU,IAAI,KAAK,CAAS,CAAC,CAAC,YAAY,EACnE,SAAA,EAAA,SAAS,EAAW,CAAM,CACzB,CAAA,CAEN,IAEP,GAAe,EAAY,OAAS,IACjC,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,YACjB,SAAA,EAAY,IAAK,IACd,EAAA,EAAA,IAAA,CAAC,KAAD,CAAwB,UAAW,EAAA,QAAO,WACrC,SAAA,EAAW,KACR,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,MAClB,IAAK,EAAW,IAChB,IAAK,EAAW,IACnB,CAAA,GAED,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAiB,SAAA,EAAW,IAAW,CAAA,GAC/D,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eACnB,SAAA,EAAW,OAAS,IAAA,GAEf,EAAW,SADX,EAAA,YAAY,EAAW,IAAI,CAE/B,CAAA,CACR,CAAA,CAAA,CAEN,EAjBK,EAAW,EAiBhB,CACP,CACD,CAAA,EAGP,IACG,EAAA,EAAA,IAAA,CAAC,EAAA,YAAD,CACI,UAAW,EAAA,QAAO,UAClB,YAAa,GAAyB,IAAc,IAAQ,IAAY,GACxE,SACI,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,aAAD,CACI,UAAW,EAAA,QAAO,iBAClB,KAAM,GACN,cAAA,EACH,CAAA,EACA,EAAQ,SACX,CAAA,CAAA,EAGN,UAAA,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,cAAgB,SAAA,CAAe,CAAA,CAC7C,CAAA,EAGhB,GACG,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,UACI,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,QAAS,GAAG,EAAQ,GAAG,OAAS,SAAA,EAAQ,OAAe,CAAA,CAClD,CAAA,GAChB,EAAA,EAAA,IAAA,CAAC,WAAD,CACI,GAAI,GAAG,EAAQ,GAAG,OAClB,UAAW,EAAA,QAAO,UAClB,MAAO,EACP,KAAM,KAAK,IAAI,EAAM,MAAM;CAAI,CAAC,CAAC,OAAS,EAAG,EAAE,EAC/C,SAAW,GAAU,EAAS,EAAM,OAAO,KAAK,CACnD,CAAA,GACD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,YAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,YAAe,CACX,EAAS,CAAO,EAChB,EAAW,EAAK,CACpB,EAEC,SAAA,EAAQ,MACL,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAU,EAAM,KAAK,IAAM,GACrC,YAAe,KAAK,EAAW,EAE9B,SAAA,EAAQ,IACL,CAAA,CACP,GACJ,CAEL,CAAA,GAAA,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,GAAa,EAAA,QAAO,aAAa,EAAjE,SAAA,CACK,EACA,IAAc,IAAQ,IAAY,KAC/B,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAM,cAAY,OAA1C,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAO,CAAA,GACP,EAAA,EAAA,IAAA,CAAC,OAAD,CAAO,CAAA,GACP,EAAA,EAAA,IAAA,CAAC,OAAD,CAAO,CAAA,CACL,CAET,CAAA,CAAA,IAGR,IACG,EAAA,EAAA,KAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,MAAO,KAAK,QAAjC,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,CAAY,CAAA,EAClB,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,YAAe,EAAQ,CAAO,EAE7B,SAAA,EAAQ,KACL,CAAA,CAEb,IAGN,IACG,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAS,aAAY,EAAQ,YAAa,KAAK,QAAtE,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CACI,UAAW,EAAA,QAAO,OAClB,MAAO,EACP,aAAY,EAAQ,KACpB,MAAO,EAAQ,KAEf,UAAA,EAAA,EAAA,IAAA,CAAC,EAAA,KAAD,CAAM,KAAM,GAAI,cAAA,EAAa,CAAA,CACrB,CAAA,EAEX,GAAU,IACP,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,OAClB,aAAY,EAAQ,KACpB,MAAO,EAAQ,KACf,YAAe,CACX,EAAS,CAAO,EAChB,EAAW,EAAI,CACnB,EAEA,UAAA,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CAAQ,KAAM,GAAI,cAAA,EAAa,CAAA,CAC3B,CAAA,EAGX,CAAC,GAAU,IACR,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,GAAiB,IACd,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,OAClB,aAAY,EAAQ,WACpB,MAAO,EAAQ,WACf,YAAe,EAAa,CAAO,EAEnC,UAAA,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CAAW,KAAM,GAAI,cAAA,EAAa,CAAA,CAC9B,CAAA,EAEX,IACG,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GACP,EAAA,QAAO,OACP,IAAgB,MAAQ,EAAA,QAAO,YACnC,EACA,aAAY,EAAQ,KACpB,MAAO,EAAQ,KACf,eAAc,IAAgB,KAC9B,YAAe,EAAK,IAAI,EAExB,UAAA,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CAAU,KAAM,GAAI,cAAA,EAAa,CAAA,CAC7B,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GACP,EAAA,QAAO,OACP,IAAgB,QAAU,EAAA,QAAO,YACrC,EACA,aAAY,EAAQ,IACpB,MAAO,EAAQ,IACf,eAAc,IAAgB,OAC9B,YAAe,EAAK,MAAM,EAE1B,UAAA,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CAAY,KAAM,GAAI,cAAA,EAAa,CAAA,CAC/B,CAAA,CACV,CAAA,CAAA,CAER,CAAA,CAAA,CAEL,GAEJ,GAEjB"}
1
+ {"version":3,"file":"AIChatTurn.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — one turn renders six\n * things that each have their own state: Markdown body, collapsible reasoning,\n * attachments, streaming caret, inline edit form and the action row. The 13 props\n * are the callbacks those actions need plus the render slots, and the body is long\n * because the edit form and the action row share the turn's message and its pending\n * state.\n */\nimport { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":"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"}
@@ -34,7 +34,7 @@ function b({ message: b, locale: x = "pt-BR", canRegenerate: S = !1, onRegenerat
34
34
  X !== e && (U(e), w?.(b, e));
35
35
  }, re = !L && !W, ie = w !== void 0 || S && C !== void 0;
36
36
  return /* @__PURE__ */ p("article", {
37
- className: e(s.turn, Z && s.userTurn, P === "system" && s.systemTurn, L && s.streamingTurn),
37
+ className: e(s.turn, Z && s.userTurn, P === "system" && s.systemTurn),
38
38
  "aria-busy": L || void 0,
39
39
  "data-role": P,
40
40
  children: [
@@ -1 +1 @@
1
- {"version":3,"file":"AIChatTurn.js","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — one turn renders six\n * things that each have their own state: Markdown body, collapsible reasoning,\n * attachments, streaming caret, inline edit form and the action row. The 13 props\n * are the callbacks those actions need plus the render slots, and the body is long\n * because the edit form and the action row share the turn's message and its pending\n * state.\n */\nimport { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n streaming && styles.streamingTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AIChatTurn.js","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — one turn renders six\n * things that each have their own state: Markdown body, collapsible reasoning,\n * attachments, streaming caret, inline edit form and the action row. The 13 props\n * are the callbacks those actions need plus the render slots, and the body is long\n * because the edit form and the action row share the turn's message and its pending\n * state.\n */\nimport { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAkFA,SAAgB,EAAW,EACvB,YACA,YAAS,SACT,mBAAgB,IAChB,iBACA,eACA,iBACA,gBACA,YACA,iBACA,kBACA,SACA,0BAAuB,IACvB,qBAAkB,MACF;CAChB,IAAM,IAAU,EAAc,CAAM,GAC9B,EAAE,SAAM,YAAS,cAAW,cAAW,UAAO,UAAO,cAAW,mBAAgB,GAChF,CAAC,GAAW,KAAgB,EAA4B,IAAI,GAC5D,CAAC,GAAS,KAAc,EAAS,EAAK,GACtC,CAAC,GAAO,KAAY,EAAS,CAAO,GACpC,CAAC,GAAQ,KAAa,EAAS,EAAK,GAEpC,IAAc,KAAQ,GACtB,IAAS,MAAS,QAUlB,IAAW,QAET,kBAAC,GAAD;EACI,QAAQ;EACS;EACjB,WAAW,EAAO;CACrB,CAAA,GAEL,CAAC,GAAS,CAAe,CAC7B,GASM,KACE,IAAsB,EAAc,CAAO,IAC3C,IAAe,kBAAC,OAAD;EAAK,WAAW,EAAO;EAAQ,UAAA;CAAa,CAAA,IAC3D,MAAY,KAAW,OACpB,GAGL,KAAa,YAA2B;EAC1C,IAAM,IAAO,EAAM,KAAK;EACpB,IAAC,KAAQ,MAAW,GACxB;KAAU,EAAI;GACd,IAAI;IAEA,AADA,MAAM,EAAa,GAAS,CAAI,GAChC,EAAW,EAAK;GACpB,SAAS,GAAO;IACZ,IAAc,CAAK;GACvB,UAAU;IACN,EAAU,EAAK;GACnB;EARc;CASlB,GAEM,KAAQ,MAA2B;EACjC,MAAgB,MACpB,EAAa,CAAI,GACjB,IAAa,GAAS,CAAI;CAC9B,GAEM,KAAc,CAAC,KAAa,CAAC,GAC7B,KACF,MAAe,KAAA,KAAc,KAAiB,MAAiB,KAAA;CAEnE,OACI,kBAAC,WAAD;EACI,WAAW,EACP,EAAO,MACP,KAAU,EAAO,UACjB,MAAS,YAAY,EAAO,UAChC;EACA,aAAW,KAAa,KAAA;EACxB,aAAW;EAPf,UAAA;GASI,kBAAC,UAAD;IAAQ,WAAW,EAAO;IAA1B,UAAA;KACK,KACG,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAQ,eAAY;MACvC,UAAA,EAAa,CAAO;KACnB,CAAA;KAEV,kBAAC,QAAD;MAAM,WAAW,EAAG,EAAO,MAAM,KAAU,EAAO,kBAAkB;MAC/D,UAAA,EAAU,GAAM,CAAO;KACtB,CAAA;KACL,KAAS,CAAC,KAAU,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAQ,UAAA;KAAY,CAAA;KAChE,MAAc,KAAA,KACX,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAM,UAAU,IAAI,KAAK,CAAS,CAAC,CAAC,YAAY;MACnE,UAAA,EAAS,GAAW,CAAM;KACzB,CAAA;IAEN;;GAEP,KAAe,EAAY,SAAS,KACjC,kBAAC,MAAD;IAAI,WAAW,EAAO;IACjB,UAAA,EAAY,KAAK,MACd,kBAAC,MAAD;KAAwB,WAAW,EAAO;KACrC,UAAA,EAAW,MACR,kBAAC,OAAD;MACI,WAAW,EAAO;MAClB,KAAK,EAAW;MAChB,KAAK,EAAW;KACnB,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAiB,UAAA,EAAW;KAAW,CAAA,GAC/D,kBAAC,QAAD;MAAM,WAAW,EAAO;MACnB,UAAA,EAAW,SAAS,KAAA,IAEf,EAAW,WADX,EAAY,EAAW,IAAI;KAE/B,CAAA,CACR,EAAA,CAAA;IAEN,GAjBK,EAAW,EAiBhB,CACP;GACD,CAAA;GAGP,KACG,kBAAC,GAAD;IACI,WAAW,EAAO;IAClB,aAAa,KAAyB,MAAc,MAAQ,MAAY;IACxE,SACI,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,GAAD;KACI,WAAW,EAAO;KAClB,MAAM;KACN,eAAA;IACH,CAAA,GACA,EAAQ,SACX,EAAA,CAAA;IAGN,UAAA,kBAAC,OAAD;KAAK,WAAW,EAAO;KAAgB,UAAA;IAAe,CAAA;GAC7C,CAAA;GAGhB,IACG,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA;KACI,kBAAC,GAAD,EAAA,UACI,kBAAC,SAAD;MAAO,SAAS,GAAG,EAAQ,GAAG;MAAS,UAAA,EAAQ;KAAe,CAAA,EAClD,CAAA;KAChB,kBAAC,YAAD;MACI,IAAI,GAAG,EAAQ,GAAG;MAClB,WAAW,EAAO;MAClB,OAAO;MACP,MAAM,KAAK,IAAI,EAAM,MAAM,IAAI,CAAC,CAAC,SAAS,GAAG,EAAE;MAC/C,WAAW,MAAU,EAAS,EAAM,OAAO,KAAK;KACnD,CAAA;KACD,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAvB,UAAA,CACI,kBAAC,UAAD;OACI,MAAK;OACL,WAAW,EAAO;OAClB,eAAe;QAEX,AADA,EAAS,CAAO,GAChB,EAAW,EAAK;OACpB;OAEC,UAAA,EAAQ;MACL,CAAA,GACR,kBAAC,UAAD;OACI,MAAK;OACL,WAAW,EAAO;OAClB,UAAU,KAAU,EAAM,KAAK,MAAM;OACrC,eAAe,KAAK,GAAW;OAE9B,UAAA,EAAQ;MACL,CAAA,CACP;;IACJ;GAEL,CAAA,IAAA,kBAAC,OAAD;IAAK,WAAW,EAAG,EAAO,MAAM,KAAa,EAAO,aAAa;IAAjE,UAAA,CACK,IACA,MAAc,MAAQ,MAAY,MAC/B,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAM,eAAY;KAA1C,UAAA;MACI,kBAAC,QAAD,CAAO,CAAA;MACP,kBAAC,QAAD,CAAO,CAAA;MACP,kBAAC,QAAD,CAAO,CAAA;KACL;IAET,CAAA,CAAA;;GAGR,KACG,kBAAC,KAAD;IAAG,WAAW,EAAO;IAAO,MAAK;IAAjC,UAAA,CACI,kBAAC,QAAD,EAAA,UAAO,EAAY,CAAA,GAClB,KACG,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe,EAAQ,CAAO;KAE7B,UAAA,EAAQ;IACL,CAAA,CAEb;;GAGN,MACG,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAS,cAAY,EAAQ;IAAa,MAAK;IAAtE,UAAA;KACI,kBAAC,IAAD;MACI,WAAW,EAAO;MAClB,OAAO;MACP,cAAY,EAAQ;MACpB,OAAO,EAAQ;MAEf,UAAA,kBAAC,GAAD;OAAM,MAAM;OAAI,eAAA;MAAa,CAAA;KACrB,CAAA;KAEX,KAAU,KACP,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,cAAY,EAAQ;MACpB,OAAO,EAAQ;MACf,eAAe;OAEX,AADA,EAAS,CAAO,GAChB,EAAW,EAAI;MACnB;MAEA,UAAA,kBAAC,GAAD;OAAQ,MAAM;OAAI,eAAA;MAAa,CAAA;KAC3B,CAAA;KAGX,CAAC,KAAU,MACR,kBAAA,GAAA,EAAA,UAAA,CACK,KAAiB,KACd,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,cAAY,EAAQ;MACpB,OAAO,EAAQ;MACf,eAAe,EAAa,CAAO;MAEnC,UAAA,kBAAC,GAAD;OAAW,MAAM;OAAI,eAAA;MAAa,CAAA;KAC9B,CAAA,GAEX,KACG,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EACP,EAAO,QACP,MAAgB,QAAQ,EAAO,YACnC;MACA,cAAY,EAAQ;MACpB,OAAO,EAAQ;MACf,gBAAc,MAAgB;MAC9B,eAAe,EAAK,IAAI;MAExB,UAAA,kBAAC,GAAD;OAAU,MAAM;OAAI,eAAA;MAAa,CAAA;KAC7B,CAAA,GACR,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EACP,EAAO,QACP,MAAgB,UAAU,EAAO,YACrC;MACA,cAAY,EAAQ;MACpB,OAAO,EAAQ;MACf,gBAAc,MAAgB;MAC9B,eAAe,EAAK,MAAM;MAE1B,UAAA,kBAAC,GAAD;OAAY,MAAM;OAAI,eAAA;MAAa,CAAA;KAC/B,CAAA,CACV,EAAA,CAAA,CAER,EAAA,CAAA;IAEL;;EAEJ;;AAEjB"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("./use-sticky-body-warning.cjs"),n=require("./AppBar.module.cjs");let r=require("react/jsx-runtime"),i=require("lucide-react");function a({title:a,leading:o,showBack:s=!1,onBack:c,backLabel:l=`Go back`,backIcon:u,brand:d,actions:f,centered:p=!1,sticky:m=!0,tone:h=`surface`,bordered:g=!0,safeArea:_=!0,className:v,...y}){t.useStickyBodyWarning(m);let b=c??(()=>window.history.back()),x=o??(0,r.jsxs)(r.Fragment,{children:[s&&(0,r.jsx)(`button`,{type:`button`,className:n.default.iconBtn,onClick:b,"aria-label":l,children:u??(0,r.jsx)(i.ArrowLeft,{size:22,"aria-hidden":`true`})}),d&&(0,r.jsx)(`span`,{className:n.default.brand,children:d})]});return(0,r.jsxs)(`header`,{className:e.cn(n.default.bar,n.default[h],p&&n.default.centered,m&&n.default.sticky,g&&n.default.bordered,_&&n.default.safeArea,v),...y,children:[(0,r.jsx)(`div`,{className:n.default.leading,children:x}),a!=null&&a!==``&&(0,r.jsx)(`h1`,{className:n.default.title,"aria-live":`polite`,children:a}),(0,r.jsx)(`div`,{className:n.default.actions,children:f})]})}exports.AppBar=a;
1
+ const e=require("../../utils/cn.cjs"),t=require("./use-sticky-body-warning.cjs"),n=require("./AppBar.module.cjs");let r=require("react/jsx-runtime"),i=require("lucide-react");function a({title:a,leading:o,showBack:s=!1,onBack:c,backLabel:l=`Go back`,backIcon:u,brand:d,actions:f,centered:p=!1,sticky:m=!0,tone:h=`surface`,bordered:g=!0,safeArea:_=!0,className:v,...y}){t.useStickyBodyWarning(m);let b=c??(()=>window.history.back()),x=o??(0,r.jsxs)(r.Fragment,{children:[s&&(0,r.jsx)(`button`,{type:`button`,className:n.default.iconBtn,onClick:b,"aria-label":l,children:u??(0,r.jsx)(i.ArrowLeft,{size:22,"aria-hidden":`true`})}),d&&(0,r.jsx)(`span`,{className:n.default.brand,children:d})]});return(0,r.jsxs)(`header`,{className:e.cn(n.default.bar,n.default[h],p&&n.default.centered,m&&n.default.sticky,g&&n.default.bordered,_&&n.default.safeArea,v),"data-tempest-tone":h===`inverse`?`inverse`:void 0,...y,children:[(0,r.jsx)(`div`,{className:n.default.leading,children:x}),a!=null&&a!==``&&(0,r.jsx)(`h1`,{className:n.default.title,"aria-live":`polite`,children:a}),(0,r.jsx)(`div`,{className:n.default.actions,children:f})]})}exports.AppBar=a;
2
2
  //# sourceMappingURL=AppBar.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppBar.cjs","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport { useStickyBodyWarning } from \"./use-sticky-body-warning\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /**\n * Stick to the top of the scroll container. Default `true`.\n *\n * The page has to leave the body out of the scrolling: `body { overflow-x:\n * hidden }` makes the body a scroll container and the bar scrolls away with\n * the content. Use `overflow-x: clip` on `html` and `body` instead — in\n * development the bar says so in the console when it detects it.\n */\n sticky?: boolean;\n /** Visual tone. Default `\"surface\"`. */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n useStickyBodyWarning(sticky);\n\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":"+KA+EA,SAAgB,EAAO,CACnB,QACA,UACA,WAAW,GACX,SACA,YAAY,UACZ,WACA,QACA,UACA,WAAW,GACX,SAAS,GACT,OAAO,UACP,WAAW,GACX,WAAW,GACX,YACA,GAAG,GACS,CACZ,EAAA,qBAAqB,CAAM,EAE3B,IAAM,EAAa,QAAiB,OAAO,QAAQ,KAAK,GAElD,EAAiB,IACnB,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,QAAS,EACT,aAAY,EAEX,SAAA,IAAY,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CAAW,KAAM,GAAI,cAAY,MAAQ,CAAA,CAClD,CAAA,EAEX,IAAS,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAY,CAAA,CACxD,CAAA,CAAA,EAGN,OACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,IACP,EAAA,QAAO,GACP,GAAY,EAAA,QAAO,SACnB,GAAU,EAAA,QAAO,OACjB,GAAY,EAAA,QAAO,SACnB,GAAY,EAAA,QAAO,SACnB,CACJ,EACA,GAAI,EAVR,SAAA,EAYI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,CAAoB,CAAA,EACpD,GAAS,MAAQ,IAAU,KACxB,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAO,YAAU,SAClC,SAAA,CACD,CAAA,GAER,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,CAAa,CAAA,CAC1C,GAEhB"}
1
+ {"version":3,"file":"AppBar.cjs","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport { useStickyBodyWarning } from \"./use-sticky-body-warning\";\nimport styles from \"./AppBar.module.css\";\n\n/**\n * Visual tone of an {@link AppBar}.\n *\n * - `\"surface\"` — the page's surface and text.\n * - `\"primary\"` — the brand fill (`--tempest-primary`) with\n * `--tempest-primary-foreground` ink. Only the bar's own text and back button\n * follow the fill: components in `actions` keep the page's tokens. Measured in\n * Chromium over the SDK's `#0066ff`, a primary `Button` inside it has the same\n * color as the bar (1:1) and its focus ring reads 1.41:1.\n * - `\"inverse\"` — the inverse surface: every color token inverted inside the bar,\n * so the components in it are measured against the fill. See\n * {@link AppBarProps.tone}.\n * - `\"transparent\"` — no background; the text inherits.\n */\nexport type AppBarTone = \"surface\" | \"primary\" | \"inverse\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /**\n * Stick to the top of the scroll container. Default `true`.\n *\n * The page has to leave the body out of the scrolling: `body { overflow-x:\n * hidden }` makes the body a scroll container and the bar scrolls away with\n * the content. Use `overflow-x: clip` on `html` and `body` instead — in\n * development the bar says so in the console when it detects it.\n */\n sticky?: boolean;\n /**\n * Visual tone. Default `\"surface\"`.\n *\n * `\"inverse\"` writes `data-tempest-tone=\"inverse\"` and paints the bar with the\n * inverse surface's `--tempest-bg`, so the back button, the title and the\n * components in `actions` invert with it. The tokens are opt-in:\n * `tempest-react-sdk/styles/inverse.css` for the SDK's own blue, or\n * `createTheme({ primary, inverse: createInverseSurface })` for yours. With\n * neither loaded the bar falls back to the page's `--tempest-bg` and\n * `--tempest-text` — legible, not inverted.\n *\n * A separate tone rather than a change to `\"primary\"`, because the inverse\n * fill is not always the brand's `500`: for `#0066ff` it is the `800`\n * (`#042e75`), and turning `\"primary\"` into it would repaint every bar that\n * uses it today.\n */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n useStickyBodyWarning(sticky);\n\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n data-tempest-tone={tone === \"inverse\" ? \"inverse\" : undefined}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":"+KA4GA,SAAgB,EAAO,CACnB,QACA,UACA,WAAW,GACX,SACA,YAAY,UACZ,WACA,QACA,UACA,WAAW,GACX,SAAS,GACT,OAAO,UACP,WAAW,GACX,WAAW,GACX,YACA,GAAG,GACS,CACZ,EAAA,qBAAqB,CAAM,EAE3B,IAAM,EAAa,QAAiB,OAAO,QAAQ,KAAK,GAElD,EAAiB,IACnB,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,QAAS,EACT,aAAY,EAEX,SAAA,IAAY,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CAAW,KAAM,GAAI,cAAY,MAAQ,CAAA,CAClD,CAAA,EAEX,IAAS,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAY,CAAA,CACxD,CAAA,CAAA,EAGN,OACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,IACP,EAAA,QAAO,GACP,GAAY,EAAA,QAAO,SACnB,GAAU,EAAA,QAAO,OACjB,GAAY,EAAA,QAAO,SACnB,GAAY,EAAA,QAAO,SACnB,CACJ,EACA,oBAAmB,IAAS,UAAY,UAAY,IAAA,GACpD,GAAI,EAXR,SAAA,EAaI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,CAAoB,CAAA,EACpD,GAAS,MAAQ,IAAU,KACxB,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAO,YAAU,SAClC,SAAA,CACD,CAAA,GAER,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,CAAa,CAAA,CAC1C,GAEhB"}
@@ -21,6 +21,7 @@ function s({ title: s, leading: c, showBack: l = !1, onBack: u, backLabel: d = "
21
21
  })] });
22
22
  return /* @__PURE__ */ a("header", {
23
23
  className: e(n.bar, n[_], h && n.centered, g && n.sticky, v && n.bordered, y && n.safeArea, b),
24
+ "data-tempest-tone": _ === "inverse" ? "inverse" : void 0,
24
25
  ...x,
25
26
  children: [
26
27
  /* @__PURE__ */ i("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"AppBar.js","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport { useStickyBodyWarning } from \"./use-sticky-body-warning\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /**\n * Stick to the top of the scroll container. Default `true`.\n *\n * The page has to leave the body out of the scrolling: `body { overflow-x:\n * hidden }` makes the body a scroll container and the bar scrolls away with\n * the content. Use `overflow-x: clip` on `html` and `body` instead — in\n * development the bar says so in the console when it detects it.\n */\n sticky?: boolean;\n /** Visual tone. Default `\"surface\"`. */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n useStickyBodyWarning(sticky);\n\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":";;;;;;AA+EA,SAAgB,EAAO,EACnB,UACA,YACA,cAAW,IACX,WACA,eAAY,WACZ,aACA,UACA,YACA,cAAW,IACX,YAAS,IACT,UAAO,WACP,cAAW,IACX,cAAW,IACX,cACA,GAAG,KACS;CACZ,EAAqB,CAAM;CAE3B,IAAM,IAAa,YAAiB,OAAO,QAAQ,KAAK,IAElD,IAAiB,KACnB,kBAAA,GAAA,EAAA,UAAA,CACK,KACG,kBAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAO;EAClB,SAAS;EACT,cAAY;EAEX,UAAA,KAAY,kBAAC,GAAD;GAAW,MAAM;GAAI,eAAY;EAAQ,CAAA;CAClD,CAAA,GAEX,KAAS,kBAAC,QAAD;EAAM,WAAW,EAAO;EAAQ,UAAA;CAAY,CAAA,CACxD,EAAA,CAAA;CAGN,OACI,kBAAC,UAAD;EACI,WAAW,EACP,EAAO,KACP,EAAO,IACP,KAAY,EAAO,UACnB,KAAU,EAAO,QACjB,KAAY,EAAO,UACnB,KAAY,EAAO,UACnB,CACJ;EACA,GAAI;EAVR,UAAA;GAYI,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU,UAAA;GAAoB,CAAA;GACpD,KAAS,QAAQ,MAAU,MACxB,kBAAC,MAAD;IAAI,WAAW,EAAO;IAAO,aAAU;IAClC,UAAA;GACD,CAAA;GAER,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU,UAAA;GAAa,CAAA;EAC1C;;AAEhB"}
1
+ {"version":3,"file":"AppBar.js","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport { useStickyBodyWarning } from \"./use-sticky-body-warning\";\nimport styles from \"./AppBar.module.css\";\n\n/**\n * Visual tone of an {@link AppBar}.\n *\n * - `\"surface\"` — the page's surface and text.\n * - `\"primary\"` — the brand fill (`--tempest-primary`) with\n * `--tempest-primary-foreground` ink. Only the bar's own text and back button\n * follow the fill: components in `actions` keep the page's tokens. Measured in\n * Chromium over the SDK's `#0066ff`, a primary `Button` inside it has the same\n * color as the bar (1:1) and its focus ring reads 1.41:1.\n * - `\"inverse\"` — the inverse surface: every color token inverted inside the bar,\n * so the components in it are measured against the fill. See\n * {@link AppBarProps.tone}.\n * - `\"transparent\"` — no background; the text inherits.\n */\nexport type AppBarTone = \"surface\" | \"primary\" | \"inverse\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /**\n * Stick to the top of the scroll container. Default `true`.\n *\n * The page has to leave the body out of the scrolling: `body { overflow-x:\n * hidden }` makes the body a scroll container and the bar scrolls away with\n * the content. Use `overflow-x: clip` on `html` and `body` instead — in\n * development the bar says so in the console when it detects it.\n */\n sticky?: boolean;\n /**\n * Visual tone. Default `\"surface\"`.\n *\n * `\"inverse\"` writes `data-tempest-tone=\"inverse\"` and paints the bar with the\n * inverse surface's `--tempest-bg`, so the back button, the title and the\n * components in `actions` invert with it. The tokens are opt-in:\n * `tempest-react-sdk/styles/inverse.css` for the SDK's own blue, or\n * `createTheme({ primary, inverse: createInverseSurface })` for yours. With\n * neither loaded the bar falls back to the page's `--tempest-bg` and\n * `--tempest-text` — legible, not inverted.\n *\n * A separate tone rather than a change to `\"primary\"`, because the inverse\n * fill is not always the brand's `500`: for `#0066ff` it is the `800`\n * (`#042e75`), and turning `\"primary\"` into it would repaint every bar that\n * uses it today.\n */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n useStickyBodyWarning(sticky);\n\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n data-tempest-tone={tone === \"inverse\" ? \"inverse\" : undefined}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":";;;;;;AA4GA,SAAgB,EAAO,EACnB,UACA,YACA,cAAW,IACX,WACA,eAAY,WACZ,aACA,UACA,YACA,cAAW,IACX,YAAS,IACT,UAAO,WACP,cAAW,IACX,cAAW,IACX,cACA,GAAG,KACS;CACZ,EAAqB,CAAM;CAE3B,IAAM,IAAa,YAAiB,OAAO,QAAQ,KAAK,IAElD,IAAiB,KACnB,kBAAA,GAAA,EAAA,UAAA,CACK,KACG,kBAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAO;EAClB,SAAS;EACT,cAAY;EAEX,UAAA,KAAY,kBAAC,GAAD;GAAW,MAAM;GAAI,eAAY;EAAQ,CAAA;CAClD,CAAA,GAEX,KAAS,kBAAC,QAAD;EAAM,WAAW,EAAO;EAAQ,UAAA;CAAY,CAAA,CACxD,EAAA,CAAA;CAGN,OACI,kBAAC,UAAD;EACI,WAAW,EACP,EAAO,KACP,EAAO,IACP,KAAY,EAAO,UACnB,KAAU,EAAO,QACjB,KAAY,EAAO,UACnB,KAAY,EAAO,UACnB,CACJ;EACA,qBAAmB,MAAS,YAAY,YAAY,KAAA;EACpD,GAAI;EAXR,UAAA;GAaI,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU,UAAA;GAAoB,CAAA;GACpD,KAAS,QAAQ,MAAU,MACxB,kBAAC,MAAD;IAAI,WAAW,EAAO;IAAO,aAAU;IAClC,UAAA;GACD,CAAA;GAER,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU,UAAA;GAAa,CAAA;EAC1C;;AAEhB"}
@@ -1,3 +1,3 @@
1
1
  require("../../styles/component/AppBar.css");
2
- var e=`tempest_bar_SHqWS`,t=`tempest_centered_S0CCw`,n=`tempest_surface_fusKb`,r=`tempest_primary_QcCyN`,i=`tempest_transparent_uGrsu`,a=`tempest_bordered_5M9FZ`,o=`tempest_sticky_DStUO`,s=`tempest_safeArea_Vo3Bi`,c=`tempest_leading_mXv-d`,l=`tempest_actions_x7tMx`,u=`tempest_title_qaMMQ`,d=`tempest_brand_xKt54`,f=`tempest_iconBtn_S-BTg`,p={bar:e,centered:t,surface:n,primary:r,transparent:i,bordered:a,sticky:o,safeArea:s,leading:c,actions:l,title:u,brand:d,iconBtn:f};exports.actions=l,exports.bar=e,exports.bordered=a,exports.brand=d,exports.centered=t,exports.default=p,exports.iconBtn=f,exports.leading=c,exports.primary=r,exports.safeArea=s,exports.sticky=o,exports.surface=n,exports.title=u,exports.transparent=i;
2
+ var e=`tempest_bar_SHqWS`,t=`tempest_centered_S0CCw`,n=`tempest_surface_fusKb`,r=`tempest_primary_QcCyN`,i=`tempest_inverse_Icool`,a=`tempest_transparent_uGrsu`,o=`tempest_bordered_5M9FZ`,s=`tempest_sticky_DStUO`,c=`tempest_safeArea_Vo3Bi`,l=`tempest_leading_mXv-d`,u=`tempest_actions_x7tMx`,d=`tempest_title_qaMMQ`,f=`tempest_brand_xKt54`,p=`tempest_iconBtn_S-BTg`,m={bar:e,centered:t,surface:n,primary:r,inverse:i,transparent:a,bordered:o,sticky:s,safeArea:c,leading:l,actions:u,title:d,brand:f,iconBtn:p};exports.actions=u,exports.bar=e,exports.bordered=o,exports.brand=f,exports.centered=t,exports.default=m,exports.iconBtn=p,exports.inverse=i,exports.leading=l,exports.primary=r,exports.safeArea=c,exports.sticky=s,exports.surface=n,exports.title=d,exports.transparent=a;
3
3
  //# sourceMappingURL=AppBar.module.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppBar.module.cjs","names":[],"sources":["../../../src/components/AppBar/AppBar.module.css"],"sourcesContent":[".bar {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n min-height: 56px;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n font-family: var(--tempest-font-sans);\n}\n\n/* Symmetric side columns so the title is truly centered. */\n.bar.centered {\n grid-template-columns: 1fr auto 1fr;\n}\n\n.bar.surface {\n background: var(--tempest-surface);\n color: var(--tempest-text);\n}\n\n.bar.primary {\n background: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n}\n\n.bar.transparent {\n background: transparent;\n color: inherit;\n}\n\n.bar.bordered {\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.bar.sticky {\n position: sticky;\n top: 0;\n z-index: 30;\n}\n\n.bar.safeArea {\n padding-top: max(var(--tempest-space-3), env(safe-area-inset-top, 0px));\n}\n\n.leading {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n min-width: 0;\n}\n\n.actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--tempest-space-1);\n min-width: 0;\n}\n\n.title {\n margin: 0;\n min-width: 0;\n font-size: var(--tempest-text-xl);\n font-weight: var(--tempest-weight-bold);\n line-height: 1.2;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.bar.centered .title {\n text-align: center;\n}\n\n.brand {\n font-size: var(--tempest-text-lg);\n font-weight: var(--tempest-weight-bold);\n color: inherit;\n white-space: nowrap;\n}\n\n.iconBtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 36px;\n height: 36px;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-full);\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.iconBtn:hover {\n background: var(--tempest-surface-2);\n}\n\n.iconBtn:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"AppBar.module.cjs","names":[],"sources":["../../../src/components/AppBar/AppBar.module.css"],"sourcesContent":[".bar {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n min-height: 56px;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n font-family: var(--tempest-font-sans);\n}\n\n/* Symmetric side columns so the title is truly centered. */\n.bar.centered {\n grid-template-columns: 1fr auto 1fr;\n}\n\n.bar.surface {\n background: var(--tempest-surface);\n color: var(--tempest-text);\n}\n\n.bar.primary {\n background: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n}\n\n.bar.inverse {\n background: var(--tempest-bg);\n color: var(--tempest-text);\n}\n\n.bar.transparent {\n background: transparent;\n color: inherit;\n}\n\n.bar.bordered {\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.bar.sticky {\n position: sticky;\n top: 0;\n z-index: 30;\n}\n\n.bar.safeArea {\n padding-top: max(var(--tempest-space-3), env(safe-area-inset-top, 0px));\n}\n\n.leading {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n min-width: 0;\n}\n\n.actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--tempest-space-1);\n min-width: 0;\n}\n\n.title {\n margin: 0;\n min-width: 0;\n font-size: var(--tempest-text-xl);\n font-weight: var(--tempest-weight-bold);\n line-height: 1.2;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.bar.centered .title {\n text-align: center;\n}\n\n.brand {\n font-size: var(--tempest-text-lg);\n font-weight: var(--tempest-weight-bold);\n color: inherit;\n white-space: nowrap;\n}\n\n.iconBtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 36px;\n height: 36px;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-full);\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.iconBtn:hover {\n background: var(--tempest-surface-2);\n}\n\n.iconBtn:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n"],"mappings":";"}