tempest-react-sdk 0.74.0 → 0.76.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.
- package/README.md +3 -1
- package/dist/br/BrazilMap.module.cjs.map +1 -1
- package/dist/br/BrazilMap.module.js.map +1 -1
- package/dist/br/MunicipalitySearch.cjs +1 -1
- package/dist/br/MunicipalitySearch.cjs.map +1 -1
- package/dist/br/MunicipalitySearch.js +66 -48
- package/dist/br/MunicipalitySearch.js.map +1 -1
- package/dist/br/MunicipalitySearch.module.cjs +1 -1
- package/dist/br/MunicipalitySearch.module.cjs.map +1 -1
- package/dist/br/MunicipalitySearch.module.js +4 -3
- package/dist/br/MunicipalitySearch.module.js.map +1 -1
- package/dist/br.d.ts +12 -0
- package/dist/components/AIChat/AIChat.module.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.module.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js +1 -0
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AppBar/AppBar.module.cjs +1 -1
- package/dist/components/AppBar/AppBar.module.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.module.js +12 -11
- package/dist/components/AppBar/AppBar.module.js.map +1 -1
- package/dist/components/Button/Button.module.cjs.map +1 -1
- package/dist/components/Button/Button.module.js.map +1 -1
- package/dist/components/Button/ButtonContent.cjs.map +1 -1
- package/dist/components/Button/ButtonContent.js.map +1 -1
- package/dist/components/Carousel/Carousel.module.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.module.js.map +1 -1
- package/dist/components/Chat/Chat.module.cjs.map +1 -1
- package/dist/components/Chat/Chat.module.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js +18 -23
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Combobox/use-listbox-dismiss.cjs +2 -0
- package/dist/components/Combobox/use-listbox-dismiss.cjs.map +1 -0
- package/dist/components/Combobox/use-listbox-dismiss.js +34 -0
- package/dist/components/Combobox/use-listbox-dismiss.js.map +1 -0
- package/dist/components/Dropzone/Dropzone.module.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.module.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.module.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.module.js.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.module.cjs.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.module.js.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.module.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.module.js.map +1 -1
- package/dist/components/ListTile/ListTile.module.cjs.map +1 -1
- package/dist/components/ListTile/ListTile.module.js.map +1 -1
- package/dist/components/Markdown/Markdown.module.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.module.js.map +1 -1
- package/dist/components/Menubar/Menubar.module.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.module.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js +55 -54
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/Navbar/Navbar.cjs +1 -1
- package/dist/components/Navbar/Navbar.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.js +1 -0
- package/dist/components/Navbar/Navbar.js.map +1 -1
- package/dist/components/Navbar/Navbar.module.cjs +1 -1
- package/dist/components/Navbar/Navbar.module.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.module.js +9 -8
- package/dist/components/Navbar/Navbar.module.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.module.cjs.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.module.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.js.map +1 -1
- package/dist/components/PinInput/PinInput.module.cjs.map +1 -1
- package/dist/components/PinInput/PinInput.module.js.map +1 -1
- package/dist/components/SectionHeader/SectionHeader.cjs +1 -1
- package/dist/components/SectionHeader/SectionHeader.cjs.map +1 -1
- package/dist/components/SectionHeader/SectionHeader.js +33 -25
- package/dist/components/SectionHeader/SectionHeader.js.map +1 -1
- package/dist/components/Tour/Tour.module.cjs.map +1 -1
- package/dist/components/Tour/Tour.module.js.map +1 -1
- package/dist/components/Transfer/Transfer.module.cjs.map +1 -1
- package/dist/components/Transfer/Transfer.module.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.module.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.module.js.map +1 -1
- package/dist/geo/TrajectoryMap.module.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.module.js.map +1 -1
- package/dist/icons/IconPicker.module.cjs.map +1 -1
- package/dist/icons/IconPicker.module.js.map +1 -1
- package/dist/neural.cjs +2 -0
- package/dist/neural.cjs.map +1 -0
- package/dist/neural.d.ts +110 -0
- package/dist/neural.js +48 -0
- package/dist/neural.js.map +1 -0
- package/dist/styles/actions.css +2 -2
- package/dist/styles/br.css +3 -3
- package/dist/styles/chat.css +5 -5
- package/dist/styles/component/AIChat.css +3 -3
- package/dist/styles/component/AppBar.css +1 -0
- package/dist/styles/component/BrazilMap.css +2 -2
- package/dist/styles/component/Button.css +1 -1
- package/dist/styles/component/Carousel.css +3 -3
- package/dist/styles/component/Chat.css +2 -2
- package/dist/styles/component/Dropzone.css +2 -1
- package/dist/styles/component/FilterBar.css +2 -2
- package/dist/styles/component/FloatingActionButton.css +1 -1
- package/dist/styles/component/IconPicker.css +1 -1
- package/dist/styles/component/ImageCropper.css +2 -2
- package/dist/styles/component/ListTile.css +1 -1
- package/dist/styles/component/Markdown.css +1 -1
- package/dist/styles/component/Menubar.css +1 -1
- package/dist/styles/component/MunicipalitySearch.css +1 -1
- package/dist/styles/component/Navbar.css +1 -0
- package/dist/styles/component/NavigationMenu.css +1 -1
- package/dist/styles/component/NavigationRail.css +1 -1
- package/dist/styles/component/NotificationCenter.css +3 -3
- package/dist/styles/component/PasswordInput.css +1 -1
- package/dist/styles/component/PinInput.css +1 -1
- package/dist/styles/component/Tour.css +2 -2
- package/dist/styles/component/TrajectoryMap.css +1 -1
- package/dist/styles/component/Transfer.css +2 -2
- package/dist/styles/component/VirtualTable.css +2 -2
- package/dist/styles/data.css +10 -10
- package/dist/styles/feedback.css +3 -3
- package/dist/styles/forms.css +6 -5
- package/dist/styles/geo.css +1 -1
- package/dist/styles/icons.css +1 -1
- package/dist/styles/inverse.css +225 -0
- package/dist/styles/navigation.css +8 -6
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.d.ts +130 -17
- package/dist/theme/create-theme.cjs +1 -1
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +6 -1
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/inverse-render.cjs +1 -1
- package/dist/theme/inverse-render.cjs.map +1 -1
- package/dist/theme/inverse-render.js +1 -1
- package/dist/theme/inverse-render.js.map +1 -1
- package/dist/theme/inverse-restore.cjs +5 -0
- package/dist/theme/inverse-restore.cjs.map +1 -0
- package/dist/theme/inverse-restore.js +30 -0
- package/dist/theme/inverse-restore.js.map +1 -0
- package/dist/theme/inverse-surface.cjs +1 -1
- package/dist/theme/inverse-surface.cjs.map +1 -1
- package/dist/theme/inverse-surface.js +69 -72
- package/dist/theme/inverse-surface.js.map +1 -1
- package/package.json +11 -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
|
|
|
@@ -287,6 +287,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
287
287
|
| `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`), `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
|
|
288
288
|
| `charts` _(subpath `tempest-react-sdk/charts`, opt. peer `recharts`)_ | `AreaChart`, `BarChart`, `LineChart`, `PieChart`, `RadarChart`, `DEFAULT_CHART_COLORS` — themed recharts wrappers (recharts externalized; install it only if you use charts) |
|
|
289
289
|
| `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2065 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one of 46 balanced shards** (40 slugs each) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces — sharding by range rather than by initial letter took the worst fetch from 19.10 KB to 4.78 KB brotli. Unknown slug renders `fallback` and never throws; lucide's 258 deprecated aliases keep resolving |
|
|
290
|
+
| `neural` _(subpath `tempest-react-sdk/neural`, opt. peer `@sapphi-red/web-noise-suppressor`)_ | `isNeuralSuppressionSupported`, `prepareNeuralSuppressor`, `createNeuralSuppressor`, types `NeuralSuppressorAssets`, `NeuralSuppressor` — RNNoise noise suppression: a recurrent model in an `AudioWorklet` separates speech from non-speech on a 48 kHz context and hands back a denoised `MediaStreamTrack` to publish in place of the capture (peer externalized; install it only if you use neural) |
|
|
290
291
|
| `audio` | `createAudioPlayer`, `playAudio`, `stopAudio`, `useAudio`, `createAudioBus` / `useAudioBus` (gain above 100%, post-mix limiter, `setSinkId` routing), `DEFAULT_MAX_GAIN` |
|
|
291
292
|
| `capture` | `BarcodeScanner`, `useBarcodeScanner`, `useTorch`, `isBarcodeDetectionSupported`, `getSupportedBarcodeFormats`, `createBarcodeDetector`, `normalizeBarcode`, `useVideoRecorder`, `createVideoRecorder`, `pickVideoMimeType`, `isVideoRecordingSupported`, `useScreenCapture`, `isScreenCaptureSupported`, `useSpeechRecognition`, `isSpeechRecognitionSupported`, `createMediaRecorder` — barcode/QR reading over the native `BarcodeDetector` (Chromium-only, so inject a polyfill through `detector` or render the `unsupported` fallback), video + screen recording on the same engine as the audio recorder, and Web Speech dictation (**Chromium streams the audio to a Google server**) |
|
|
292
293
|
| `offline` _(peer: `dexie`)_ | `createOfflineStore`, `createOfflineSync` (outbox + delta-pull + watermark engine, now with `subscribe`/`getState`/`dispose` + cross-tab `crossTab`), `useOfflineSync`/`useSyncStatus` (reactive hooks), `lastWriteWins`/`higherVersionWins` (conflict resolvers), types: `OfflineStore`, `OfflineStoreConfig`, `ListOptions`, `OfflineSync`, `OfflineSyncConfig`, `OutboxEntry`, `PullPage`, `SyncPhase`, `SyncState`, `SyncRunSummary`, `SyncTrigger`, `SyncStatus`, `SyncTone`, `WatermarkStore` |
|
|
@@ -325,6 +326,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
325
326
|
| `vision` _(subpath `tempest-react-sdk/vision`, opt. peer `onnxruntime-web`)_ | `Classifier`, `Detector`, `Segmenter`, `DetectClassify` (fused detect→classify pipeline, optionally with a segmentation stage) + `warmup()`, FP16 models, `cache` (Cache Storage), video/`VideoFrame` inputs, `raiseOnEmpty`/`NoDetectionsError`, `LetterboxPipeline`, `readFusionSpec` and results/labels/preprocess — on-device ONNX vision inference, vendored from `@mauriciobenjamin700/ort-vision-sdk-web` (onnxruntime-web externalized; install it only if you use vision). Plus browser capture primitives: `useCameraStream` (rear-camera `getUserMedia` + classified errors + `retry()`), `computeImageLuminance`/`isLuminanceAcceptable`/`LowLuminanceError`/`LUMINANCE_SAMPLE_MAX_EDGE` (BT.709 brightness), `useLiveLuminance` (live rAF sampling) |
|
|
326
327
|
| `tabular` _(subpath `tempest-react-sdk/tabular`, opt. peer `onnxruntime-web`)_ | `TabularPredictor`, `useTabularPredictor` — scikit-learn models exported by `tempest-fastapi-sdk`'s `export_sklearn_to_onnx`, running in the browser with no inference server. Offline by design: `fetchModelBytes`/`isModelCached`/`cacheModelBytes`/`clearModelCache` (Cache Storage, cache-first) plus `configureOrtAssets`/`ortAssetUrls`/`ORT_WASM_ASSETS` for the runtime's WebAssembly binaries, which ONNX Runtime Web fetches at session creation. Import the default `onnxruntime-web` entry — the WebGPU build has no `ai.onnx.ml` kernels |
|
|
327
328
|
| `imaging` _(subpath `tempest-react-sdk/imaging`, no deps)_ | `resizeImage`, `cropImage`, `rotateImage`, `flipImage`, `compressToTarget` (binary-searches quality into a byte budget), `createThumbnails` (several sizes from one decode), `captureFrame` (one frame out of a `<video>`, at the instant you ask for), `decodeImage`/`readImageInfo`, `encodeImage`, `supportsImageType`/`bestSupportedType`, plus `useImagePreview`/`useImageProcessing`. Canvas only — decodes with EXIF orientation applied (phone photos arrive upright), reports the format actually produced (asking for AVIF where there is no encoder silently returns PNG), and re-encoding drops EXIF, which is the privacy step as much as the bandwidth one. `captureFrame` is the one with teeth: `seeked` does not mean the new frame is readable, so the naive seek-then-draw returns the neighbouring frame with no error — and a `MediaRecorder` blob reports no duration until it is probed for one, which it does for you |
|
|
329
|
+
| `neural` _(subpath `tempest-react-sdk/neural`, opt. peer `@sapphi-red/web-noise-suppressor`)_ | `isNeuralSuppressionSupported`, `prepareNeuralSuppressor`, `createNeuralSuppressor`, types `NeuralSuppressorAssets`, `NeuralSuppressor` — RNNoise noise suppression: a recurrent model in an `AudioWorklet` separates speech from non-speech on a 48 kHz context and hands back a denoised `MediaStreamTrack` to publish in place of the capture (peer externalized; install it only if you use neural) |
|
|
328
330
|
| `audio` | `createAudioPlayer`, `playAudio`, `stopAudio`, `useAudio`, `createAudioBus` / `useAudioBus` (gain above 100%, post-mix limiter, `setSinkId` routing), `DEFAULT_MAX_GAIN` |
|
|
329
331
|
| `capture` | `BarcodeScanner`, `useBarcodeScanner`, `useTorch`, `isBarcodeDetectionSupported`, `getSupportedBarcodeFormats`, `createBarcodeDetector`, `normalizeBarcode`, `useVideoRecorder`, `createVideoRecorder`, `pickVideoMimeType`, `isVideoRecordingSupported`, `useScreenCapture`, `isScreenCaptureSupported`, `useSpeechRecognition`, `isSpeechRecognitionSupported`, `createMediaRecorder` — barcode/QR reading over the native `BarcodeDetector` (Chromium-only, so inject a polyfill through `detector` or render the `unsupported` fallback), video + screen recording on the same engine as the audio recorder, and Web Speech dictation (**Chromium streams the audio to a Google server**) |
|
|
330
332
|
| `offline` _(peer: `dexie`)_ | `createOfflineStore`, `createOfflineSync` (outbox + delta-pull + watermark engine, now with `subscribe`/`getState`/`dispose` + cross-tab `crossTab`), `useOfflineSync`/`useSyncStatus` (reactive hooks), `lastWriteWins`/`higherVersionWins` (conflict resolvers), types: `OfflineStore`, `OfflineStoreConfig`, `ListOptions`, `OfflineSync`, `OfflineSyncConfig`, `OutboxEntry`, `PullPage`, `SyncPhase`, `SyncState`, `SyncRunSummary`, `SyncTrigger`, `SyncStatus`, `SyncTone`, `WatermarkStore` |
|
|
@@ -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-
|
|
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-
|
|
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"),
|
|
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 {
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import r from "./
|
|
5
|
-
import
|
|
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(
|
|
9
|
-
|
|
10
|
-
|
|
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 (!
|
|
13
|
-
|
|
16
|
+
if (!E.trim()) {
|
|
17
|
+
x([]);
|
|
14
18
|
return;
|
|
15
19
|
}
|
|
16
|
-
return
|
|
17
|
-
uf:
|
|
18
|
-
limit:
|
|
20
|
+
return r(E, {
|
|
21
|
+
uf: d,
|
|
22
|
+
limit: h
|
|
19
23
|
}).then((t) => {
|
|
20
|
-
e &&
|
|
24
|
+
e && (x(t), T(0));
|
|
21
25
|
}), () => {
|
|
22
26
|
e = !1;
|
|
23
27
|
};
|
|
24
28
|
}, [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
E,
|
|
30
|
+
d,
|
|
31
|
+
h
|
|
28
32
|
]);
|
|
29
|
-
function
|
|
30
|
-
|
|
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:
|
|
34
|
-
children: [/* @__PURE__ */ c(
|
|
35
|
-
label:
|
|
36
|
-
value:
|
|
37
|
-
placeholder:
|
|
38
|
-
disabled:
|
|
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":
|
|
41
|
-
"aria-controls":
|
|
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
|
-
|
|
64
|
+
y(e.target.value), C(!0);
|
|
45
65
|
},
|
|
46
|
-
onFocus: () =>
|
|
47
|
-
onBlur: () =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
73
|
+
onMouseDown: u,
|
|
74
|
+
children: b.map((t, n) => /* @__PURE__ */ l("li", {
|
|
75
|
+
id: k(n),
|
|
56
76
|
role: "option",
|
|
57
|
-
"aria-selected":
|
|
58
|
-
className:
|
|
59
|
-
|
|
60
|
-
onClick: () =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
}
|
|
85
|
+
}, t.id))
|
|
68
86
|
})]
|
|
69
87
|
});
|
|
70
88
|
}
|
|
71
89
|
//#endregion
|
|
72
|
-
export {
|
|
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 {
|
|
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`,
|
|
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
|
|
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 = "
|
|
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
|
-
|
|
7
|
+
active: r,
|
|
8
|
+
uf: i
|
|
8
9
|
};
|
|
9
10
|
//#endregion
|
|
10
|
-
export {
|
|
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
|
|
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":";"}
|