tempest-react-sdk 0.74.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 (142) hide show
  1. package/README.md +1 -1
  2. package/dist/br/BrazilMap.module.cjs.map +1 -1
  3. package/dist/br/BrazilMap.module.js.map +1 -1
  4. package/dist/br/MunicipalitySearch.cjs +1 -1
  5. package/dist/br/MunicipalitySearch.cjs.map +1 -1
  6. package/dist/br/MunicipalitySearch.js +66 -48
  7. package/dist/br/MunicipalitySearch.js.map +1 -1
  8. package/dist/br/MunicipalitySearch.module.cjs +1 -1
  9. package/dist/br/MunicipalitySearch.module.cjs.map +1 -1
  10. package/dist/br/MunicipalitySearch.module.js +4 -3
  11. package/dist/br/MunicipalitySearch.module.js.map +1 -1
  12. package/dist/br.d.ts +12 -0
  13. package/dist/components/AIChat/AIChat.module.cjs.map +1 -1
  14. package/dist/components/AIChat/AIChat.module.js.map +1 -1
  15. package/dist/components/AppBar/AppBar.cjs +1 -1
  16. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  17. package/dist/components/AppBar/AppBar.js +1 -0
  18. package/dist/components/AppBar/AppBar.js.map +1 -1
  19. package/dist/components/AppBar/AppBar.module.cjs +1 -1
  20. package/dist/components/AppBar/AppBar.module.cjs.map +1 -1
  21. package/dist/components/AppBar/AppBar.module.js +12 -11
  22. package/dist/components/AppBar/AppBar.module.js.map +1 -1
  23. package/dist/components/Button/Button.module.cjs.map +1 -1
  24. package/dist/components/Button/Button.module.js.map +1 -1
  25. package/dist/components/Button/ButtonContent.cjs.map +1 -1
  26. package/dist/components/Button/ButtonContent.js.map +1 -1
  27. package/dist/components/Carousel/Carousel.module.cjs.map +1 -1
  28. package/dist/components/Carousel/Carousel.module.js.map +1 -1
  29. package/dist/components/Chat/Chat.module.cjs.map +1 -1
  30. package/dist/components/Chat/Chat.module.js.map +1 -1
  31. package/dist/components/Combobox/Combobox.cjs +1 -1
  32. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  33. package/dist/components/Combobox/Combobox.js +18 -23
  34. package/dist/components/Combobox/Combobox.js.map +1 -1
  35. package/dist/components/Combobox/use-listbox-dismiss.cjs +2 -0
  36. package/dist/components/Combobox/use-listbox-dismiss.cjs.map +1 -0
  37. package/dist/components/Combobox/use-listbox-dismiss.js +34 -0
  38. package/dist/components/Combobox/use-listbox-dismiss.js.map +1 -0
  39. package/dist/components/Dropzone/Dropzone.module.cjs.map +1 -1
  40. package/dist/components/Dropzone/Dropzone.module.js.map +1 -1
  41. package/dist/components/FilterBar/FilterBar.module.cjs.map +1 -1
  42. package/dist/components/FilterBar/FilterBar.module.js.map +1 -1
  43. package/dist/components/FloatingActionButton/FloatingActionButton.module.cjs.map +1 -1
  44. package/dist/components/FloatingActionButton/FloatingActionButton.module.js.map +1 -1
  45. package/dist/components/ImageCropper/ImageCropper.module.cjs.map +1 -1
  46. package/dist/components/ImageCropper/ImageCropper.module.js.map +1 -1
  47. package/dist/components/ListTile/ListTile.module.cjs.map +1 -1
  48. package/dist/components/ListTile/ListTile.module.js.map +1 -1
  49. package/dist/components/Markdown/Markdown.module.cjs.map +1 -1
  50. package/dist/components/Markdown/Markdown.module.js.map +1 -1
  51. package/dist/components/Menubar/Menubar.module.cjs.map +1 -1
  52. package/dist/components/Menubar/Menubar.module.js.map +1 -1
  53. package/dist/components/MultiSelect/MultiSelect.cjs +1 -1
  54. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  55. package/dist/components/MultiSelect/MultiSelect.js +55 -54
  56. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  57. package/dist/components/Navbar/Navbar.cjs +1 -1
  58. package/dist/components/Navbar/Navbar.cjs.map +1 -1
  59. package/dist/components/Navbar/Navbar.js +1 -0
  60. package/dist/components/Navbar/Navbar.js.map +1 -1
  61. package/dist/components/Navbar/Navbar.module.cjs +1 -1
  62. package/dist/components/Navbar/Navbar.module.cjs.map +1 -1
  63. package/dist/components/Navbar/Navbar.module.js +9 -8
  64. package/dist/components/Navbar/Navbar.module.js.map +1 -1
  65. package/dist/components/NavigationMenu/NavigationMenu.module.cjs.map +1 -1
  66. package/dist/components/NavigationMenu/NavigationMenu.module.js.map +1 -1
  67. package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
  68. package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
  69. package/dist/components/NotificationCenter/NotificationCenter.module.cjs.map +1 -1
  70. package/dist/components/NotificationCenter/NotificationCenter.module.js.map +1 -1
  71. package/dist/components/PasswordInput/PasswordInput.module.cjs.map +1 -1
  72. package/dist/components/PasswordInput/PasswordInput.module.js.map +1 -1
  73. package/dist/components/PinInput/PinInput.module.cjs.map +1 -1
  74. package/dist/components/PinInput/PinInput.module.js.map +1 -1
  75. package/dist/components/SectionHeader/SectionHeader.cjs +1 -1
  76. package/dist/components/SectionHeader/SectionHeader.cjs.map +1 -1
  77. package/dist/components/SectionHeader/SectionHeader.js +33 -25
  78. package/dist/components/SectionHeader/SectionHeader.js.map +1 -1
  79. package/dist/components/Tour/Tour.module.cjs.map +1 -1
  80. package/dist/components/Tour/Tour.module.js.map +1 -1
  81. package/dist/components/Transfer/Transfer.module.cjs.map +1 -1
  82. package/dist/components/Transfer/Transfer.module.js.map +1 -1
  83. package/dist/components/VirtualTable/VirtualTable.module.cjs.map +1 -1
  84. package/dist/components/VirtualTable/VirtualTable.module.js.map +1 -1
  85. package/dist/geo/TrajectoryMap.module.cjs.map +1 -1
  86. package/dist/geo/TrajectoryMap.module.js.map +1 -1
  87. package/dist/icons/IconPicker.module.cjs.map +1 -1
  88. package/dist/icons/IconPicker.module.js.map +1 -1
  89. package/dist/styles/actions.css +2 -2
  90. package/dist/styles/br.css +3 -3
  91. package/dist/styles/chat.css +5 -5
  92. package/dist/styles/component/AIChat.css +3 -3
  93. package/dist/styles/component/AppBar.css +1 -0
  94. package/dist/styles/component/BrazilMap.css +2 -2
  95. package/dist/styles/component/Button.css +1 -1
  96. package/dist/styles/component/Carousel.css +3 -3
  97. package/dist/styles/component/Chat.css +2 -2
  98. package/dist/styles/component/Dropzone.css +2 -1
  99. package/dist/styles/component/FilterBar.css +2 -2
  100. package/dist/styles/component/FloatingActionButton.css +1 -1
  101. package/dist/styles/component/IconPicker.css +1 -1
  102. package/dist/styles/component/ImageCropper.css +2 -2
  103. package/dist/styles/component/ListTile.css +1 -1
  104. package/dist/styles/component/Markdown.css +1 -1
  105. package/dist/styles/component/Menubar.css +1 -1
  106. package/dist/styles/component/MunicipalitySearch.css +1 -1
  107. package/dist/styles/component/Navbar.css +1 -0
  108. package/dist/styles/component/NavigationMenu.css +1 -1
  109. package/dist/styles/component/NavigationRail.css +1 -1
  110. package/dist/styles/component/NotificationCenter.css +3 -3
  111. package/dist/styles/component/PasswordInput.css +1 -1
  112. package/dist/styles/component/PinInput.css +1 -1
  113. package/dist/styles/component/Tour.css +2 -2
  114. package/dist/styles/component/TrajectoryMap.css +1 -1
  115. package/dist/styles/component/Transfer.css +2 -2
  116. package/dist/styles/component/VirtualTable.css +2 -2
  117. package/dist/styles/data.css +10 -10
  118. package/dist/styles/feedback.css +3 -3
  119. package/dist/styles/forms.css +6 -5
  120. package/dist/styles/geo.css +1 -1
  121. package/dist/styles/icons.css +1 -1
  122. package/dist/styles/inverse.css +225 -0
  123. package/dist/styles/navigation.css +8 -6
  124. package/dist/styles.css +1 -1
  125. package/dist/tempest-react-sdk.d.ts +130 -17
  126. package/dist/theme/create-theme.cjs +1 -1
  127. package/dist/theme/create-theme.cjs.map +1 -1
  128. package/dist/theme/create-theme.js +6 -1
  129. package/dist/theme/create-theme.js.map +1 -1
  130. package/dist/theme/inverse-render.cjs +1 -1
  131. package/dist/theme/inverse-render.cjs.map +1 -1
  132. package/dist/theme/inverse-render.js +1 -1
  133. package/dist/theme/inverse-render.js.map +1 -1
  134. package/dist/theme/inverse-restore.cjs +5 -0
  135. package/dist/theme/inverse-restore.cjs.map +1 -0
  136. package/dist/theme/inverse-restore.js +30 -0
  137. package/dist/theme/inverse-restore.js.map +1 -0
  138. package/dist/theme/inverse-surface.cjs +1 -1
  139. package/dist/theme/inverse-surface.cjs.map +1 -1
  140. package/dist/theme/inverse-surface.js +69 -72
  141. package/dist/theme/inverse-surface.js.map +1 -1
  142. package/package.json +1 -1
package/README.md CHANGED
@@ -249,7 +249,7 @@ applyTheme(createTheme(themePresets.violet)); // or start from a preset
249
249
 
250
250
  The ramp is derived in OKLCH (HSL lightness is not perceptual, which is what makes generated palettes look broken for some hues) and **anchored at step `500`**, so the color you pass is the color your buttons get. `--tempest-primary-foreground` and the text-on-soft step are picked by **measured** contrast, not convention — hardcoding white breaks a yellow brand. See [Theme › createTheme](https://mauriciobenjamin700.github.io/tempest-react-sdk/theme/).
251
251
 
252
- **A section in the brand color (opt-in).** `<Section tone="inverse">` (or `data-tempest-tone="inverse"` on any element) paints a hero, CTA band or footer in the brand and redefines every color token the components read inside it — text ≥ 7:1, muted and subtle text ≥ 4.5:1, focus ring ≥ 3:1, each measured against the fill and a lighter glow over it. Load `tempest-react-sdk/styles/inverse.css` for the SDK's blue, or pass `createTheme({ primary, inverse: createInverseSurface })` for yours; apps that do neither pay nothing. See [Theme › Inverse surface](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/theme/#inverse-surface-a-section-in-the-brand-color).
252
+ **A section in the brand color (opt-in).** `<Section tone="inverse">` (or `data-tempest-tone="inverse"` on any element) paints a hero, CTA band or footer in the brand and redefines every color token the components read inside it — text ≥ 7:1, muted and subtle text ≥ 4.5:1, focus ring ≥ 3:1, each measured against the fill and a lighter glow over it. Load `tempest-react-sdk/styles/inverse.css` for the SDK's blue, or pass `createTheme({ primary, inverse: createInverseSurface })` for yours; apps that do neither pay nothing. Inside the band, `data-tempest-tone="default"` (or `<Section tone="default">`) brings a region — a white `Card` in a navy hero — back to the page's tokens, light or dark. See [Theme › Inverse surface](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/theme/#inverse-surface-a-section-in-the-brand-color). `Navbar` and `AppBar` take `tone="inverse"` for a bar in the brand color whose buttons, links and focus ring invert with it; `tone="primary"` keeps painting only the background.
253
253
 
254
254
  **Optional layout layer.** CSS Modules cover the inside of each component; the layer _around_ them (page shell, two-column form, action row, card, a region that scrolls sideways instead of the page) is a second, opt-in stylesheet:
255
255
 
@@ -1 +1 @@
1
- {"version":3,"file":"BrazilMap.module.cjs","names":[],"sources":["../../src/br/BrazilMap.module.css"],"sourcesContent":[".map {\n position: relative;\n width: 100%;\n background: var(--tempest-surface-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.svg {\n display: block;\n width: 100%;\n}\n\n.loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.state {\n fill: var(--tempest-surface-3);\n stroke: var(--tempest-border-strong);\n stroke-width: 0.75;\n stroke-linejoin: round;\n transition:\n fill 0.12s ease,\n opacity 0.12s ease;\n}\n\n.state[role=\"button\"] {\n cursor: pointer;\n}\n\n.state[role=\"button\"]:hover {\n fill: var(--tempest-primary-soft);\n}\n\n.state[role=\"button\"]:focus-visible {\n outline: none;\n stroke: var(--tempest-primary);\n stroke-width: 1.5;\n}\n\n.selected {\n fill: var(--tempest-primary);\n stroke: var(--tempest-primary-active);\n stroke-width: 1.25;\n}\n\n.selected:hover {\n fill: var(--tempest-primary-hover);\n}\n\n.label {\n fill: var(--tempest-text);\n font-size: 10px;\n font-weight: var(--tempest-weight-medium);\n pointer-events: none;\n user-select: none;\n}\n\n.tooltip {\n position: absolute;\n z-index: 2;\n transform: translate(12px, 12px);\n max-width: 240px;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: var(--tempest-surface);\n color: var(--tempest-text);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n box-shadow: var(--tempest-shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-snug);\n pointer-events: none;\n white-space: nowrap;\n}\n\n.tooltipTitle {\n font-weight: var(--tempest-weight-semibold);\n font-size: var(--tempest-text-sm);\n}\n\n.tooltipMeta {\n color: var(--tempest-text-muted);\n}\n\n.marker {\n stroke: var(--tempest-surface);\n stroke-width: 2;\n}\n\n.marker[role=\"button\"] {\n cursor: pointer;\n}\n\n.marker[role=\"button\"]:hover {\n stroke: var(--tempest-primary-active);\n}\n\n.marker[role=\"button\"]:focus-visible {\n outline: none;\n stroke: var(--tempest-primary);\n stroke-width: 3;\n}\n\n.zoomReset {\n position: absolute;\n top: var(--tempest-space-2);\n right: var(--tempest-space-2);\n padding: var(--tempest-space-1) var(--tempest-space-2);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text);\n background: var(--tempest-surface);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-sm);\n cursor: pointer;\n}\n\n.zoomReset:hover {\n background: var(--tempest-surface-2);\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"BrazilMap.module.cjs","names":[],"sources":["../../src/br/BrazilMap.module.css"],"sourcesContent":[".map {\n position: relative;\n width: 100%;\n background: var(--tempest-surface-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.svg {\n display: block;\n width: 100%;\n}\n\n.loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.state {\n fill: var(--tempest-surface-3);\n stroke: var(--tempest-border-strong);\n stroke-width: 0.75;\n stroke-linejoin: round;\n transition:\n fill 0.12s ease,\n opacity 0.12s ease;\n}\n\n.state[role=\"button\"] {\n cursor: pointer;\n}\n\n.state[role=\"button\"]:hover {\n fill: var(--tempest-primary-soft);\n}\n\n.state[role=\"button\"]:focus-visible {\n outline: none;\n stroke: var(--tempest-focus-ring-color);\n stroke-width: 1.5;\n}\n\n.selected {\n fill: var(--tempest-primary);\n stroke: var(--tempest-primary-active);\n stroke-width: 1.25;\n}\n\n.selected:hover {\n fill: var(--tempest-primary-hover);\n}\n\n.label {\n fill: var(--tempest-text);\n font-size: 10px;\n font-weight: var(--tempest-weight-medium);\n pointer-events: none;\n user-select: none;\n}\n\n.tooltip {\n position: absolute;\n z-index: 2;\n transform: translate(12px, 12px);\n max-width: 240px;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: var(--tempest-surface);\n color: var(--tempest-text);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n box-shadow: var(--tempest-shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-snug);\n pointer-events: none;\n white-space: nowrap;\n}\n\n.tooltipTitle {\n font-weight: var(--tempest-weight-semibold);\n font-size: var(--tempest-text-sm);\n}\n\n.tooltipMeta {\n color: var(--tempest-text-muted);\n}\n\n.marker {\n stroke: var(--tempest-surface);\n stroke-width: 2;\n}\n\n.marker[role=\"button\"] {\n cursor: pointer;\n}\n\n.marker[role=\"button\"]:hover {\n stroke: var(--tempest-primary-active);\n}\n\n.marker[role=\"button\"]:focus-visible {\n outline: none;\n stroke: var(--tempest-focus-ring-color);\n stroke-width: 3;\n}\n\n.zoomReset {\n position: absolute;\n top: var(--tempest-space-2);\n right: var(--tempest-space-2);\n padding: var(--tempest-space-1) var(--tempest-space-2);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text);\n background: var(--tempest-surface);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-sm);\n cursor: pointer;\n}\n\n.zoomReset:hover {\n background: var(--tempest-surface-2);\n}\n"],"mappings":";"}
@@ -1 +1 @@
1
- {"version":3,"file":"BrazilMap.module.js","names":[],"sources":["../../src/br/BrazilMap.module.css"],"sourcesContent":[".map {\n position: relative;\n width: 100%;\n background: var(--tempest-surface-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.svg {\n display: block;\n width: 100%;\n}\n\n.loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.state {\n fill: var(--tempest-surface-3);\n stroke: var(--tempest-border-strong);\n stroke-width: 0.75;\n stroke-linejoin: round;\n transition:\n fill 0.12s ease,\n opacity 0.12s ease;\n}\n\n.state[role=\"button\"] {\n cursor: pointer;\n}\n\n.state[role=\"button\"]:hover {\n fill: var(--tempest-primary-soft);\n}\n\n.state[role=\"button\"]:focus-visible {\n outline: none;\n stroke: var(--tempest-primary);\n stroke-width: 1.5;\n}\n\n.selected {\n fill: var(--tempest-primary);\n stroke: var(--tempest-primary-active);\n stroke-width: 1.25;\n}\n\n.selected:hover {\n fill: var(--tempest-primary-hover);\n}\n\n.label {\n fill: var(--tempest-text);\n font-size: 10px;\n font-weight: var(--tempest-weight-medium);\n pointer-events: none;\n user-select: none;\n}\n\n.tooltip {\n position: absolute;\n z-index: 2;\n transform: translate(12px, 12px);\n max-width: 240px;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: var(--tempest-surface);\n color: var(--tempest-text);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n box-shadow: var(--tempest-shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-snug);\n pointer-events: none;\n white-space: nowrap;\n}\n\n.tooltipTitle {\n font-weight: var(--tempest-weight-semibold);\n font-size: var(--tempest-text-sm);\n}\n\n.tooltipMeta {\n color: var(--tempest-text-muted);\n}\n\n.marker {\n stroke: var(--tempest-surface);\n stroke-width: 2;\n}\n\n.marker[role=\"button\"] {\n cursor: pointer;\n}\n\n.marker[role=\"button\"]:hover {\n stroke: var(--tempest-primary-active);\n}\n\n.marker[role=\"button\"]:focus-visible {\n outline: none;\n stroke: var(--tempest-primary);\n stroke-width: 3;\n}\n\n.zoomReset {\n position: absolute;\n top: var(--tempest-space-2);\n right: var(--tempest-space-2);\n padding: var(--tempest-space-1) var(--tempest-space-2);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text);\n background: var(--tempest-surface);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-sm);\n cursor: pointer;\n}\n\n.zoomReset:hover {\n background: var(--tempest-surface-2);\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"BrazilMap.module.js","names":[],"sources":["../../src/br/BrazilMap.module.css"],"sourcesContent":[".map {\n position: relative;\n width: 100%;\n background: var(--tempest-surface-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.svg {\n display: block;\n width: 100%;\n}\n\n.loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.state {\n fill: var(--tempest-surface-3);\n stroke: var(--tempest-border-strong);\n stroke-width: 0.75;\n stroke-linejoin: round;\n transition:\n fill 0.12s ease,\n opacity 0.12s ease;\n}\n\n.state[role=\"button\"] {\n cursor: pointer;\n}\n\n.state[role=\"button\"]:hover {\n fill: var(--tempest-primary-soft);\n}\n\n.state[role=\"button\"]:focus-visible {\n outline: none;\n stroke: var(--tempest-focus-ring-color);\n stroke-width: 1.5;\n}\n\n.selected {\n fill: var(--tempest-primary);\n stroke: var(--tempest-primary-active);\n stroke-width: 1.25;\n}\n\n.selected:hover {\n fill: var(--tempest-primary-hover);\n}\n\n.label {\n fill: var(--tempest-text);\n font-size: 10px;\n font-weight: var(--tempest-weight-medium);\n pointer-events: none;\n user-select: none;\n}\n\n.tooltip {\n position: absolute;\n z-index: 2;\n transform: translate(12px, 12px);\n max-width: 240px;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: var(--tempest-surface);\n color: var(--tempest-text);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n box-shadow: var(--tempest-shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-snug);\n pointer-events: none;\n white-space: nowrap;\n}\n\n.tooltipTitle {\n font-weight: var(--tempest-weight-semibold);\n font-size: var(--tempest-text-sm);\n}\n\n.tooltipMeta {\n color: var(--tempest-text-muted);\n}\n\n.marker {\n stroke: var(--tempest-surface);\n stroke-width: 2;\n}\n\n.marker[role=\"button\"] {\n cursor: pointer;\n}\n\n.marker[role=\"button\"]:hover {\n stroke: var(--tempest-primary-active);\n}\n\n.marker[role=\"button\"]:focus-visible {\n outline: none;\n stroke: var(--tempest-focus-ring-color);\n stroke-width: 3;\n}\n\n.zoomReset {\n position: absolute;\n top: var(--tempest-space-2);\n right: var(--tempest-space-2);\n padding: var(--tempest-space-1) var(--tempest-space-2);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text);\n background: var(--tempest-surface);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-sm);\n cursor: pointer;\n}\n\n.zoomReset:hover {\n background: var(--tempest-surface-2);\n}\n"],"mappings":";"}
@@ -1,2 +1,2 @@
1
- const e=require("../components/Input/Input.cjs"),t=require("../hooks/use-debounce.cjs"),n=require("./geocode.cjs"),r=require("./MunicipalitySearch.module.cjs");let i=require("react"),a=require("react/jsx-runtime");function o({uf:o,onSelect:s,label:c,placeholder:l=`Buscar município…`,limit:u=8,debounceMs:d=250,disabled:f=!1}){let[p,m]=(0,i.useState)(``),[h,g]=(0,i.useState)([]),[_,v]=(0,i.useState)(!1),y=t.useDebounce(p,d),b=(0,i.useId)(),x=(0,i.useRef)(null);(0,i.useEffect)(()=>{let e=!0;if(!y.trim()){g([]);return}return n.searchMunicipalities(y,{uf:o,limit:u}).then(t=>{e&&g(t)}),()=>{e=!1}},[y,o,u]);function S(e){s(e),m(e.name),v(!1)}return(0,a.jsxs)(`div`,{className:r.default.search,children:[(0,a.jsx)(e.Input,{label:c,value:p,placeholder:l,disabled:f,role:`combobox`,"aria-expanded":_&&h.length>0,"aria-controls":b,autoComplete:`off`,onChange:e=>{m(e.target.value),v(!0)},onFocus:()=>v(!0),onBlur:()=>{x.current=setTimeout(()=>v(!1),120)}}),_&&h.length>0&&(0,a.jsx)(`ul`,{id:b,className:r.default.results,role:`listbox`,children:h.map(e=>(0,a.jsx)(`li`,{children:(0,a.jsxs)(`button`,{type:`button`,role:`option`,"aria-selected":!1,className:r.default.result,onMouseDown:e=>e.preventDefault(),onClick:()=>{x.current&&clearTimeout(x.current),S(e)},children:[(0,a.jsx)(`span`,{children:e.name}),(0,a.jsx)(`span`,{className:r.default.uf,children:e.uf})]})},e.id))})]})}exports.MunicipalitySearch=o;
1
+ const e=require("../utils/cn.cjs"),t=require("../components/Input/Input.cjs"),n=require("../hooks/use-debounce.cjs"),r=require("./geocode.cjs"),i=require("./MunicipalitySearch.module.cjs");let a=require("react"),o=require("react/jsx-runtime");function s(e){e.preventDefault()}function c({uf:c,onSelect:l,label:u,placeholder:d=`Buscar município…`,limit:f=8,debounceMs:p=250,disabled:m=!1}){let[h,g]=(0,a.useState)(``),[_,v]=(0,a.useState)([]),[y,b]=(0,a.useState)(!1),[x,S]=(0,a.useState)(0),C=n.useDebounce(h,p),w=(0,a.useId)(),T=y&&_.length>0,E=e=>`${w}-option-${e}`;(0,a.useEffect)(()=>{let e=!0;if(!C.trim()){v([]);return}return r.searchMunicipalities(C,{uf:c,limit:f}).then(t=>{e&&(v(t),S(0))}),()=>{e=!1}},[C,c,f]);function D(e){l(e),g(e.name),b(!1)}function O(e){if(e.key===`ArrowDown`){if(e.preventDefault(),!T){b(!0);return}S(e=>Math.min(_.length-1,e+1))}else if(e.key===`ArrowUp`)e.preventDefault(),S(e=>Math.max(0,e-1));else if(e.key===`Enter`){let t=T?_[x]:void 0;if(!t)return;e.preventDefault(),D(t)}else e.key===`Escape`&&(T&&e.preventDefault(),b(!1))}return(0,o.jsxs)(`div`,{className:i.default.search,children:[(0,o.jsx)(t.Input,{label:u,value:h,placeholder:d,disabled:m,role:`combobox`,"aria-expanded":T,"aria-controls":w,"aria-autocomplete":`list`,"aria-activedescendant":T?E(x):void 0,autoComplete:`off`,onChange:e=>{g(e.target.value),b(!0)},onFocus:()=>b(!0),onBlur:()=>b(!1),onKeyDown:O}),T&&(0,o.jsx)(`ul`,{id:w,className:i.default.results,role:`listbox`,onMouseDown:s,children:_.map((t,n)=>(0,o.jsxs)(`li`,{id:E(n),role:`option`,"aria-selected":n===x,className:e.cn(i.default.result,n===x&&i.default.active),onMouseEnter:()=>S(n),onClick:()=>D(t),children:[(0,o.jsx)(`span`,{children:t.name}),(0,o.jsx)(`span`,{className:i.default.uf,children:t.uf})]},t.id))})]})}exports.MunicipalitySearch=c;
2
2
  //# sourceMappingURL=MunicipalitySearch.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"MunicipalitySearch.cjs","names":[],"sources":["../../src/br/MunicipalitySearch.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type ReactElement } from \"react\";\nimport { Input } from \"@/components/Input\";\nimport { useDebounce } from \"@/hooks\";\nimport { searchMunicipalities, type MunicipalityCentroid } from \"./geocode\";\nimport type { UF } from \"./locations\";\nimport styles from \"./MunicipalitySearch.module.css\";\n\nexport interface MunicipalitySearchProps {\n /** Restrict the search to a single UF. */\n uf?: UF;\n /** Fired when a municipality is picked from the results. */\n onSelect: (municipality: MunicipalityCentroid) => void;\n /** Field label. */\n label?: string;\n /** Input placeholder. Default: `\"Buscar município…\"`. */\n placeholder?: string;\n /** Max results shown. Default: `8`. */\n limit?: number;\n /** Debounce in ms before searching. Default: `250`. */\n debounceMs?: number;\n /** Disable the field. */\n disabled?: boolean;\n}\n\n/**\n * Debounced municipality autocomplete backed by the offline\n * {@link searchMunicipalities} index — no network. Type a partial name, pick a\n * result, and `onSelect` fires with its `{ id, name, uf, latitude, longitude }`.\n * Wire `onSelect` to a `BrazilStateMap` `selected` to highlight it on the map.\n *\n * @example\n * const [uf, setUf] = useState<UF>(\"SP\");\n * const [city, setCity] = useState<string | null>(null);\n * <MunicipalitySearch uf={uf} onSelect={(m) => setCity(m.name)} />\n * <BrazilStateMap uf={uf} selected={city} />\n */\nexport function MunicipalitySearch({\n uf,\n onSelect,\n label,\n placeholder = \"Buscar município…\",\n limit = 8,\n debounceMs = 250,\n disabled = false,\n}: MunicipalitySearchProps): ReactElement {\n const [query, setQuery] = useState<string>(\"\");\n const [results, setResults] = useState<MunicipalityCentroid[]>([]);\n const [open, setOpen] = useState<boolean>(false);\n const debounced = useDebounce(query, debounceMs);\n const listId = useId();\n const blurTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n let active = true;\n if (!debounced.trim()) {\n setResults([]);\n return;\n }\n void searchMunicipalities(debounced, { uf, limit }).then((hits) => {\n if (active) setResults(hits);\n });\n return () => {\n active = false;\n };\n }, [debounced, uf, limit]);\n\n function choose(municipality: MunicipalityCentroid): void {\n onSelect(municipality);\n setQuery(municipality.name);\n setOpen(false);\n }\n\n return (\n <div className={styles.search}>\n <Input\n label={label}\n value={query}\n placeholder={placeholder}\n disabled={disabled}\n role=\"combobox\"\n aria-expanded={open && results.length > 0}\n aria-controls={listId}\n autoComplete=\"off\"\n onChange={(e) => {\n setQuery(e.target.value);\n setOpen(true);\n }}\n onFocus={() => setOpen(true)}\n onBlur={() => {\n // Delay so a result click registers before the list closes.\n blurTimer.current = setTimeout(() => setOpen(false), 120);\n }}\n />\n {open && results.length > 0 && (\n <ul id={listId} className={styles.results} role=\"listbox\">\n {results.map((m) => (\n <li key={m.id}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n className={styles.result}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n if (blurTimer.current) clearTimeout(blurTimer.current);\n choose(m);\n }}\n >\n <span>{m.name}</span>\n <span className={styles.uf}>{m.uf}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"sNAoCA,SAAgB,EAAmB,CAC/B,KACA,WACA,QACA,cAAc,oBACd,QAAQ,EACR,aAAa,IACb,WAAW,IAC2B,CACtC,GAAM,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAiB,EAAE,EACvC,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAiC,CAAC,CAAC,EAC3D,CAAC,EAAM,IAAA,EAAW,EAAA,SAAA,CAAkB,EAAK,EACzC,EAAY,EAAA,YAAY,EAAO,CAAU,EACzC,GAAA,EAAS,EAAA,MAAA,CAAM,EACf,GAAA,EAAY,EAAA,OAAA,CAA6C,IAAI,GAEnE,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAS,GACb,GAAI,CAAC,EAAU,KAAK,EAAG,CACnB,EAAW,CAAC,CAAC,EACb,MACJ,CAIA,OAHA,EAAK,qBAAqB,EAAW,CAAE,KAAI,OAAM,CAAC,CAAC,CAAC,KAAM,GAAS,CAC3D,GAAQ,EAAW,CAAI,CAC/B,CAAC,MACY,CACT,EAAS,EACb,CACJ,EAAG,CAAC,EAAW,EAAI,CAAK,CAAC,EAEzB,SAAS,EAAO,EAA0C,CACtD,EAAS,CAAY,EACrB,EAAS,EAAa,IAAI,EAC1B,EAAQ,EAAK,CACjB,CAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CACW,QACP,MAAO,EACM,cACH,WACV,KAAK,WACL,gBAAe,GAAQ,EAAQ,OAAS,EACxC,gBAAe,EACf,aAAa,MACb,SAAW,GAAM,CACb,EAAS,EAAE,OAAO,KAAK,EACvB,EAAQ,EAAI,CAChB,EACA,YAAe,EAAQ,EAAI,EAC3B,WAAc,CAEV,EAAU,QAAU,eAAiB,EAAQ,EAAK,EAAG,GAAG,CAC5D,CACH,CAAA,EACA,GAAQ,EAAQ,OAAS,IACtB,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,GAAI,EAAQ,UAAW,EAAA,QAAO,QAAS,KAAK,UAC3C,SAAA,EAAQ,IAAK,IACV,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,UACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,KAAK,SACL,KAAK,SACL,gBAAe,GACf,UAAW,EAAA,QAAO,OAClB,YAAc,GAAM,EAAE,eAAe,EACrC,YAAe,CACP,EAAU,SAAS,aAAa,EAAU,OAAO,EACrD,EAAO,CAAC,CACZ,EATJ,SAAA,EAWI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAE,IAAW,CAAA,GACpB,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,GAAK,SAAA,EAAE,EAAS,CAAA,CACpC,CACR,CAAA,CAAA,EAfK,EAAE,EAeP,CACP,CACD,CAAA,CAEP,GAEb"}
1
+ {"version":3,"file":"MunicipalitySearch.cjs","names":[],"sources":["../../src/br/MunicipalitySearch.tsx"],"sourcesContent":["import {\n useEffect,\n useId,\n useState,\n type KeyboardEvent,\n type MouseEvent,\n type ReactElement,\n} from \"react\";\nimport { Input } from \"@/components/Input\";\nimport { useDebounce } from \"@/hooks\";\nimport { cn } from \"@/utils/cn\";\nimport { searchMunicipalities, type MunicipalityCentroid } from \"./geocode\";\nimport type { UF } from \"./locations\";\nimport styles from \"./MunicipalitySearch.module.css\";\n\nexport interface MunicipalitySearchProps {\n /** Restrict the search to a single UF. */\n uf?: UF;\n /** Fired when a municipality is picked from the results. */\n onSelect: (municipality: MunicipalityCentroid) => void;\n /** Field label. */\n label?: string;\n /** Input placeholder. Default: `\"Buscar município…\"`. */\n placeholder?: string;\n /** Max results shown. Default: `8`. */\n limit?: number;\n /** Debounce in ms before searching. Default: `250`. */\n debounceMs?: number;\n /** Disable the field. */\n disabled?: boolean;\n}\n\n/**\n * Keeps focus on the input when a press lands anywhere on the list — an option,\n * the padding or the scrollbar — so the input never blurs and the list closes\n * only when focus really leaves the field.\n *\n * @param event - The `mousedown` on the list.\n */\nfunction keepInputFocus(event: MouseEvent<HTMLUListElement>): void {\n event.preventDefault();\n}\n\n/**\n * Debounced municipality autocomplete backed by the offline\n * {@link searchMunicipalities} index — no network. Type a partial name, pick a\n * result, and `onSelect` fires with its `{ id, name, uf, latitude, longitude }`.\n * Wire `onSelect` to a `BrazilStateMap` `selected` to highlight it on the map.\n *\n * Keyboard follows the SDK's `Combobox` (the WAI-ARIA combobox pattern): focus\n * stays on the input, `ArrowDown`/`ArrowUp` move the active result — announced\n * through `aria-activedescendant` — `Enter` picks it, `Escape` closes the list\n * and `Tab` leaves the field, closing the list on the way out.\n *\n * The list used to close on a 120 ms timer after the input blurred, with each\n * result a focusable `<button>`. Measured in Chromium (gallery build, issue\n * #426): `Tab` moved focus to the first result, the timer then unmounted it and\n * focus fell to `<body>`; arrows, `Enter` and `Escape` did nothing. Results are\n * now plain options outside the tab order, and a press on the list cancels the\n * blur instead of racing it.\n *\n * @example\n * const [uf, setUf] = useState<UF>(\"SP\");\n * const [city, setCity] = useState<string | null>(null);\n * <MunicipalitySearch uf={uf} onSelect={(m) => setCity(m.name)} />\n * <BrazilStateMap uf={uf} selected={city} />\n */\nexport function MunicipalitySearch({\n uf,\n onSelect,\n label,\n placeholder = \"Buscar município…\",\n limit = 8,\n debounceMs = 250,\n disabled = false,\n}: MunicipalitySearchProps): ReactElement {\n const [query, setQuery] = useState<string>(\"\");\n const [results, setResults] = useState<MunicipalityCentroid[]>([]);\n const [open, setOpen] = useState<boolean>(false);\n const [activeIndex, setActiveIndex] = useState<number>(0);\n const debounced = useDebounce(query, debounceMs);\n const listId = useId();\n const expanded = open && results.length > 0;\n const optionId = (index: number): string => `${listId}-option-${index}`;\n\n useEffect(() => {\n let active = true;\n if (!debounced.trim()) {\n setResults([]);\n return;\n }\n void searchMunicipalities(debounced, { uf, limit }).then((hits) => {\n if (!active) return;\n setResults(hits);\n setActiveIndex(0);\n });\n return () => {\n active = false;\n };\n }, [debounced, uf, limit]);\n\n function choose(municipality: MunicipalityCentroid): void {\n onSelect(municipality);\n setQuery(municipality.name);\n setOpen(false);\n }\n\n /**\n * Keyboard model of the input, the same as `Combobox`'s.\n *\n * `Escape` calls `preventDefault()` only while the list is open, so an\n * enclosing `Modal` or `Drawer` stays open and the next `Escape` reaches it.\n * `Enter` is consumed only when it picks a result, so with the list closed\n * it still submits an enclosing form.\n *\n * @param event - The `keydown` on the input.\n */\n function handleKeyDown(event: KeyboardEvent<HTMLInputElement>): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n if (!expanded) {\n setOpen(true);\n return;\n }\n setActiveIndex((current) => Math.min(results.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n const municipality = expanded ? results[activeIndex] : undefined;\n if (!municipality) return;\n event.preventDefault();\n choose(municipality);\n } else if (event.key === \"Escape\") {\n if (expanded) event.preventDefault();\n setOpen(false);\n }\n }\n\n return (\n <div className={styles.search}>\n <Input\n label={label}\n value={query}\n placeholder={placeholder}\n disabled={disabled}\n role=\"combobox\"\n aria-expanded={expanded}\n aria-controls={listId}\n aria-autocomplete=\"list\"\n aria-activedescendant={expanded ? optionId(activeIndex) : undefined}\n autoComplete=\"off\"\n onChange={(e) => {\n setQuery(e.target.value);\n setOpen(true);\n }}\n onFocus={() => setOpen(true)}\n onBlur={() => setOpen(false)}\n onKeyDown={handleKeyDown}\n />\n {expanded && (\n <ul\n id={listId}\n className={styles.results}\n role=\"listbox\"\n onMouseDown={keepInputFocus}\n >\n {results.map((m, index) => (\n <li\n key={m.id}\n id={optionId(index)}\n role=\"option\"\n aria-selected={index === activeIndex}\n className={cn(styles.result, index === activeIndex && styles.active)}\n onMouseEnter={() => setActiveIndex(index)}\n onClick={() => choose(m)}\n >\n <span>{m.name}</span>\n <span className={styles.uf}>{m.uf}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"mPAuCA,SAAS,EAAe,EAA2C,CAC/D,EAAM,eAAe,CACzB,CA0BA,SAAgB,EAAmB,CAC/B,KACA,WACA,QACA,cAAc,oBACd,QAAQ,EACR,aAAa,IACb,WAAW,IAC2B,CACtC,GAAM,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAiB,EAAE,EACvC,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAiC,CAAC,CAAC,EAC3D,CAAC,EAAM,IAAA,EAAW,EAAA,SAAA,CAAkB,EAAK,EACzC,CAAC,EAAa,IAAA,EAAkB,EAAA,SAAA,CAAiB,CAAC,EAClD,EAAY,EAAA,YAAY,EAAO,CAAU,EACzC,GAAA,EAAS,EAAA,MAAA,CAAM,EACf,EAAW,GAAQ,EAAQ,OAAS,EACpC,EAAY,GAA0B,GAAG,EAAO,UAAU,KAEhE,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAS,GACb,GAAI,CAAC,EAAU,KAAK,EAAG,CACnB,EAAW,CAAC,CAAC,EACb,MACJ,CAMA,OALA,EAAK,qBAAqB,EAAW,CAAE,KAAI,OAAM,CAAC,CAAC,CAAC,KAAM,GAAS,CAC1D,IACL,EAAW,CAAI,EACf,EAAe,CAAC,EACpB,CAAC,MACY,CACT,EAAS,EACb,CACJ,EAAG,CAAC,EAAW,EAAI,CAAK,CAAC,EAEzB,SAAS,EAAO,EAA0C,CACtD,EAAS,CAAY,EACrB,EAAS,EAAa,IAAI,EAC1B,EAAQ,EAAK,CACjB,CAYA,SAAS,EAAc,EAA8C,CACjE,GAAI,EAAM,MAAQ,YAAa,CAE3B,GADA,EAAM,eAAe,EACjB,CAAC,EAAU,CACX,EAAQ,EAAI,EACZ,MACJ,CACA,EAAgB,GAAY,KAAK,IAAI,EAAQ,OAAS,EAAG,EAAU,CAAC,CAAC,CACzE,MAAO,GAAI,EAAM,MAAQ,UACrB,EAAM,eAAe,EACrB,EAAgB,GAAY,KAAK,IAAI,EAAG,EAAU,CAAC,CAAC,OACjD,GAAI,EAAM,MAAQ,QAAS,CAC9B,IAAM,EAAe,EAAW,EAAQ,GAAe,IAAA,GACvD,GAAI,CAAC,EAAc,OACnB,EAAM,eAAe,EACrB,EAAO,CAAY,CACvB,MAAW,EAAM,MAAQ,WACjB,GAAU,EAAM,eAAe,EACnC,EAAQ,EAAK,EAErB,CAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CACW,QACP,MAAO,EACM,cACH,WACV,KAAK,WACL,gBAAe,EACf,gBAAe,EACf,oBAAkB,OAClB,wBAAuB,EAAW,EAAS,CAAW,EAAI,IAAA,GAC1D,aAAa,MACb,SAAW,GAAM,CACb,EAAS,EAAE,OAAO,KAAK,EACvB,EAAQ,EAAI,CAChB,EACA,YAAe,EAAQ,EAAI,EAC3B,WAAc,EAAQ,EAAK,EAC3B,UAAW,CACd,CAAA,EACA,IACG,EAAA,EAAA,IAAA,CAAC,KAAD,CACI,GAAI,EACJ,UAAW,EAAA,QAAO,QAClB,KAAK,UACL,YAAa,EAEZ,SAAA,EAAQ,KAAK,EAAG,KACb,EAAA,EAAA,KAAA,CAAC,KAAD,CAEI,GAAI,EAAS,CAAK,EAClB,KAAK,SACL,gBAAe,IAAU,EACzB,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,IAAU,GAAe,EAAA,QAAO,MAAM,EACnE,iBAAoB,EAAe,CAAK,EACxC,YAAe,EAAO,CAAC,EAP3B,SAAA,EASI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAE,IAAW,CAAA,GACpB,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,GAAK,SAAA,EAAE,EAAS,CAAA,CACxC,CAVK,EAAA,EAAE,EAUP,CACP,CACD,CAAA,CAEP,GAEb"}
@@ -1,74 +1,92 @@
1
- import { Input as e } from "../components/Input/Input.js";
2
- import { useDebounce as t } from "../hooks/use-debounce.js";
3
- import { searchMunicipalities as n } from "./geocode.js";
4
- import r from "./MunicipalitySearch.module.js";
5
- import { useEffect as i, useId as a, useRef as o, useState as s } from "react";
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { Input as t } from "../components/Input/Input.js";
3
+ import { useDebounce as n } from "../hooks/use-debounce.js";
4
+ import { searchMunicipalities as r } from "./geocode.js";
5
+ import i from "./MunicipalitySearch.module.js";
6
+ import { useEffect as a, useId as o, useState as s } from "react";
6
7
  import { jsx as c, jsxs as l } from "react/jsx-runtime";
7
8
  //#region src/br/MunicipalitySearch.tsx
8
- function u({ uf: u, onSelect: d, label: f, placeholder: p = "Buscar município…", limit: m = 8, debounceMs: h = 250, disabled: g = !1 }) {
9
- let [_, v] = s(""), [y, b] = s([]), [x, S] = s(!1), C = t(_, h), w = a(), T = o(null);
10
- i(() => {
9
+ function u(e) {
10
+ e.preventDefault();
11
+ }
12
+ function d({ uf: d, onSelect: f, label: p, placeholder: m = "Buscar município…", limit: h = 8, debounceMs: g = 250, disabled: _ = !1 }) {
13
+ let [v, y] = s(""), [b, x] = s([]), [S, C] = s(!1), [w, T] = s(0), E = n(v, g), D = o(), O = S && b.length > 0, k = (e) => `${D}-option-${e}`;
14
+ a(() => {
11
15
  let e = !0;
12
- if (!C.trim()) {
13
- b([]);
16
+ if (!E.trim()) {
17
+ x([]);
14
18
  return;
15
19
  }
16
- return n(C, {
17
- uf: u,
18
- limit: m
20
+ return r(E, {
21
+ uf: d,
22
+ limit: h
19
23
  }).then((t) => {
20
- e && b(t);
24
+ e && (x(t), T(0));
21
25
  }), () => {
22
26
  e = !1;
23
27
  };
24
28
  }, [
25
- C,
26
- u,
27
- m
29
+ E,
30
+ d,
31
+ h
28
32
  ]);
29
- function E(e) {
30
- d(e), v(e.name), S(!1);
33
+ function A(e) {
34
+ f(e), y(e.name), C(!1);
35
+ }
36
+ function j(e) {
37
+ if (e.key === "ArrowDown") {
38
+ if (e.preventDefault(), !O) {
39
+ C(!0);
40
+ return;
41
+ }
42
+ T((e) => Math.min(b.length - 1, e + 1));
43
+ } else if (e.key === "ArrowUp") e.preventDefault(), T((e) => Math.max(0, e - 1));
44
+ else if (e.key === "Enter") {
45
+ let t = O ? b[w] : void 0;
46
+ if (!t) return;
47
+ e.preventDefault(), A(t);
48
+ } else e.key === "Escape" && (O && e.preventDefault(), C(!1));
31
49
  }
32
50
  return /* @__PURE__ */ l("div", {
33
- className: r.search,
34
- children: [/* @__PURE__ */ c(e, {
35
- label: f,
36
- value: _,
37
- placeholder: p,
38
- disabled: g,
51
+ className: i.search,
52
+ children: [/* @__PURE__ */ c(t, {
53
+ label: p,
54
+ value: v,
55
+ placeholder: m,
56
+ disabled: _,
39
57
  role: "combobox",
40
- "aria-expanded": x && y.length > 0,
41
- "aria-controls": w,
58
+ "aria-expanded": O,
59
+ "aria-controls": D,
60
+ "aria-autocomplete": "list",
61
+ "aria-activedescendant": O ? k(w) : void 0,
42
62
  autoComplete: "off",
43
63
  onChange: (e) => {
44
- v(e.target.value), S(!0);
64
+ y(e.target.value), C(!0);
45
65
  },
46
- onFocus: () => S(!0),
47
- onBlur: () => {
48
- T.current = setTimeout(() => S(!1), 120);
49
- }
50
- }), x && y.length > 0 && /* @__PURE__ */ c("ul", {
51
- id: w,
52
- className: r.results,
66
+ onFocus: () => C(!0),
67
+ onBlur: () => C(!1),
68
+ onKeyDown: j
69
+ }), O && /* @__PURE__ */ c("ul", {
70
+ id: D,
71
+ className: i.results,
53
72
  role: "listbox",
54
- children: y.map((e) => /* @__PURE__ */ c("li", { children: /* @__PURE__ */ l("button", {
55
- type: "button",
73
+ onMouseDown: u,
74
+ children: b.map((t, n) => /* @__PURE__ */ l("li", {
75
+ id: k(n),
56
76
  role: "option",
57
- "aria-selected": !1,
58
- className: r.result,
59
- onMouseDown: (e) => e.preventDefault(),
60
- onClick: () => {
61
- T.current && clearTimeout(T.current), E(e);
62
- },
63
- children: [/* @__PURE__ */ c("span", { children: e.name }), /* @__PURE__ */ c("span", {
64
- className: r.uf,
65
- children: e.uf
77
+ "aria-selected": n === w,
78
+ className: e(i.result, n === w && i.active),
79
+ onMouseEnter: () => T(n),
80
+ onClick: () => A(t),
81
+ children: [/* @__PURE__ */ c("span", { children: t.name }), /* @__PURE__ */ c("span", {
82
+ className: i.uf,
83
+ children: t.uf
66
84
  })]
67
- }) }, e.id))
85
+ }, t.id))
68
86
  })]
69
87
  });
70
88
  }
71
89
  //#endregion
72
- export { u as MunicipalitySearch };
90
+ export { d as MunicipalitySearch };
73
91
 
74
92
  //# sourceMappingURL=MunicipalitySearch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MunicipalitySearch.js","names":[],"sources":["../../src/br/MunicipalitySearch.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type ReactElement } from \"react\";\nimport { Input } from \"@/components/Input\";\nimport { useDebounce } from \"@/hooks\";\nimport { searchMunicipalities, type MunicipalityCentroid } from \"./geocode\";\nimport type { UF } from \"./locations\";\nimport styles from \"./MunicipalitySearch.module.css\";\n\nexport interface MunicipalitySearchProps {\n /** Restrict the search to a single UF. */\n uf?: UF;\n /** Fired when a municipality is picked from the results. */\n onSelect: (municipality: MunicipalityCentroid) => void;\n /** Field label. */\n label?: string;\n /** Input placeholder. Default: `\"Buscar município…\"`. */\n placeholder?: string;\n /** Max results shown. Default: `8`. */\n limit?: number;\n /** Debounce in ms before searching. Default: `250`. */\n debounceMs?: number;\n /** Disable the field. */\n disabled?: boolean;\n}\n\n/**\n * Debounced municipality autocomplete backed by the offline\n * {@link searchMunicipalities} index — no network. Type a partial name, pick a\n * result, and `onSelect` fires with its `{ id, name, uf, latitude, longitude }`.\n * Wire `onSelect` to a `BrazilStateMap` `selected` to highlight it on the map.\n *\n * @example\n * const [uf, setUf] = useState<UF>(\"SP\");\n * const [city, setCity] = useState<string | null>(null);\n * <MunicipalitySearch uf={uf} onSelect={(m) => setCity(m.name)} />\n * <BrazilStateMap uf={uf} selected={city} />\n */\nexport function MunicipalitySearch({\n uf,\n onSelect,\n label,\n placeholder = \"Buscar município…\",\n limit = 8,\n debounceMs = 250,\n disabled = false,\n}: MunicipalitySearchProps): ReactElement {\n const [query, setQuery] = useState<string>(\"\");\n const [results, setResults] = useState<MunicipalityCentroid[]>([]);\n const [open, setOpen] = useState<boolean>(false);\n const debounced = useDebounce(query, debounceMs);\n const listId = useId();\n const blurTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n let active = true;\n if (!debounced.trim()) {\n setResults([]);\n return;\n }\n void searchMunicipalities(debounced, { uf, limit }).then((hits) => {\n if (active) setResults(hits);\n });\n return () => {\n active = false;\n };\n }, [debounced, uf, limit]);\n\n function choose(municipality: MunicipalityCentroid): void {\n onSelect(municipality);\n setQuery(municipality.name);\n setOpen(false);\n }\n\n return (\n <div className={styles.search}>\n <Input\n label={label}\n value={query}\n placeholder={placeholder}\n disabled={disabled}\n role=\"combobox\"\n aria-expanded={open && results.length > 0}\n aria-controls={listId}\n autoComplete=\"off\"\n onChange={(e) => {\n setQuery(e.target.value);\n setOpen(true);\n }}\n onFocus={() => setOpen(true)}\n onBlur={() => {\n // Delay so a result click registers before the list closes.\n blurTimer.current = setTimeout(() => setOpen(false), 120);\n }}\n />\n {open && results.length > 0 && (\n <ul id={listId} className={styles.results} role=\"listbox\">\n {results.map((m) => (\n <li key={m.id}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n className={styles.result}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n if (blurTimer.current) clearTimeout(blurTimer.current);\n choose(m);\n }}\n >\n <span>{m.name}</span>\n <span className={styles.uf}>{m.uf}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAoCA,SAAgB,EAAmB,EAC/B,OACA,aACA,UACA,iBAAc,qBACd,WAAQ,GACR,gBAAa,KACb,cAAW,MAC2B;CACtC,IAAM,CAAC,GAAO,KAAY,EAAiB,EAAE,GACvC,CAAC,GAAS,KAAc,EAAiC,CAAC,CAAC,GAC3D,CAAC,GAAM,KAAW,EAAkB,EAAK,GACzC,IAAY,EAAY,GAAO,CAAU,GACzC,IAAS,EAAM,GACf,IAAY,EAA6C,IAAI;CAEnE,QAAgB;EACZ,IAAI,IAAS;EACb,IAAI,CAAC,EAAU,KAAK,GAAG;GACnB,EAAW,CAAC,CAAC;GACb;EACJ;EAIA,OAHA,EAA0B,GAAW;GAAE;GAAI;EAAM,CAAC,CAAC,CAAC,MAAM,MAAS;GAC/D,AAAI,KAAQ,EAAW,CAAI;EAC/B,CAAC,SACY;GACT,IAAS;EACb;CACJ,GAAG;EAAC;EAAW;EAAI;CAAK,CAAC;CAEzB,SAAS,EAAO,GAA0C;EAGtD,AAFA,EAAS,CAAY,GACrB,EAAS,EAAa,IAAI,GAC1B,EAAQ,EAAK;CACjB;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAO;EAAvB,UAAA,CACI,kBAAC,GAAD;GACW;GACP,OAAO;GACM;GACH;GACV,MAAK;GACL,iBAAe,KAAQ,EAAQ,SAAS;GACxC,iBAAe;GACf,cAAa;GACb,WAAW,MAAM;IAEb,AADA,EAAS,EAAE,OAAO,KAAK,GACvB,EAAQ,EAAI;GAChB;GACA,eAAe,EAAQ,EAAI;GAC3B,cAAc;IAEV,EAAU,UAAU,iBAAiB,EAAQ,EAAK,GAAG,GAAG;GAC5D;EACH,CAAA,GACA,KAAQ,EAAQ,SAAS,KACtB,kBAAC,MAAD;GAAI,IAAI;GAAQ,WAAW,EAAO;GAAS,MAAK;GAC3C,UAAA,EAAQ,KAAK,MACV,kBAAC,MAAD,EAAA,UACI,kBAAC,UAAD;IACI,MAAK;IACL,MAAK;IACL,iBAAe;IACf,WAAW,EAAO;IAClB,cAAc,MAAM,EAAE,eAAe;IACrC,eAAe;KAEX,AADI,EAAU,WAAS,aAAa,EAAU,OAAO,GACrD,EAAO,CAAC;IACZ;IATJ,UAAA,CAWI,kBAAC,QAAD,EAAA,UAAO,EAAE,KAAW,CAAA,GACpB,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAK,UAAA,EAAE;IAAS,CAAA,CACpC;GACR,CAAA,EAAA,GAfK,EAAE,EAeP,CACP;EACD,CAAA,CAEP;;AAEb"}
1
+ {"version":3,"file":"MunicipalitySearch.js","names":[],"sources":["../../src/br/MunicipalitySearch.tsx"],"sourcesContent":["import {\n useEffect,\n useId,\n useState,\n type KeyboardEvent,\n type MouseEvent,\n type ReactElement,\n} from \"react\";\nimport { Input } from \"@/components/Input\";\nimport { useDebounce } from \"@/hooks\";\nimport { cn } from \"@/utils/cn\";\nimport { searchMunicipalities, type MunicipalityCentroid } from \"./geocode\";\nimport type { UF } from \"./locations\";\nimport styles from \"./MunicipalitySearch.module.css\";\n\nexport interface MunicipalitySearchProps {\n /** Restrict the search to a single UF. */\n uf?: UF;\n /** Fired when a municipality is picked from the results. */\n onSelect: (municipality: MunicipalityCentroid) => void;\n /** Field label. */\n label?: string;\n /** Input placeholder. Default: `\"Buscar município…\"`. */\n placeholder?: string;\n /** Max results shown. Default: `8`. */\n limit?: number;\n /** Debounce in ms before searching. Default: `250`. */\n debounceMs?: number;\n /** Disable the field. */\n disabled?: boolean;\n}\n\n/**\n * Keeps focus on the input when a press lands anywhere on the list — an option,\n * the padding or the scrollbar — so the input never blurs and the list closes\n * only when focus really leaves the field.\n *\n * @param event - The `mousedown` on the list.\n */\nfunction keepInputFocus(event: MouseEvent<HTMLUListElement>): void {\n event.preventDefault();\n}\n\n/**\n * Debounced municipality autocomplete backed by the offline\n * {@link searchMunicipalities} index — no network. Type a partial name, pick a\n * result, and `onSelect` fires with its `{ id, name, uf, latitude, longitude }`.\n * Wire `onSelect` to a `BrazilStateMap` `selected` to highlight it on the map.\n *\n * Keyboard follows the SDK's `Combobox` (the WAI-ARIA combobox pattern): focus\n * stays on the input, `ArrowDown`/`ArrowUp` move the active result — announced\n * through `aria-activedescendant` — `Enter` picks it, `Escape` closes the list\n * and `Tab` leaves the field, closing the list on the way out.\n *\n * The list used to close on a 120 ms timer after the input blurred, with each\n * result a focusable `<button>`. Measured in Chromium (gallery build, issue\n * #426): `Tab` moved focus to the first result, the timer then unmounted it and\n * focus fell to `<body>`; arrows, `Enter` and `Escape` did nothing. Results are\n * now plain options outside the tab order, and a press on the list cancels the\n * blur instead of racing it.\n *\n * @example\n * const [uf, setUf] = useState<UF>(\"SP\");\n * const [city, setCity] = useState<string | null>(null);\n * <MunicipalitySearch uf={uf} onSelect={(m) => setCity(m.name)} />\n * <BrazilStateMap uf={uf} selected={city} />\n */\nexport function MunicipalitySearch({\n uf,\n onSelect,\n label,\n placeholder = \"Buscar município…\",\n limit = 8,\n debounceMs = 250,\n disabled = false,\n}: MunicipalitySearchProps): ReactElement {\n const [query, setQuery] = useState<string>(\"\");\n const [results, setResults] = useState<MunicipalityCentroid[]>([]);\n const [open, setOpen] = useState<boolean>(false);\n const [activeIndex, setActiveIndex] = useState<number>(0);\n const debounced = useDebounce(query, debounceMs);\n const listId = useId();\n const expanded = open && results.length > 0;\n const optionId = (index: number): string => `${listId}-option-${index}`;\n\n useEffect(() => {\n let active = true;\n if (!debounced.trim()) {\n setResults([]);\n return;\n }\n void searchMunicipalities(debounced, { uf, limit }).then((hits) => {\n if (!active) return;\n setResults(hits);\n setActiveIndex(0);\n });\n return () => {\n active = false;\n };\n }, [debounced, uf, limit]);\n\n function choose(municipality: MunicipalityCentroid): void {\n onSelect(municipality);\n setQuery(municipality.name);\n setOpen(false);\n }\n\n /**\n * Keyboard model of the input, the same as `Combobox`'s.\n *\n * `Escape` calls `preventDefault()` only while the list is open, so an\n * enclosing `Modal` or `Drawer` stays open and the next `Escape` reaches it.\n * `Enter` is consumed only when it picks a result, so with the list closed\n * it still submits an enclosing form.\n *\n * @param event - The `keydown` on the input.\n */\n function handleKeyDown(event: KeyboardEvent<HTMLInputElement>): void {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n if (!expanded) {\n setOpen(true);\n return;\n }\n setActiveIndex((current) => Math.min(results.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n const municipality = expanded ? results[activeIndex] : undefined;\n if (!municipality) return;\n event.preventDefault();\n choose(municipality);\n } else if (event.key === \"Escape\") {\n if (expanded) event.preventDefault();\n setOpen(false);\n }\n }\n\n return (\n <div className={styles.search}>\n <Input\n label={label}\n value={query}\n placeholder={placeholder}\n disabled={disabled}\n role=\"combobox\"\n aria-expanded={expanded}\n aria-controls={listId}\n aria-autocomplete=\"list\"\n aria-activedescendant={expanded ? optionId(activeIndex) : undefined}\n autoComplete=\"off\"\n onChange={(e) => {\n setQuery(e.target.value);\n setOpen(true);\n }}\n onFocus={() => setOpen(true)}\n onBlur={() => setOpen(false)}\n onKeyDown={handleKeyDown}\n />\n {expanded && (\n <ul\n id={listId}\n className={styles.results}\n role=\"listbox\"\n onMouseDown={keepInputFocus}\n >\n {results.map((m, index) => (\n <li\n key={m.id}\n id={optionId(index)}\n role=\"option\"\n aria-selected={index === activeIndex}\n className={cn(styles.result, index === activeIndex && styles.active)}\n onMouseEnter={() => setActiveIndex(index)}\n onClick={() => choose(m)}\n >\n <span>{m.name}</span>\n <span className={styles.uf}>{m.uf}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAuCA,SAAS,EAAe,GAA2C;CAC/D,EAAM,eAAe;AACzB;AA0BA,SAAgB,EAAmB,EAC/B,OACA,aACA,UACA,iBAAc,qBACd,WAAQ,GACR,gBAAa,KACb,cAAW,MAC2B;CACtC,IAAM,CAAC,GAAO,KAAY,EAAiB,EAAE,GACvC,CAAC,GAAS,KAAc,EAAiC,CAAC,CAAC,GAC3D,CAAC,GAAM,KAAW,EAAkB,EAAK,GACzC,CAAC,GAAa,KAAkB,EAAiB,CAAC,GAClD,IAAY,EAAY,GAAO,CAAU,GACzC,IAAS,EAAM,GACf,IAAW,KAAQ,EAAQ,SAAS,GACpC,KAAY,MAA0B,GAAG,EAAO,UAAU;CAEhE,QAAgB;EACZ,IAAI,IAAS;EACb,IAAI,CAAC,EAAU,KAAK,GAAG;GACnB,EAAW,CAAC,CAAC;GACb;EACJ;EAMA,OALA,EAA0B,GAAW;GAAE;GAAI;EAAM,CAAC,CAAC,CAAC,MAAM,MAAS;GAC1D,MACL,EAAW,CAAI,GACf,EAAe,CAAC;EACpB,CAAC,SACY;GACT,IAAS;EACb;CACJ,GAAG;EAAC;EAAW;EAAI;CAAK,CAAC;CAEzB,SAAS,EAAO,GAA0C;EAGtD,AAFA,EAAS,CAAY,GACrB,EAAS,EAAa,IAAI,GAC1B,EAAQ,EAAK;CACjB;CAYA,SAAS,EAAc,GAA8C;EACjE,IAAI,EAAM,QAAQ,aAAa;GAE3B,IADA,EAAM,eAAe,GACjB,CAAC,GAAU;IACX,EAAQ,EAAI;IACZ;GACJ;GACA,GAAgB,MAAY,KAAK,IAAI,EAAQ,SAAS,GAAG,IAAU,CAAC,CAAC;EACzE,OAAO,IAAI,EAAM,QAAQ,WAErB,AADA,EAAM,eAAe,GACrB,GAAgB,MAAY,KAAK,IAAI,GAAG,IAAU,CAAC,CAAC;OACjD,IAAI,EAAM,QAAQ,SAAS;GAC9B,IAAM,IAAe,IAAW,EAAQ,KAAe,KAAA;GACvD,IAAI,CAAC,GAAc;GAEnB,AADA,EAAM,eAAe,GACrB,EAAO,CAAY;EACvB,OAAO,AAAI,EAAM,QAAQ,aACjB,KAAU,EAAM,eAAe,GACnC,EAAQ,EAAK;CAErB;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAO;EAAvB,UAAA,CACI,kBAAC,GAAD;GACW;GACP,OAAO;GACM;GACH;GACV,MAAK;GACL,iBAAe;GACf,iBAAe;GACf,qBAAkB;GAClB,yBAAuB,IAAW,EAAS,CAAW,IAAI,KAAA;GAC1D,cAAa;GACb,WAAW,MAAM;IAEb,AADA,EAAS,EAAE,OAAO,KAAK,GACvB,EAAQ,EAAI;GAChB;GACA,eAAe,EAAQ,EAAI;GAC3B,cAAc,EAAQ,EAAK;GAC3B,WAAW;EACd,CAAA,GACA,KACG,kBAAC,MAAD;GACI,IAAI;GACJ,WAAW,EAAO;GAClB,MAAK;GACL,aAAa;GAEZ,UAAA,EAAQ,KAAK,GAAG,MACb,kBAAC,MAAD;IAEI,IAAI,EAAS,CAAK;IAClB,MAAK;IACL,iBAAe,MAAU;IACzB,WAAW,EAAG,EAAO,QAAQ,MAAU,KAAe,EAAO,MAAM;IACnE,oBAAoB,EAAe,CAAK;IACxC,eAAe,EAAO,CAAC;IAP3B,UAAA,CASI,kBAAC,QAAD,EAAA,UAAO,EAAE,KAAW,CAAA,GACpB,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAK,UAAA,EAAE;IAAS,CAAA,CACxC;GAVK,GAAA,EAAE,EAUP,CACP;EACD,CAAA,CAEP;;AAEb"}
@@ -1,3 +1,3 @@
1
1
  require("../styles/component/MunicipalitySearch.css");
2
- var e=`tempest_search_iYSqC`,t=`tempest_results_-a0Jg`,n=`tempest_result_D5-no`,r=`tempest_uf_Rj15X`,i={search:e,results:t,result:n,uf:r};exports.default=i,exports.result=n,exports.results=t,exports.search=e,exports.uf=r;
2
+ var e=`tempest_search_iYSqC`,t=`tempest_results_-a0Jg`,n=`tempest_result_D5-no`,r=`tempest_active_qX2ct`,i=`tempest_uf_Rj15X`,a={search:e,results:t,result:n,active:r,uf:i};exports.active=r,exports.default=a,exports.result=n,exports.results=t,exports.search=e,exports.uf=i;
3
3
  //# sourceMappingURL=MunicipalitySearch.module.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"MunicipalitySearch.module.cjs","names":[],"sources":["../../src/br/MunicipalitySearch.module.css"],"sourcesContent":[".search {\n position: relative;\n font-family: var(--tempest-font-sans);\n}\n\n.results {\n position: absolute;\n z-index: 10;\n top: 100%;\n left: 0;\n right: 0;\n margin: var(--tempest-space-1) 0 0;\n padding: var(--tempest-space-1);\n list-style: none;\n max-height: 260px;\n overflow-y: auto;\n background: var(--tempest-surface);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n box-shadow: var(--tempest-shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n}\n\n.result {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: none;\n background: transparent;\n border-radius: var(--tempest-radius-sm);\n font-size: var(--tempest-text-sm);\n color: var(--tempest-text);\n text-align: left;\n cursor: pointer;\n}\n\n.result:hover,\n.result:focus-visible {\n outline: none;\n background: var(--tempest-primary-soft);\n}\n\n.uf {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n font-weight: var(--tempest-weight-medium);\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"MunicipalitySearch.module.cjs","names":[],"sources":["../../src/br/MunicipalitySearch.module.css"],"sourcesContent":[".search {\n position: relative;\n font-family: var(--tempest-font-sans);\n}\n\n.results {\n position: absolute;\n z-index: 10;\n top: 100%;\n left: 0;\n right: 0;\n margin: var(--tempest-space-1) 0 0;\n padding: var(--tempest-space-1);\n list-style: none;\n max-height: 260px;\n overflow-y: auto;\n background: var(--tempest-surface);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n box-shadow: var(--tempest-shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n}\n\n.result {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: none;\n background: transparent;\n border-radius: var(--tempest-radius-sm);\n font-size: var(--tempest-text-sm);\n color: var(--tempest-text);\n text-align: left;\n cursor: pointer;\n}\n\n.result:hover,\n.result.active {\n background: var(--tempest-primary-soft);\n}\n\n.uf {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n font-weight: var(--tempest-weight-medium);\n}\n"],"mappings":";"}
@@ -1,12 +1,13 @@
1
1
  import "../styles/component/MunicipalitySearch.css";
2
2
  //#region src/br/MunicipalitySearch.module.css
3
- var e = "tempest_search_iYSqC", t = "tempest_results_-a0Jg", n = "tempest_result_D5-no", r = "tempest_uf_Rj15X", i = {
3
+ var e = "tempest_search_iYSqC", t = "tempest_results_-a0Jg", n = "tempest_result_D5-no", r = "tempest_active_qX2ct", i = "tempest_uf_Rj15X", a = {
4
4
  search: e,
5
5
  results: t,
6
6
  result: n,
7
- uf: r
7
+ active: r,
8
+ uf: i
8
9
  };
9
10
  //#endregion
10
- export { i as default, n as result, t as results, e as search, r as uf };
11
+ export { r as active, a as default, n as result, t as results, e as search, i as uf };
11
12
 
12
13
  //# sourceMappingURL=MunicipalitySearch.module.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MunicipalitySearch.module.js","names":[],"sources":["../../src/br/MunicipalitySearch.module.css"],"sourcesContent":[".search {\n position: relative;\n font-family: var(--tempest-font-sans);\n}\n\n.results {\n position: absolute;\n z-index: 10;\n top: 100%;\n left: 0;\n right: 0;\n margin: var(--tempest-space-1) 0 0;\n padding: var(--tempest-space-1);\n list-style: none;\n max-height: 260px;\n overflow-y: auto;\n background: var(--tempest-surface);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n box-shadow: var(--tempest-shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n}\n\n.result {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: none;\n background: transparent;\n border-radius: var(--tempest-radius-sm);\n font-size: var(--tempest-text-sm);\n color: var(--tempest-text);\n text-align: left;\n cursor: pointer;\n}\n\n.result:hover,\n.result:focus-visible {\n outline: none;\n background: var(--tempest-primary-soft);\n}\n\n.uf {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n font-weight: var(--tempest-weight-medium);\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"MunicipalitySearch.module.js","names":[],"sources":["../../src/br/MunicipalitySearch.module.css"],"sourcesContent":[".search {\n position: relative;\n font-family: var(--tempest-font-sans);\n}\n\n.results {\n position: absolute;\n z-index: 10;\n top: 100%;\n left: 0;\n right: 0;\n margin: var(--tempest-space-1) 0 0;\n padding: var(--tempest-space-1);\n list-style: none;\n max-height: 260px;\n overflow-y: auto;\n background: var(--tempest-surface);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n box-shadow: var(--tempest-shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));\n}\n\n.result {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: none;\n background: transparent;\n border-radius: var(--tempest-radius-sm);\n font-size: var(--tempest-text-sm);\n color: var(--tempest-text);\n text-align: left;\n cursor: pointer;\n}\n\n.result:hover,\n.result.active {\n background: var(--tempest-primary-soft);\n}\n\n.uf {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n font-weight: var(--tempest-weight-medium);\n}\n"],"mappings":";"}
package/dist/br.d.ts CHANGED
@@ -964,6 +964,18 @@ export declare interface MunicipalityFeature {
964
964
  * result, and `onSelect` fires with its `{ id, name, uf, latitude, longitude }`.
965
965
  * Wire `onSelect` to a `BrazilStateMap` `selected` to highlight it on the map.
966
966
  *
967
+ * Keyboard follows the SDK's `Combobox` (the WAI-ARIA combobox pattern): focus
968
+ * stays on the input, `ArrowDown`/`ArrowUp` move the active result — announced
969
+ * through `aria-activedescendant` — `Enter` picks it, `Escape` closes the list
970
+ * and `Tab` leaves the field, closing the list on the way out.
971
+ *
972
+ * The list used to close on a 120 ms timer after the input blurred, with each
973
+ * result a focusable `<button>`. Measured in Chromium (gallery build, issue
974
+ * #426): `Tab` moved focus to the first result, the timer then unmounted it and
975
+ * focus fell to `<body>`; arrows, `Enter` and `Escape` did nothing. Results are
976
+ * now plain options outside the tab order, and a press on the list cancels the
977
+ * blur instead of racing it.
978
+ *
967
979
  * @example
968
980
  * const [uf, setUf] = useState<UF>("SP");
969
981
  * const [city, setCity] = useState<string | null>(null);
@@ -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,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