@mailwoman/react 10.0.0 → 10.1.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 +17 -16
- package/fonts.css +97 -22
- package/lib/common/About.tsx +7 -7
- package/lib/common/ClientOnly.tsx +2 -14
- package/lib/common/CopyButton.tsx +3 -1
- package/lib/common/LoadingIndicator.tsx +4 -2
- package/lib/common/PresetChips.tsx +9 -8
- package/lib/common/confidence-tiers.ts +7 -6
- package/lib/common/cx.ts +3 -1
- package/lib/common/text-tokens.ts +7 -4
- package/lib/common/useBuildInfo.ts +54 -0
- package/lib/common/useClipboard.ts +2 -11
- package/lib/common.ts +22 -0
- package/lib/index.ts +27 -17
- package/lib/map/AppIdentity.tsx +85 -0
- package/lib/map/BackendControl.tsx +9 -10
- package/lib/map/CompareToggle.tsx +8 -31
- package/lib/map/DebugInfo.tsx +88 -0
- package/lib/map/Geocoder.tsx +60 -46
- package/lib/map/GeocoderControls.tsx +430 -134
- package/lib/map/GraticuleLayer.tsx +54 -0
- package/lib/map/MapCanvas.tsx +23 -12
- package/lib/map/MapChipRow.tsx +4 -8
- package/lib/map/MapCompass.tsx +15 -24
- package/lib/map/MapControlStack.tsx +8 -9
- package/lib/map/MapFooter.tsx +2 -16
- package/lib/map/MapProgressBar.tsx +10 -13
- package/lib/map/MapSearchBar.tsx +31 -17
- package/lib/map/MapSheet.tsx +24 -15
- package/lib/map/OverlayLayers.tsx +1 -1
- package/lib/map/PlaceAutocomplete.tsx +10 -21
- package/lib/map/PlaceMarker.tsx +4 -2
- package/lib/map/ResolvedPlaceLayers.tsx +5 -14
- package/lib/map/ResultCamera.tsx +8 -31
- package/lib/map/ResultOverlay.tsx +2 -12
- package/lib/map/ResultPanel.tsx +1 -20
- package/lib/map/VersionPicker.tsx +10 -19
- package/lib/map/fake-runtime.ts +10 -12
- package/lib/map/geometry.ts +14 -47
- package/lib/map/graticule.ts +80 -0
- package/lib/map/map-debug.ts +1 -1
- package/lib/map/place-render.ts +28 -60
- package/lib/map/types.ts +58 -104
- package/lib/map/useCompareState.ts +5 -4
- package/lib/map/useGeocode.ts +7 -5
- package/lib/map/useMapBearing.ts +13 -13
- package/lib/map/useMapLabelPick.ts +21 -30
- package/lib/map/useMapPlaceRender.ts +2 -6
- package/lib/map/usePlaceAutocomplete.ts +44 -50
- package/lib/map.ts +95 -0
- package/lib/pipeline/CandidatePicker.tsx +13 -5
- package/lib/pipeline/ConfidenceCell.tsx +1 -1
- package/lib/pipeline/FailureDiagnostic.tsx +8 -2
- package/lib/pipeline/QueryForm.tsx +5 -2
- package/lib/pipeline/ResolvedPlace.tsx +1 -1
- package/lib/pipeline/SpanHighlight.tsx +7 -5
- package/lib/pipeline/TimingPanel.tsx +1 -1
- package/lib/pipeline/TreeView.tsx +1 -1
- package/lib/pipeline/copy.ts +22 -25
- package/lib/pipeline/presets.ts +15 -13
- package/lib/pipeline/types.ts +21 -14
- package/lib/pipeline/useParsePipeline.ts +16 -13
- package/lib/pipeline.ts +36 -0
- package/lib/poi/AbstainPanel.tsx +1 -1
- package/lib/poi/LiveResultsBlock.tsx +2 -1
- package/lib/poi/POIExplorer.tsx +24 -15
- package/lib/poi/QueryInput.tsx +1 -1
- package/lib/poi/SubjectPanel.tsx +4 -3
- package/lib/poi/runtime.ts +5 -16
- package/lib/poi/types.ts +39 -27
- package/lib/poi/usePOISearch.ts +39 -44
- package/lib/poi.ts +39 -0
- package/lib/runtime/useReleaseRuntime.ts +125 -74
- package/lib/search/SearchModal.tsx +317 -0
- package/lib/search/search.css +144 -0
- package/lib/search/types.ts +67 -0
- package/lib/search.ts +11 -0
- package/out/common/About.d.ts +5 -5
- package/out/common/About.d.ts.map +1 -1
- package/out/common/About.js +1 -1
- package/out/common/About.js.map +1 -1
- package/out/common/ClientOnly.d.ts +2 -11
- package/out/common/ClientOnly.d.ts.map +1 -1
- package/out/common/ClientOnly.js +1 -13
- package/out/common/ClientOnly.js.map +1 -1
- package/out/common/CopyButton.d.ts +3 -1
- package/out/common/CopyButton.d.ts.map +1 -1
- package/out/common/CopyButton.js.map +1 -1
- package/out/common/LoadingIndicator.d.ts +4 -2
- package/out/common/LoadingIndicator.d.ts.map +1 -1
- package/out/common/LoadingIndicator.js.map +1 -1
- package/out/common/PresetChips.d.ts +9 -8
- package/out/common/PresetChips.d.ts.map +1 -1
- package/out/common/PresetChips.js.map +1 -1
- package/out/common/confidence-tiers.d.ts +7 -6
- package/out/common/confidence-tiers.d.ts.map +1 -1
- package/out/common/confidence-tiers.js +7 -6
- package/out/common/confidence-tiers.js.map +1 -1
- package/out/common/cx.d.ts +3 -1
- package/out/common/cx.d.ts.map +1 -1
- package/out/common/cx.js +3 -1
- package/out/common/cx.js.map +1 -1
- package/out/common/text-tokens.d.ts +4 -3
- package/out/common/text-tokens.d.ts.map +1 -1
- package/out/common/text-tokens.js +6 -3
- package/out/common/text-tokens.js.map +1 -1
- package/out/common/useBuildInfo.d.ts +27 -0
- package/out/common/useBuildInfo.d.ts.map +1 -0
- package/out/common/useBuildInfo.js +32 -0
- package/out/common/useBuildInfo.js.map +1 -0
- package/out/common/useClipboard.d.ts +1 -3
- package/out/common/useClipboard.d.ts.map +1 -1
- package/out/common/useClipboard.js +2 -11
- package/out/common/useClipboard.js.map +1 -1
- package/out/common.d.ts +22 -0
- package/out/common.d.ts.map +1 -0
- package/out/common.js +15 -0
- package/out/common.js.map +1 -0
- package/out/index.d.ts +26 -16
- package/out/index.d.ts.map +1 -1
- package/out/index.js +21 -15
- package/out/index.js.map +1 -1
- package/out/map/AppIdentity.d.ts +42 -0
- package/out/map/AppIdentity.d.ts.map +1 -0
- package/out/map/AppIdentity.js +16 -0
- package/out/map/AppIdentity.js.map +1 -0
- package/out/map/BackendControl.d.ts +9 -10
- package/out/map/BackendControl.d.ts.map +1 -1
- package/out/map/BackendControl.js +1 -1
- package/out/map/BackendControl.js.map +1 -1
- package/out/map/CompareToggle.d.ts +8 -31
- package/out/map/CompareToggle.d.ts.map +1 -1
- package/out/map/CompareToggle.js +4 -1
- package/out/map/CompareToggle.js.map +1 -1
- package/out/map/DebugInfo.d.ts +34 -0
- package/out/map/DebugInfo.d.ts.map +1 -0
- package/out/map/DebugInfo.js +39 -0
- package/out/map/DebugInfo.js.map +1 -0
- package/out/map/Geocoder.d.ts +41 -30
- package/out/map/Geocoder.d.ts.map +1 -1
- package/out/map/Geocoder.js +20 -36
- package/out/map/Geocoder.js.map +1 -1
- package/out/map/GeocoderControls.d.ts +14 -42
- package/out/map/GeocoderControls.d.ts.map +1 -1
- package/out/map/GeocoderControls.js +250 -38
- package/out/map/GeocoderControls.js.map +1 -1
- package/out/map/GraticuleLayer.d.ts +28 -0
- package/out/map/GraticuleLayer.d.ts.map +1 -0
- package/out/map/GraticuleLayer.js +25 -0
- package/out/map/GraticuleLayer.js.map +1 -0
- package/out/map/MapCanvas.d.ts +23 -12
- package/out/map/MapCanvas.d.ts.map +1 -1
- package/out/map/MapCanvas.js +5 -2
- package/out/map/MapCanvas.js.map +1 -1
- package/out/map/MapChipRow.d.ts +4 -8
- package/out/map/MapChipRow.d.ts.map +1 -1
- package/out/map/MapChipRow.js.map +1 -1
- package/out/map/MapCompass.d.ts +11 -20
- package/out/map/MapCompass.d.ts.map +1 -1
- package/out/map/MapCompass.js +5 -1
- package/out/map/MapCompass.js.map +1 -1
- package/out/map/MapControlStack.d.ts +8 -9
- package/out/map/MapControlStack.d.ts.map +1 -1
- package/out/map/MapControlStack.js +4 -1
- package/out/map/MapControlStack.js.map +1 -1
- package/out/map/MapFooter.d.ts +2 -15
- package/out/map/MapFooter.d.ts.map +1 -1
- package/out/map/MapFooter.js +0 -14
- package/out/map/MapFooter.js.map +1 -1
- package/out/map/MapProgressBar.d.ts +10 -13
- package/out/map/MapProgressBar.d.ts.map +1 -1
- package/out/map/MapProgressBar.js.map +1 -1
- package/out/map/MapSearchBar.d.ts +16 -17
- package/out/map/MapSearchBar.d.ts.map +1 -1
- package/out/map/MapSearchBar.js +6 -0
- package/out/map/MapSearchBar.js.map +1 -1
- package/out/map/MapSheet.d.ts +15 -12
- package/out/map/MapSheet.d.ts.map +1 -1
- package/out/map/MapSheet.js +8 -12
- package/out/map/MapSheet.js.map +1 -1
- package/out/map/OverlayLayers.d.ts +1 -1
- package/out/map/OverlayLayers.js +1 -1
- package/out/map/PlaceAutocomplete.d.ts +10 -21
- package/out/map/PlaceAutocomplete.d.ts.map +1 -1
- package/out/map/PlaceAutocomplete.js +2 -1
- package/out/map/PlaceAutocomplete.js.map +1 -1
- package/out/map/PlaceMarker.d.ts +4 -2
- package/out/map/PlaceMarker.d.ts.map +1 -1
- package/out/map/PlaceMarker.js +3 -1
- package/out/map/PlaceMarker.js.map +1 -1
- package/out/map/ResolvedPlaceLayers.d.ts +5 -14
- package/out/map/ResolvedPlaceLayers.d.ts.map +1 -1
- package/out/map/ResolvedPlaceLayers.js +1 -1
- package/out/map/ResolvedPlaceLayers.js.map +1 -1
- package/out/map/ResultCamera.d.ts +8 -31
- package/out/map/ResultCamera.d.ts.map +1 -1
- package/out/map/ResultCamera.js +3 -28
- package/out/map/ResultCamera.js.map +1 -1
- package/out/map/ResultOverlay.d.ts +2 -12
- package/out/map/ResultOverlay.d.ts.map +1 -1
- package/out/map/ResultOverlay.js +1 -2
- package/out/map/ResultOverlay.js.map +1 -1
- package/out/map/ResultPanel.d.ts +1 -20
- package/out/map/ResultPanel.d.ts.map +1 -1
- package/out/map/ResultPanel.js.map +1 -1
- package/out/map/VersionPicker.d.ts +7 -19
- package/out/map/VersionPicker.d.ts.map +1 -1
- package/out/map/VersionPicker.js +5 -2
- package/out/map/VersionPicker.js.map +1 -1
- package/out/map/fake-runtime.d.ts +9 -11
- package/out/map/fake-runtime.d.ts.map +1 -1
- package/out/map/fake-runtime.js +9 -12
- package/out/map/fake-runtime.js.map +1 -1
- package/out/map/geometry.d.ts +13 -34
- package/out/map/geometry.d.ts.map +1 -1
- package/out/map/geometry.js +9 -34
- package/out/map/geometry.js.map +1 -1
- package/out/map/graticule.d.ts +17 -0
- package/out/map/graticule.d.ts.map +1 -0
- package/out/map/graticule.js +67 -0
- package/out/map/graticule.js.map +1 -0
- package/out/map/map-debug.d.ts +1 -1
- package/out/map/map-debug.js +1 -1
- package/out/map/place-render.d.ts +23 -43
- package/out/map/place-render.d.ts.map +1 -1
- package/out/map/place-render.js +4 -39
- package/out/map/place-render.js.map +1 -1
- package/out/map/types.d.ts +58 -102
- package/out/map/types.d.ts.map +1 -1
- package/out/map/types.js +1 -10
- package/out/map/types.js.map +1 -1
- package/out/map/useCompareState.d.ts +5 -4
- package/out/map/useCompareState.d.ts.map +1 -1
- package/out/map/useCompareState.js +3 -3
- package/out/map/useCompareState.js.map +1 -1
- package/out/map/useGeocode.d.ts +5 -3
- package/out/map/useGeocode.d.ts.map +1 -1
- package/out/map/useGeocode.js +4 -4
- package/out/map/useGeocode.js.map +1 -1
- package/out/map/useMapBearing.d.ts +11 -11
- package/out/map/useMapBearing.d.ts.map +1 -1
- package/out/map/useMapBearing.js +6 -11
- package/out/map/useMapBearing.js.map +1 -1
- package/out/map/useMapLabelPick.d.ts +4 -14
- package/out/map/useMapLabelPick.d.ts.map +1 -1
- package/out/map/useMapLabelPick.js +21 -30
- package/out/map/useMapLabelPick.js.map +1 -1
- package/out/map/useMapPlaceRender.d.ts +2 -6
- package/out/map/useMapPlaceRender.d.ts.map +1 -1
- package/out/map/useMapPlaceRender.js +2 -6
- package/out/map/useMapPlaceRender.js.map +1 -1
- package/out/map/usePlaceAutocomplete.d.ts +30 -28
- package/out/map/usePlaceAutocomplete.d.ts.map +1 -1
- package/out/map/usePlaceAutocomplete.js +6 -33
- package/out/map/usePlaceAutocomplete.js.map +1 -1
- package/out/map.d.ts +69 -0
- package/out/map.d.ts.map +1 -0
- package/out/map.js +46 -0
- package/out/map.js.map +1 -0
- package/out/pipeline/CandidatePicker.d.ts +12 -2
- package/out/pipeline/CandidatePicker.d.ts.map +1 -1
- package/out/pipeline/CandidatePicker.js +1 -1
- package/out/pipeline/CandidatePicker.js.map +1 -1
- package/out/pipeline/ConfidenceCell.d.ts +1 -1
- package/out/pipeline/ConfidenceCell.js +1 -1
- package/out/pipeline/FailureDiagnostic.d.ts +8 -2
- package/out/pipeline/FailureDiagnostic.d.ts.map +1 -1
- package/out/pipeline/FailureDiagnostic.js +3 -0
- package/out/pipeline/FailureDiagnostic.js.map +1 -1
- package/out/pipeline/QueryForm.d.ts +5 -2
- package/out/pipeline/QueryForm.d.ts.map +1 -1
- package/out/pipeline/QueryForm.js.map +1 -1
- package/out/pipeline/SpanHighlight.d.ts +4 -3
- package/out/pipeline/SpanHighlight.d.ts.map +1 -1
- package/out/pipeline/SpanHighlight.js +3 -2
- package/out/pipeline/SpanHighlight.js.map +1 -1
- package/out/pipeline/TimingPanel.d.ts +1 -1
- package/out/pipeline/TreeView.d.ts +1 -1
- package/out/pipeline/copy.d.ts +1 -1
- package/out/pipeline/copy.d.ts.map +1 -1
- package/out/pipeline/copy.js +4 -3
- package/out/pipeline/copy.js.map +1 -1
- package/out/pipeline/presets.d.ts +5 -4
- package/out/pipeline/presets.d.ts.map +1 -1
- package/out/pipeline/presets.js +15 -13
- package/out/pipeline/presets.js.map +1 -1
- package/out/pipeline/types.d.ts +21 -14
- package/out/pipeline/types.d.ts.map +1 -1
- package/out/pipeline/types.js +2 -4
- package/out/pipeline/types.js.map +1 -1
- package/out/pipeline/useParsePipeline.d.ts +14 -9
- package/out/pipeline/useParsePipeline.d.ts.map +1 -1
- package/out/pipeline/useParsePipeline.js +7 -8
- package/out/pipeline/useParsePipeline.js.map +1 -1
- package/out/pipeline.d.ts +33 -0
- package/out/pipeline.d.ts.map +1 -0
- package/out/pipeline.js +21 -0
- package/out/pipeline.js.map +1 -0
- package/out/poi/AbstainPanel.d.ts +1 -1
- package/out/poi/LiveResultsBlock.d.ts +2 -1
- package/out/poi/LiveResultsBlock.d.ts.map +1 -1
- package/out/poi/LiveResultsBlock.js.map +1 -1
- package/out/poi/POIExplorer.d.ts +22 -12
- package/out/poi/POIExplorer.d.ts.map +1 -1
- package/out/poi/POIExplorer.js +9 -8
- package/out/poi/POIExplorer.js.map +1 -1
- package/out/poi/QueryInput.d.ts +1 -1
- package/out/poi/SubjectPanel.d.ts +2 -2
- package/out/poi/SubjectPanel.d.ts.map +1 -1
- package/out/poi/SubjectPanel.js +2 -1
- package/out/poi/SubjectPanel.js.map +1 -1
- package/out/poi/runtime.d.ts +5 -14
- package/out/poi/runtime.d.ts.map +1 -1
- package/out/poi/runtime.js +5 -16
- package/out/poi/runtime.js.map +1 -1
- package/out/poi/types.d.ts +39 -27
- package/out/poi/types.d.ts.map +1 -1
- package/out/poi/types.js +2 -4
- package/out/poi/types.js.map +1 -1
- package/out/poi/usePOISearch.d.ts +28 -17
- package/out/poi/usePOISearch.d.ts.map +1 -1
- package/out/poi/usePOISearch.js +6 -32
- package/out/poi/usePOISearch.js.map +1 -1
- package/out/poi.d.ts +23 -0
- package/out/poi.d.ts.map +1 -0
- package/out/poi.js +15 -0
- package/out/poi.js.map +1 -0
- package/out/runtime/useReleaseRuntime.d.ts +64 -65
- package/out/runtime/useReleaseRuntime.d.ts.map +1 -1
- package/out/runtime/useReleaseRuntime.js +31 -26
- package/out/runtime/useReleaseRuntime.js.map +1 -1
- package/out/{runtime/index.d.ts → runtime.d.ts} +1 -1
- package/out/runtime.d.ts.map +1 -0
- package/out/{runtime/index.js → runtime.js} +1 -1
- package/out/runtime.js.map +1 -0
- package/out/search/SearchModal.d.ts +33 -0
- package/out/search/SearchModal.d.ts.map +1 -0
- package/out/search/SearchModal.js +187 -0
- package/out/search/SearchModal.js.map +1 -0
- package/out/search/types.d.ts +64 -0
- package/out/search/types.d.ts.map +1 -0
- package/out/search/types.js +13 -0
- package/out/search/types.js.map +1 -0
- package/out/search.d.ts +11 -0
- package/out/search.d.ts.map +1 -0
- package/out/search.js +9 -0
- package/out/search.js.map +1 -0
- package/package.json +60 -142
- package/styles.css +482 -266
- package/tokens/index.css +24 -6
- package/tokens/tokens.json +114 -6
- package/lib/common/index.ts +0 -22
- package/lib/map/index.ts +0 -95
- package/lib/pipeline/index.ts +0 -36
- package/lib/poi/index.ts +0 -39
- package/out/common/index.d.ts +0 -22
- package/out/common/index.d.ts.map +0 -1
- package/out/common/index.js +0 -15
- package/out/common/index.js.map +0 -1
- package/out/map/index.d.ts +0 -69
- package/out/map/index.d.ts.map +0 -1
- package/out/map/index.js +0 -46
- package/out/map/index.js.map +0 -1
- package/out/pipeline/index.d.ts +0 -33
- package/out/pipeline/index.d.ts.map +0 -1
- package/out/pipeline/index.js +0 -21
- package/out/pipeline/index.js.map +0 -1
- package/out/poi/index.d.ts +0 -23
- package/out/poi/index.d.ts.map +0 -1
- package/out/poi/index.js +0 -15
- package/out/poi/index.js.map +0 -1
- package/out/runtime/index.d.ts.map +0 -1
- package/out/runtime/index.js.map +0 -1
- package/styleframe.config.ts +0 -329
- package/vitest.node.config.ts +0 -20
- /package/lib/{runtime/index.ts → runtime.ts} +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The docs search modal. A native `<dialog>` opened with `showModal()` supplies the focus trap, the inert
|
|
7
|
+
* background, the backdrop, the close on Escape and the return of focus to the opener. The query field is an
|
|
8
|
+
* `<input type="search">` with the ARIA combobox role. DOM focus stays on the input, and the selected hit is
|
|
9
|
+
* named by `aria-activedescendant`. The component owns no data access: `search` is passed in.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { type KeyboardEvent, type MouseEvent, type ReactNode, useEffect, useId, useMemo, useRef, useState } from "react"
|
|
13
|
+
|
|
14
|
+
import { useDebouncedValue } from "#common/useDebouncedValue"
|
|
15
|
+
|
|
16
|
+
import type { SearchHit, SearchResponse } from "./types.ts"
|
|
17
|
+
|
|
18
|
+
export interface SearchModalProps {
|
|
19
|
+
open: boolean
|
|
20
|
+
onClose: () => void
|
|
21
|
+
/**
|
|
22
|
+
* Called with the hit's site-relative link on Enter or on an unmodified click.
|
|
23
|
+
* Absent, the link navigates as an ordinary anchor.
|
|
24
|
+
*/
|
|
25
|
+
onNavigate?: (href: string) => void
|
|
26
|
+
/**
|
|
27
|
+
* Runs the query.
|
|
28
|
+
*
|
|
29
|
+
* The function must keep a stable identity across renders (for example through `useCallback`),
|
|
30
|
+
* because a new identity refires the request for the current query.
|
|
31
|
+
*/
|
|
32
|
+
search: (q: string, signal: AbortSignal) => Promise<SearchResponse>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Status =
|
|
36
|
+
| { kind: "idle" }
|
|
37
|
+
| { kind: "loading" }
|
|
38
|
+
| { kind: "results"; response: SearchResponse }
|
|
39
|
+
| { kind: "failed" }
|
|
40
|
+
|
|
41
|
+
const DEBOUNCE_MS = 150
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* How long a request runs before the live region announces that the index is loading.
|
|
45
|
+
*/
|
|
46
|
+
const LOADING_ANNOUNCE_MS = 300
|
|
47
|
+
const MAX_QUERY_LENGTH = 200
|
|
48
|
+
const DEFAULT_CATEGORY = "Documentation"
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The site-relative link for a hit.
|
|
52
|
+
*/
|
|
53
|
+
export function hitHref(hit: Pick<SearchHit, "url" | "anchor">): string {
|
|
54
|
+
return hit.anchor === "" ? hit.url : `${hit.url}#${hit.anchor}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function statusText(status: Status): string {
|
|
58
|
+
if (status.kind === "failed") return "Search is unavailable. Try again in a moment."
|
|
59
|
+
|
|
60
|
+
if (status.kind === "loading") return "Loading the search index…"
|
|
61
|
+
|
|
62
|
+
if (status.kind === "idle") return ""
|
|
63
|
+
|
|
64
|
+
const { response } = status
|
|
65
|
+
|
|
66
|
+
if (!response.hits.length) return `No results for “${response.query}”.`
|
|
67
|
+
|
|
68
|
+
return response.hits.length === 1 ? "1 result." : `${response.hits.length} results.`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function highlighted(hit: SearchHit): ReactNode[] {
|
|
72
|
+
const parts: ReactNode[] = []
|
|
73
|
+
let cursor = 0
|
|
74
|
+
|
|
75
|
+
for (const [start, end] of hit.highlights) {
|
|
76
|
+
if (start < cursor) continue
|
|
77
|
+
|
|
78
|
+
parts.push(hit.snippet.slice(cursor, start), <mark key={start}>{hit.snippet.slice(start, end)}</mark>)
|
|
79
|
+
cursor = end
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
parts.push(hit.snippet.slice(cursor))
|
|
83
|
+
|
|
84
|
+
return parts
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function title(hit: SearchHit): string {
|
|
88
|
+
return hit.hierarchy
|
|
89
|
+
.slice(1)
|
|
90
|
+
.filter((entry): entry is string => entry !== null)
|
|
91
|
+
.join(" › ")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function SearchModal({ open, onClose, onNavigate, search }: SearchModalProps) {
|
|
95
|
+
const dialogRef = useRef<HTMLDialogElement>(null)
|
|
96
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
97
|
+
const baseID = useId()
|
|
98
|
+
const listboxID = `${baseID}-listbox`
|
|
99
|
+
const [query, setQuery] = useState("")
|
|
100
|
+
const [settled, setSettled] = useState<Status>({ kind: "idle" })
|
|
101
|
+
const [selected, setSelected] = useState(0)
|
|
102
|
+
const [loading, setLoading] = useState(false)
|
|
103
|
+
const trimmed = query.trim().slice(0, MAX_QUERY_LENGTH)
|
|
104
|
+
const debounced = useDebouncedValue(trimmed, DEBOUNCE_MS)
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const dialog = dialogRef.current
|
|
108
|
+
|
|
109
|
+
if (!dialog) return
|
|
110
|
+
|
|
111
|
+
if (open && !dialog.open) {
|
|
112
|
+
dialog.showModal()
|
|
113
|
+
inputRef.current?.focus()
|
|
114
|
+
} else if (!open && dialog.open) {
|
|
115
|
+
dialog.close()
|
|
116
|
+
}
|
|
117
|
+
}, [open])
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (debounced === "") return
|
|
121
|
+
|
|
122
|
+
// Aborting the earlier request is what keeps a late response from replacing newer results.
|
|
123
|
+
const controller = new AbortController()
|
|
124
|
+
const loadingTimer = setTimeout(() => setLoading(true), LOADING_ANNOUNCE_MS)
|
|
125
|
+
|
|
126
|
+
search(debounced, controller.signal).then(
|
|
127
|
+
(response) => {
|
|
128
|
+
if (controller.signal.aborted) return
|
|
129
|
+
|
|
130
|
+
clearTimeout(loadingTimer)
|
|
131
|
+
setLoading(false)
|
|
132
|
+
setSettled({ kind: "results", response })
|
|
133
|
+
setSelected(0)
|
|
134
|
+
},
|
|
135
|
+
() => {
|
|
136
|
+
if (controller.signal.aborted) return
|
|
137
|
+
|
|
138
|
+
clearTimeout(loadingTimer)
|
|
139
|
+
setLoading(false)
|
|
140
|
+
setSettled({ kind: "failed" })
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
clearTimeout(loadingTimer)
|
|
146
|
+
setLoading(false)
|
|
147
|
+
controller.abort()
|
|
148
|
+
}
|
|
149
|
+
}, [debounced, search])
|
|
150
|
+
|
|
151
|
+
// An empty query shows no results, whatever the last settled response was.
|
|
152
|
+
const status: Status = debounced === "" ? { kind: "idle" } : settled
|
|
153
|
+
const response = status.kind === "results" ? status.response : undefined
|
|
154
|
+
// The announcement reports a slow request while the last results stay on screen.
|
|
155
|
+
const announced: Status = loading && debounced !== "" ? { kind: "loading" } : status
|
|
156
|
+
|
|
157
|
+
const groups = useMemo(() => {
|
|
158
|
+
const hits = response?.hits ?? []
|
|
159
|
+
const byCategory = new Map<string, SearchHit[]>()
|
|
160
|
+
|
|
161
|
+
for (const hit of hits) {
|
|
162
|
+
const category = hit.hierarchy[0] ?? DEFAULT_CATEGORY
|
|
163
|
+
|
|
164
|
+
byCategory.set(category, [...(byCategory.get(category) ?? []), hit])
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Options are numbered in display order so the arrow keys follow what the reader sees.
|
|
168
|
+
let order = 0
|
|
169
|
+
|
|
170
|
+
return [...byCategory].map(([category, entries]) => ({
|
|
171
|
+
category,
|
|
172
|
+
entries: entries.map((hit) => ({ hit, order: order++ })),
|
|
173
|
+
}))
|
|
174
|
+
}, [response])
|
|
175
|
+
|
|
176
|
+
const ordered = groups.flatMap((group) => group.entries)
|
|
177
|
+
const optionID = (order: number) => `${baseID}-option-${order}`
|
|
178
|
+
|
|
179
|
+
function go(hit: SearchHit) {
|
|
180
|
+
const href = hitHref(hit)
|
|
181
|
+
|
|
182
|
+
onClose()
|
|
183
|
+
|
|
184
|
+
if (onNavigate) {
|
|
185
|
+
onNavigate(href)
|
|
186
|
+
} else {
|
|
187
|
+
globalThis.location.assign(href)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
|
192
|
+
// A key that commits or edits an IME composition is not a command.
|
|
193
|
+
if (event.nativeEvent.isComposing) return
|
|
194
|
+
|
|
195
|
+
if (!ordered.length) return
|
|
196
|
+
|
|
197
|
+
const last = ordered.length - 1
|
|
198
|
+
|
|
199
|
+
const next =
|
|
200
|
+
event.key === "ArrowDown"
|
|
201
|
+
? selected === last
|
|
202
|
+
? 0
|
|
203
|
+
: selected + 1
|
|
204
|
+
: event.key === "ArrowUp"
|
|
205
|
+
? selected === 0
|
|
206
|
+
? last
|
|
207
|
+
: selected - 1
|
|
208
|
+
: event.key === "Home"
|
|
209
|
+
? 0
|
|
210
|
+
: event.key === "End"
|
|
211
|
+
? last
|
|
212
|
+
: undefined
|
|
213
|
+
|
|
214
|
+
if (next !== undefined) {
|
|
215
|
+
event.preventDefault()
|
|
216
|
+
setSelected(next)
|
|
217
|
+
document.getElementById(optionID(next))?.scrollIntoView({ block: "nearest" })
|
|
218
|
+
} else if (event.key === "Enter") {
|
|
219
|
+
event.preventDefault()
|
|
220
|
+
|
|
221
|
+
// The hits on screen answer an earlier query until the debounce settles.
|
|
222
|
+
if (trimmed !== debounced) return
|
|
223
|
+
|
|
224
|
+
const entry = ordered[selected]
|
|
225
|
+
|
|
226
|
+
if (entry) {
|
|
227
|
+
go(entry.hit)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function onDialogClick(event: MouseEvent<HTMLDialogElement>) {
|
|
233
|
+
// A click whose target is the dialog element itself landed on the backdrop.
|
|
234
|
+
if (event.target === event.currentTarget) {
|
|
235
|
+
event.currentTarget.close()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function onLinkClick(event: MouseEvent<HTMLAnchorElement>, hit: SearchHit) {
|
|
240
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return
|
|
241
|
+
|
|
242
|
+
event.preventDefault()
|
|
243
|
+
go(hit)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<dialog
|
|
248
|
+
ref={dialogRef}
|
|
249
|
+
className="mw-search"
|
|
250
|
+
aria-label="Search the documentation"
|
|
251
|
+
onClose={onClose}
|
|
252
|
+
onClick={onDialogClick}
|
|
253
|
+
>
|
|
254
|
+
<form role="search" className="mw-search__form" onSubmit={(event) => event.preventDefault()}>
|
|
255
|
+
<input
|
|
256
|
+
ref={inputRef}
|
|
257
|
+
type="search"
|
|
258
|
+
className="mw-search__input"
|
|
259
|
+
role="combobox"
|
|
260
|
+
aria-label="Search the documentation"
|
|
261
|
+
aria-autocomplete="list"
|
|
262
|
+
aria-expanded={ordered.length > 0}
|
|
263
|
+
aria-controls={listboxID}
|
|
264
|
+
aria-activedescendant={ordered.length ? optionID(selected) : undefined}
|
|
265
|
+
autoComplete="off"
|
|
266
|
+
spellCheck={false}
|
|
267
|
+
maxLength={MAX_QUERY_LENGTH}
|
|
268
|
+
placeholder="Search the documentation"
|
|
269
|
+
value={query}
|
|
270
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
271
|
+
onKeyDown={onKeyDown}
|
|
272
|
+
/>
|
|
273
|
+
</form>
|
|
274
|
+
|
|
275
|
+
{response?.corrected !== undefined && (
|
|
276
|
+
<p className="mw-search__corrected">Showing results for “{response.corrected}”</p>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
<div id={listboxID} role="listbox" aria-label="Search results" className="mw-search__results">
|
|
280
|
+
{groups.map((group, groupIndex) => (
|
|
281
|
+
<div
|
|
282
|
+
key={group.category}
|
|
283
|
+
role="group"
|
|
284
|
+
aria-labelledby={`${baseID}-group-${groupIndex}`}
|
|
285
|
+
className="mw-search__group"
|
|
286
|
+
>
|
|
287
|
+
<div id={`${baseID}-group-${groupIndex}`} className="mw-search__category">
|
|
288
|
+
{group.category}
|
|
289
|
+
</div>
|
|
290
|
+
{group.entries.map(({ hit, order }) => (
|
|
291
|
+
// The option is the link itself: an option may not contain an interactive element, and
|
|
292
|
+
// `option` is an allowed role on `a[href]`, which keeps the URL for open-in-new-tab.
|
|
293
|
+
<a
|
|
294
|
+
key={order}
|
|
295
|
+
id={optionID(order)}
|
|
296
|
+
role="option"
|
|
297
|
+
aria-selected={order === selected}
|
|
298
|
+
href={hitHref(hit)}
|
|
299
|
+
tabIndex={-1}
|
|
300
|
+
className="mw-search__option"
|
|
301
|
+
onPointerMove={() => setSelected(order)}
|
|
302
|
+
onClick={(event) => onLinkClick(event, hit)}
|
|
303
|
+
>
|
|
304
|
+
<span className="mw-search__title">{title(hit)}</span>
|
|
305
|
+
{hit.snippet !== "" && <span className="mw-search__snippet">{highlighted(hit)}</span>}
|
|
306
|
+
</a>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
))}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<p aria-live="polite" className="mw-search__status">
|
|
313
|
+
{statusText(announced)}
|
|
314
|
+
</p>
|
|
315
|
+
</dialog>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* ---------------------------------------------------------------------------
|
|
2
|
+
* The docs search modal.
|
|
3
|
+
*
|
|
4
|
+
* The native <dialog> has zero padding, so the only click whose target is the dialog element itself is a click on
|
|
5
|
+
* the backdrop. The padding sits on the form, the results and the status line instead.
|
|
6
|
+
*
|
|
7
|
+
* The selected option is marked by an inset accent bar as well as a fill, so the selection holds 3:1 against the
|
|
8
|
+
* panel without depending on the fill alone. Colors, radii, spacing, type and motion read semantic tokens from
|
|
9
|
+
* tokens.css. The layout literals are the panel width and height caps, the 44px input height, the 3px selection
|
|
10
|
+
* bar and the backdrop scrim, for which no token exists.
|
|
11
|
+
* --------------------------------------------------------------------------- */
|
|
12
|
+
|
|
13
|
+
.mw-search {
|
|
14
|
+
width: min(40rem, 92vw);
|
|
15
|
+
max-height: 70vh;
|
|
16
|
+
margin: 12vh auto 0;
|
|
17
|
+
padding: 0;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
color: var(--color-text-primary);
|
|
20
|
+
font-family: var(--font-family-text);
|
|
21
|
+
font-size: var(--font-size-callout);
|
|
22
|
+
line-height: var(--line-height-body);
|
|
23
|
+
background: var(--color-background-raised);
|
|
24
|
+
border: 1px solid var(--control-border);
|
|
25
|
+
border-radius: var(--radius-panel);
|
|
26
|
+
box-shadow: var(--material-glass-shadow);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.mw-search[open] {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.mw-search::backdrop {
|
|
35
|
+
background: oklch(0% 0 264 / 0.5);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.mw-search__form {
|
|
39
|
+
padding: var(--space-5);
|
|
40
|
+
border-bottom: 1px solid var(--color-separator);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.mw-search__input {
|
|
44
|
+
width: 100%;
|
|
45
|
+
min-height: 44px;
|
|
46
|
+
padding: var(--space-3) var(--space-4);
|
|
47
|
+
color: var(--control-foreground);
|
|
48
|
+
font: inherit;
|
|
49
|
+
font-size: var(--font-size-body);
|
|
50
|
+
background: var(--control-background);
|
|
51
|
+
border: 1px solid var(--control-border);
|
|
52
|
+
border-radius: var(--radius-control);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.mw-search__input:focus-visible {
|
|
56
|
+
outline: 2px solid var(--color-accent);
|
|
57
|
+
outline-offset: 1px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.mw-search__corrected {
|
|
61
|
+
margin: 0;
|
|
62
|
+
padding: var(--space-3) var(--space-5) 0;
|
|
63
|
+
color: var(--color-text-secondary);
|
|
64
|
+
font-size: var(--font-size-footnote);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.mw-search__results {
|
|
68
|
+
flex: 1 1 auto;
|
|
69
|
+
min-height: 0;
|
|
70
|
+
padding: var(--space-3) var(--space-4);
|
|
71
|
+
overflow-y: auto;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.mw-search__category {
|
|
75
|
+
padding: var(--space-4) var(--space-4) var(--space-2);
|
|
76
|
+
color: var(--color-text-secondary);
|
|
77
|
+
font-size: var(--font-size-footnote);
|
|
78
|
+
font-weight: var(--font-weight-semibold);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.mw-search__option {
|
|
82
|
+
border-radius: var(--radius-control);
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
gap: var(--space-1);
|
|
86
|
+
padding: var(--space-3) var(--space-4);
|
|
87
|
+
color: inherit;
|
|
88
|
+
text-decoration: none;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.mw-search__option[aria-selected="true"] {
|
|
92
|
+
background: var(--color-background-sunken);
|
|
93
|
+
box-shadow: inset 3px 0 0 var(--color-accent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.mw-search__title {
|
|
97
|
+
font-size: var(--font-size-subheadline);
|
|
98
|
+
font-weight: var(--font-weight-semibold);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.mw-search__snippet {
|
|
102
|
+
color: var(--color-text-secondary);
|
|
103
|
+
font-size: var(--font-size-footnote);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.mw-search__snippet mark {
|
|
107
|
+
color: var(--color-text-primary);
|
|
108
|
+
font-weight: var(--font-weight-semibold);
|
|
109
|
+
background: var(--color-accent-background);
|
|
110
|
+
border-radius: var(--radius-tick);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.mw-search__status {
|
|
114
|
+
margin: 0;
|
|
115
|
+
padding: var(--space-3) var(--space-5);
|
|
116
|
+
color: var(--color-text-secondary);
|
|
117
|
+
font-size: var(--font-size-footnote);
|
|
118
|
+
border-top: 1px solid var(--color-separator);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* An empty live region stays rendered, because a region that appears with its text is often not announced. */
|
|
122
|
+
.mw-search__status:empty {
|
|
123
|
+
position: absolute;
|
|
124
|
+
width: 1px;
|
|
125
|
+
height: 1px;
|
|
126
|
+
padding: 0;
|
|
127
|
+
overflow: hidden;
|
|
128
|
+
border: 0;
|
|
129
|
+
clip-path: inset(50%);
|
|
130
|
+
white-space: nowrap;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
134
|
+
.mw-search[open] {
|
|
135
|
+
animation: mw-search-open var(--duration-standard) var(--easing-decelerate);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@keyframes mw-search-open {
|
|
140
|
+
from {
|
|
141
|
+
opacity: 0;
|
|
142
|
+
transform: translateY(calc(var(--space-4) * -1));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The record the docs build indexes and the response the search modal renders. The modal and the
|
|
7
|
+
* query code share these through `import type`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The number of hierarchy entries: the sidebar category, then `h1` through `h6`.
|
|
12
|
+
*/
|
|
13
|
+
export const HIERARCHY_DEPTH = 7
|
|
14
|
+
|
|
15
|
+
export interface SearchRecord {
|
|
16
|
+
/**
|
|
17
|
+
* The first 32 hex characters of `sha256(url + "#" + anchor + "\n" + position)`.
|
|
18
|
+
*/
|
|
19
|
+
id: string
|
|
20
|
+
/**
|
|
21
|
+
* Site-relative path without the anchor, such as `/docs/developers/reference/cli`.
|
|
22
|
+
*/
|
|
23
|
+
url: string
|
|
24
|
+
/**
|
|
25
|
+
* The heading id the record links to, or the empty string for the top of the page.
|
|
26
|
+
*/
|
|
27
|
+
anchor: string
|
|
28
|
+
/**
|
|
29
|
+
* Seven entries, `lvl0` through `lvl6`, with `null` below the record's own level.
|
|
30
|
+
*/
|
|
31
|
+
hierarchy: (string | null)[]
|
|
32
|
+
/**
|
|
33
|
+
* Aggregated text under the heading, or the empty string for a heading without body text.
|
|
34
|
+
*/
|
|
35
|
+
content: string
|
|
36
|
+
/**
|
|
37
|
+
* Depth of the deepest non-null hierarchy entry, 0 through 6.
|
|
38
|
+
*/
|
|
39
|
+
level: number
|
|
40
|
+
/**
|
|
41
|
+
* Zero-based order of the record on its page.
|
|
42
|
+
*/
|
|
43
|
+
position: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SearchHit {
|
|
47
|
+
url: string
|
|
48
|
+
anchor: string
|
|
49
|
+
hierarchy: (string | null)[]
|
|
50
|
+
/**
|
|
51
|
+
* At most 160 characters of content around the first match.
|
|
52
|
+
*/
|
|
53
|
+
snippet: string
|
|
54
|
+
/**
|
|
55
|
+
* Character ranges within `snippet` that matched a query token.
|
|
56
|
+
*/
|
|
57
|
+
highlights: [start: number, end: number][]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SearchResponse {
|
|
61
|
+
query: string
|
|
62
|
+
/**
|
|
63
|
+
* The text actually searched when a token was corrected against the vocabulary.
|
|
64
|
+
*/
|
|
65
|
+
corrected?: string
|
|
66
|
+
hits: SearchHit[]
|
|
67
|
+
}
|
package/lib/search.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file The docs search modal and its wire types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { hitHref, SearchModal } from "./search/SearchModal.tsx"
|
|
9
|
+
export type { SearchModalProps } from "./search/SearchModal.tsx"
|
|
10
|
+
export { HIERARCHY_DEPTH } from "./search/types.ts"
|
|
11
|
+
export type { SearchHit, SearchRecord, SearchResponse } from "./search/types.ts"
|
package/out/common/About.d.ts
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* resolver, the byte-range database and the FST prior. Prose about the product a host places above or beside the
|
|
8
|
-
* geocoder; it renders nothing live.
|
|
6
|
+
* `About` — the explainer for the browser geocoder: the neural model, the confidence colors, the gazetteer resolver, the byte-range database and the FST prior. It renders no live content.
|
|
9
7
|
*/
|
|
10
8
|
import type { ReactNode } from "react";
|
|
11
9
|
export interface AboutProps {
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Wraps the prose in its own `<details>` disclosure.
|
|
12
|
+
*
|
|
13
|
+
* Set false where the host is already the disclosure, because a summary repeating
|
|
14
|
+
* an About button's words is one control too many.
|
|
15
15
|
*
|
|
16
16
|
* @default true
|
|
17
17
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"About.d.ts","sourceRoot":"","sources":["../../lib/common/About.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"About.d.ts","sourceRoot":"","sources":["../../lib/common/About.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,WAAW,UAAU;IAC1B;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,KAAK,CAAC,EAAE,WAAkB,EAAE,GAAE,UAAe,GAAG,SAAS,CAkFxE"}
|
package/out/common/About.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
export function About({ collapsible = true } = {}) {
|
|
3
|
-
const body = (_jsxs("div", { className: "mw-about__body", children: [_jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Neural model" }), _jsxs("p", { children: ["A ~29M-parameter BERT-style encoder classifies each token into one of 33 BIO labels covering 16 address components (street, house number, unit, locality, region, postcode, venue, country, intersection, etc.). It runs entirely in your browser via ", _jsx("strong", { children: "onnxruntime-web" }), " (WebGPU with WASM SIMD fallback). The model ships as a 37.6 MB int8-quantized ONNX bundle; fp32 weights are also available for higher accuracy."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Confidence colors" }), _jsx("p", { children: "Every classified span carries a confidence score between 0 and 1. The colors in the results table give you a quick visual read:" }), _jsxs("ul", { className: "mw-about__legend", children: [_jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--high" }), _jsx("strong", { children: "Green" }), " (\u2265 0.8) \u2014 high confidence. The model is nearly certain."] }), _jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--mid" }), _jsx("strong", { children: "Amber" }), " (0.5\u20130.8) \u2014 moderate confidence. Worth verifying."] }), _jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--low" }), _jsx("strong", { children: "Red" }), " (< 0.5) \u2014 low confidence. The model is unsure \u2014 likely wrong."] })] }), _jsxs("p", { children: ["These are raw model scores. For calibrated probabilities, the library ships a per-locale isotonic calibrator (", _jsx("code", { children: "calibration.json" }), ") that maps the raw scores to well-calibrated probability estimates."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "WOF resolver" }), _jsxs("p", { children: ["After classification, the resolver queries a ", _jsx("strong", { children: "Who's On First" }), " (WOF) SQLite gazetteer to match the parsed components \u2014 locality, region, and postcode \u2014 against real-world places with coordinates. The cascade tries postcode first (most precise), then locality, then the raw input text. The result is a sorted list of candidate places with WOF IDs, placetypes, lat/lon, and bounding boxes."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Byte-range gazetteer" }), _jsxs("p", { children: ["The gazetteer is a multi-gigabyte SQLite file served from a public bucket. It loads via", " ", _jsx("strong", { children: "
|
|
3
|
+
const body = (_jsxs("div", { className: "mw-about__body", children: [_jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Neural model" }), _jsxs("p", { children: ["A ~29M-parameter BERT-style encoder classifies each token into one of 33 BIO labels covering 16 address components (street, house number, unit, locality, region, postcode, venue, country, intersection, etc.). It runs entirely in your browser via ", _jsx("strong", { children: "onnxruntime-web" }), " (WebGPU with WASM SIMD fallback). The model ships as a 37.6 MB int8-quantized ONNX bundle; fp32 weights are also available for higher accuracy."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Confidence colors" }), _jsx("p", { children: "Every classified span carries a confidence score between 0 and 1. The colors in the results table give you a quick visual read:" }), _jsxs("ul", { className: "mw-about__legend", children: [_jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--high" }), _jsx("strong", { children: "Green" }), " (\u2265 0.8) \u2014 high confidence. The model is nearly certain."] }), _jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--mid" }), _jsx("strong", { children: "Amber" }), " (0.5\u20130.8) \u2014 moderate confidence. Worth verifying."] }), _jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--low" }), _jsx("strong", { children: "Red" }), " (< 0.5) \u2014 low confidence. The model is unsure \u2014 likely wrong."] })] }), _jsxs("p", { children: ["These are raw model scores. For calibrated probabilities, the library ships a per-locale isotonic calibrator (", _jsx("code", { children: "calibration.json" }), ") that maps the raw scores to well-calibrated probability estimates."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "WOF resolver" }), _jsxs("p", { children: ["After classification, the resolver queries a ", _jsx("strong", { children: "Who's On First" }), " (WOF) SQLite gazetteer to match the parsed components \u2014 locality, region, and postcode \u2014 against real-world places with coordinates. The cascade tries postcode first (most precise), then locality, then the raw input text. The result is a sorted list of candidate places with WOF IDs, placetypes, lat/lon, and bounding boxes."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Byte-range gazetteer" }), _jsxs("p", { children: ["The gazetteer is a multi-gigabyte SQLite file served from a public bucket. It loads via", " ", _jsx("strong", { children: "sqlite-wasm" }), " over a read-only HTTP file system. It range-requests only the pages a lookup touches. Therefore, your browser fetches a few hundred kilobytes per session instead of the whole file."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "FST prior" }), _jsxs("p", { children: ["A ", _jsx("strong", { children: "finite-state transducer" }), " (FST) built from ~94K US place names runs before the neural classifier. It pre-annotates known locality, region, and postcode spans in the input \u2014 places like \"Springfield\" or \"Cook County\" that the FST can match exactly. These \"FST prior\" hits feed the classifier as strong hints, improving locality and region recall on addresses where the neural model alone might miss them. The FST is active whenever the selected release includes it."] })] })] }));
|
|
4
4
|
if (!collapsible)
|
|
5
5
|
return body;
|
|
6
6
|
return (_jsxs("details", { className: "mw-about", children: [_jsxs("summary", { className: "mw-about__summary", children: [_jsx("span", { className: "mw-about__icon", children: "\u2139\uFE0F" }), "About this geocoder"] }), body] }));
|
package/out/common/About.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"About.js","sourceRoot":"","sources":["../../lib/common/About.tsx"],"names":[],"mappings":";AAsBA,MAAM,UAAU,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,EAAE,GAAe,EAAE;IAC5D,MAAM,IAAI,GAAG,CACZ,eAAK,SAAS,EAAC,gBAAgB,aAC9B,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,6BAAkB,EACnD,kRAGmC,+CAAgC,wJAE/D,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,kCAAuB,EACxD,0JAGI,EACJ,cAAI,SAAS,EAAC,kBAAkB,aAC/B,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,yCAAyC,GAAG,EAC5D,qCAAsB,0EAClB,EACL,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,wCAAwC,GAAG,EAC3D,qCAAsB,oEAClB,EACL,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,wCAAwC,GAAG,EAC3D,mCAAoB,gFAChB,IACD,EACL,0IAEC,8CAA6B,4EAC1B,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,6BAAkB,EACnD,yEAC8C,8CAAoC,uVAI9E,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,qCAA0B,EAC3D,mHACyF,GAAG,EAC3F,
|
|
1
|
+
{"version":3,"file":"About.js","sourceRoot":"","sources":["../../lib/common/About.tsx"],"names":[],"mappings":";AAsBA,MAAM,UAAU,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,EAAE,GAAe,EAAE;IAC5D,MAAM,IAAI,GAAG,CACZ,eAAK,SAAS,EAAC,gBAAgB,aAC9B,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,6BAAkB,EACnD,kRAGmC,+CAAgC,wJAE/D,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,kCAAuB,EACxD,0JAGI,EACJ,cAAI,SAAS,EAAC,kBAAkB,aAC/B,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,yCAAyC,GAAG,EAC5D,qCAAsB,0EAClB,EACL,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,wCAAwC,GAAG,EAC3D,qCAAsB,oEAClB,EACL,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,wCAAwC,GAAG,EAC3D,mCAAoB,gFAChB,IACD,EACL,0IAEC,8CAA6B,4EAC1B,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,6BAAkB,EACnD,yEAC8C,8CAAoC,uVAI9E,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,qCAA0B,EAC3D,mHACyF,GAAG,EAC3F,2CAA4B,6LAEzB,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,0BAAe,EAChD,8BACG,uDAAwC,0cAKvC,IACK,IACL,CACN,CAAA;IAED,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAA;IAE7B,OAAO,CACN,mBAAS,SAAS,EAAC,UAAU,aAC5B,mBAAS,SAAS,EAAC,mBAAmB,aACrC,eAAM,SAAS,EAAC,gBAAgB,6BAAU,2BAEjC,EACT,IAAI,IACI,CACV,CAAA;AACF,CAAC"}
|
|
@@ -3,21 +3,12 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* this is the framework-agnostic equivalent so the components stay usable in any React app (Next,
|
|
8
|
-
* Remix, Docusaurus). It renders `fallback` on the server and the first client paint, then swaps to
|
|
9
|
-
* `children()` once mounted — keeping timers, clipboard, and dynamic imports off the server render.
|
|
10
|
-
*
|
|
11
|
-
* The mount flag comes from `useSyncExternalStore` with a constant snapshot pair — `false` on the
|
|
12
|
-
* server, `true` in the browser — which is the store-shaped statement of exactly this boundary. The
|
|
13
|
-
* earlier `useState(false)` + `useEffect(() => setMounted(true))` said the same thing with a
|
|
14
|
-
* render-cascade the react(set-state-in-effect) rule rightly flags: the second render was the
|
|
15
|
-
* mechanism, not an accident, and the store form gets the same second paint without a set-state.
|
|
6
|
+
* `ClientOnly` — a portable SSR boundary that renders `fallback` on the server and first client paint. It swaps to `children()` after mounting, keeping timers, clipboard and dynamic imports off the server render.
|
|
16
7
|
*/
|
|
17
8
|
import { type ReactNode } from "react";
|
|
18
9
|
export interface ClientOnlyProps {
|
|
19
10
|
/**
|
|
20
|
-
* Rendered once mounted in the browser
|
|
11
|
+
* Rendered once mounted in the browser, as a thunk so its browser-only imports never run on the server.
|
|
21
12
|
*/
|
|
22
13
|
children: () => ReactNode;
|
|
23
14
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientOnly.d.ts","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ClientOnly.d.ts","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAA;AAM5D,MAAM,WAAW,eAAe;IAC/B;;OAEG;IACH,QAAQ,EAAE,MAAM,SAAS,CAAA;IACzB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAe,EAAE,EAAE,eAAe,GAAG,SAAS,CAQpF"}
|
package/out/common/ClientOnly.js
CHANGED
|
@@ -3,21 +3,9 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* this is the framework-agnostic equivalent so the components stay usable in any React app (Next,
|
|
8
|
-
* Remix, Docusaurus). It renders `fallback` on the server and the first client paint, then swaps to
|
|
9
|
-
* `children()` once mounted — keeping timers, clipboard, and dynamic imports off the server render.
|
|
10
|
-
*
|
|
11
|
-
* The mount flag comes from `useSyncExternalStore` with a constant snapshot pair — `false` on the
|
|
12
|
-
* server, `true` in the browser — which is the store-shaped statement of exactly this boundary. The
|
|
13
|
-
* earlier `useState(false)` + `useEffect(() => setMounted(true))` said the same thing with a
|
|
14
|
-
* render-cascade the react(set-state-in-effect) rule rightly flags: the second render was the
|
|
15
|
-
* mechanism, not an accident, and the store form gets the same second paint without a set-state.
|
|
6
|
+
* `ClientOnly` — a portable SSR boundary that renders `fallback` on the server and first client paint. It swaps to `children()` after mounting, keeping timers, clipboard and dynamic imports off the server render.
|
|
16
7
|
*/
|
|
17
8
|
import { useSyncExternalStore } from "react";
|
|
18
|
-
/**
|
|
19
|
-
* The boundary never changes after mount, so nothing ever notifies.
|
|
20
|
-
*/
|
|
21
9
|
function subscribeNever() {
|
|
22
10
|
return () => { };
|
|
23
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientOnly.js","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ClientOnly.js","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAkB,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5D,SAAS,cAAc;IACtB,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;AAChB,CAAC;AAaD,MAAM,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,EAAmB;IACxE,MAAM,OAAO,GAAG,oBAAoB,CACnC,cAAc,EACd,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACX,CAAA;IAED,OAAO,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAA;AACvC,CAAC"}
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
import type { ReactNode } from "react";
|
|
10
10
|
export interface CopyButtonProps {
|
|
11
11
|
/**
|
|
12
|
-
* The text to copy.
|
|
12
|
+
* The text to copy.
|
|
13
|
+
*
|
|
14
|
+
* If a thunk, it's evaluated at click time (so callers can copy the latest value).
|
|
13
15
|
*/
|
|
14
16
|
value: string | (() => string);
|
|
15
17
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CopyButton.d.ts","sourceRoot":"","sources":["../../lib/common/CopyButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,MAAM,WAAW,eAAe;IAC/B
|
|
1
|
+
{"version":3,"file":"CopyButton.d.ts","sourceRoot":"","sources":["../../lib/common/CopyButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,MAAM,WAAW,eAAe;IAC/B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IAC9B;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;OAEG;IACH,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,wBAAgB,UAAU,CAAC,EAC1B,KAAK,EACL,KAAc,EACd,WAAwB,EACxB,SAAoB,EACpB,QAAQ,GACR,EAAE,eAAe,GAAG,SAAS,CAa7B"}
|