@mailwoman/react 9.3.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/fonts.css +59 -0
  2. package/lib/common/About.tsx +105 -0
  3. package/lib/common/KindBadge.tsx +2 -7
  4. package/lib/common/confidence-tiers.ts +50 -0
  5. package/lib/common/index.ts +22 -0
  6. package/lib/common/text-tokens.ts +88 -0
  7. package/lib/index.ts +10 -103
  8. package/lib/map/BackendControl.tsx +7 -3
  9. package/lib/map/CompareToggle.tsx +5 -4
  10. package/lib/map/{GeocoderDemo.tsx → Geocoder.tsx} +81 -61
  11. package/lib/map/GeocoderControls.tsx +352 -0
  12. package/lib/map/{DemoMap.tsx → MapCanvas.tsx} +15 -15
  13. package/lib/map/MapChipRow.tsx +77 -0
  14. package/lib/map/MapCompass.tsx +77 -0
  15. package/lib/map/MapControlStack.tsx +98 -0
  16. package/lib/map/MapFooter.tsx +115 -0
  17. package/lib/map/MapProgressBar.tsx +58 -0
  18. package/lib/map/MapSearchBar.tsx +68 -0
  19. package/lib/map/MapSheet.tsx +62 -0
  20. package/lib/map/PlaceMarker.tsx +1 -1
  21. package/lib/map/ResolvedPlaceLayers.tsx +3 -3
  22. package/lib/map/ResultCamera.tsx +1 -1
  23. package/lib/map/ResultPanel.tsx +2 -2
  24. package/lib/map/VersionPicker.tsx +3 -3
  25. package/lib/map/fake-runtime.ts +122 -0
  26. package/lib/map/index.ts +35 -16
  27. package/lib/map/map-debug.ts +51 -0
  28. package/lib/map/place-render.ts +3 -2
  29. package/lib/map/types.ts +58 -42
  30. package/lib/map/{useDemoGeocode.ts → useGeocode.ts} +8 -8
  31. package/lib/map/useMapBearing.ts +56 -0
  32. package/lib/map/useMapLabelPick.ts +123 -0
  33. package/lib/pipeline/CandidatePicker.tsx +1 -1
  34. package/lib/pipeline/ComponentTable.tsx +1 -2
  35. package/lib/pipeline/ConfidenceCell.tsx +7 -19
  36. package/lib/pipeline/FailureDiagnostic.tsx +56 -0
  37. package/lib/pipeline/ResolvedPlace.tsx +17 -4
  38. package/lib/pipeline/SpanHighlight.tsx +97 -0
  39. package/lib/pipeline/TimingPanel.tsx +65 -0
  40. package/lib/pipeline/TreeView.tsx +55 -0
  41. package/lib/pipeline/copy.ts +1 -1
  42. package/lib/pipeline/index.ts +36 -0
  43. package/lib/pipeline/types.ts +2 -75
  44. package/lib/pipeline/useParsePipeline.ts +31 -19
  45. package/lib/poi/index.ts +39 -0
  46. package/lib/poi/types.ts +2 -1
  47. package/lib/runtime/index.ts +16 -0
  48. package/lib/runtime/{useDemoRuntime.ts → useReleaseRuntime.ts} +22 -24
  49. package/out/common/About.d.ts +21 -0
  50. package/out/common/About.d.ts.map +1 -0
  51. package/out/common/About.js +8 -0
  52. package/out/common/About.js.map +1 -0
  53. package/out/common/KindBadge.d.ts +2 -9
  54. package/out/common/KindBadge.d.ts.map +1 -1
  55. package/out/common/KindBadge.js.map +1 -1
  56. package/out/common/confidence-tiers.d.ts +36 -0
  57. package/out/common/confidence-tiers.d.ts.map +1 -0
  58. package/out/common/confidence-tiers.js +42 -0
  59. package/out/common/confidence-tiers.js.map +1 -0
  60. package/out/common/index.d.ts +22 -0
  61. package/out/common/index.d.ts.map +1 -0
  62. package/out/common/index.js +15 -0
  63. package/out/common/index.js.map +1 -0
  64. package/out/common/text-tokens.d.ts +46 -0
  65. package/out/common/text-tokens.d.ts.map +1 -0
  66. package/out/common/text-tokens.js +52 -0
  67. package/out/common/text-tokens.js.map +1 -0
  68. package/out/index.d.ts +5 -51
  69. package/out/index.d.ts.map +1 -1
  70. package/out/index.js +4 -30
  71. package/out/index.js.map +1 -1
  72. package/out/map/BackendControl.d.ts.map +1 -1
  73. package/out/map/BackendControl.js +1 -1
  74. package/out/map/BackendControl.js.map +1 -1
  75. package/out/map/CompareToggle.d.ts +2 -2
  76. package/out/map/CompareToggle.d.ts.map +1 -1
  77. package/out/map/CompareToggle.js +1 -1
  78. package/out/map/CompareToggle.js.map +1 -1
  79. package/out/map/Geocoder.d.ts +67 -0
  80. package/out/map/Geocoder.d.ts.map +1 -0
  81. package/out/map/Geocoder.js +98 -0
  82. package/out/map/Geocoder.js.map +1 -0
  83. package/out/map/GeocoderControls.d.ts +81 -0
  84. package/out/map/GeocoderControls.d.ts.map +1 -0
  85. package/out/map/GeocoderControls.js +79 -0
  86. package/out/map/GeocoderControls.js.map +1 -0
  87. package/out/map/{DemoMap.d.ts → MapCanvas.d.ts} +15 -15
  88. package/out/map/MapCanvas.d.ts.map +1 -0
  89. package/out/map/{DemoMap.js → MapCanvas.js} +2 -2
  90. package/out/map/MapCanvas.js.map +1 -0
  91. package/out/map/MapChipRow.d.ts +47 -0
  92. package/out/map/MapChipRow.d.ts.map +1 -0
  93. package/out/map/MapChipRow.js +12 -0
  94. package/out/map/MapChipRow.js.map +1 -0
  95. package/out/map/MapCompass.d.ts +42 -0
  96. package/out/map/MapCompass.d.ts.map +1 -0
  97. package/out/map/MapCompass.js +11 -0
  98. package/out/map/MapCompass.js.map +1 -0
  99. package/out/map/MapControlStack.d.ts +57 -0
  100. package/out/map/MapControlStack.d.ts.map +1 -0
  101. package/out/map/MapControlStack.js +15 -0
  102. package/out/map/MapControlStack.js.map +1 -0
  103. package/out/map/MapFooter.d.ts +40 -0
  104. package/out/map/MapFooter.d.ts.map +1 -0
  105. package/out/map/MapFooter.js +51 -0
  106. package/out/map/MapFooter.js.map +1 -0
  107. package/out/map/MapProgressBar.d.ts +34 -0
  108. package/out/map/MapProgressBar.d.ts.map +1 -0
  109. package/out/map/MapProgressBar.js +8 -0
  110. package/out/map/MapProgressBar.js.map +1 -0
  111. package/out/map/MapSearchBar.d.ts +49 -0
  112. package/out/map/MapSearchBar.d.ts.map +1 -0
  113. package/out/map/MapSearchBar.js +6 -0
  114. package/out/map/MapSearchBar.js.map +1 -0
  115. package/out/map/MapSheet.d.ts +32 -0
  116. package/out/map/MapSheet.d.ts.map +1 -0
  117. package/out/map/MapSheet.js +33 -0
  118. package/out/map/MapSheet.js.map +1 -0
  119. package/out/map/OverlayLayers.js +1 -1
  120. package/out/map/PlaceMarker.d.ts +1 -1
  121. package/out/map/ResolvedPlaceLayers.d.ts +3 -3
  122. package/out/map/ResolvedPlaceLayers.js +1 -1
  123. package/out/map/ResultCamera.d.ts +1 -1
  124. package/out/map/ResultCamera.js +1 -1
  125. package/out/map/ResultPanel.d.ts +2 -2
  126. package/out/map/ResultPanel.d.ts.map +1 -1
  127. package/out/map/ResultPanel.js +1 -1
  128. package/out/map/ResultPanel.js.map +1 -1
  129. package/out/map/VersionPicker.d.ts +3 -3
  130. package/out/map/VersionPicker.d.ts.map +1 -1
  131. package/out/map/fake-runtime.d.ts +38 -0
  132. package/out/map/fake-runtime.d.ts.map +1 -0
  133. package/out/map/fake-runtime.js +107 -0
  134. package/out/map/fake-runtime.js.map +1 -0
  135. package/out/map/index.d.ts +27 -10
  136. package/out/map/index.d.ts.map +1 -1
  137. package/out/map/index.js +16 -6
  138. package/out/map/index.js.map +1 -1
  139. package/out/map/map-debug.d.ts +31 -0
  140. package/out/map/map-debug.d.ts.map +1 -0
  141. package/out/map/map-debug.js +31 -0
  142. package/out/map/map-debug.js.map +1 -0
  143. package/out/map/place-render.d.ts +2 -2
  144. package/out/map/place-render.d.ts.map +1 -1
  145. package/out/map/place-render.js +1 -1
  146. package/out/map/place-render.js.map +1 -1
  147. package/out/map/types.d.ts +60 -42
  148. package/out/map/types.d.ts.map +1 -1
  149. package/out/map/types.js +5 -5
  150. package/out/map/{useDemoGeocode.d.ts → useGeocode.d.ts} +9 -9
  151. package/out/map/useGeocode.d.ts.map +1 -0
  152. package/out/map/{useDemoGeocode.js → useGeocode.js} +4 -4
  153. package/out/map/useGeocode.js.map +1 -0
  154. package/out/map/useMapBearing.d.ts +28 -0
  155. package/out/map/useMapBearing.d.ts.map +1 -0
  156. package/out/map/useMapBearing.js +35 -0
  157. package/out/map/useMapBearing.js.map +1 -0
  158. package/out/map/useMapLabelPick.d.ts +23 -0
  159. package/out/map/useMapLabelPick.d.ts.map +1 -0
  160. package/out/map/useMapLabelPick.js +104 -0
  161. package/out/map/useMapLabelPick.js.map +1 -0
  162. package/out/pipeline/CandidatePicker.d.ts +1 -1
  163. package/out/pipeline/CandidatePicker.d.ts.map +1 -1
  164. package/out/pipeline/CandidatePicker.js +1 -1
  165. package/out/pipeline/CandidatePicker.js.map +1 -1
  166. package/out/pipeline/ComponentTable.d.ts +1 -1
  167. package/out/pipeline/ComponentTable.d.ts.map +1 -1
  168. package/out/pipeline/ComponentTable.js.map +1 -1
  169. package/out/pipeline/ConfidenceCell.d.ts +2 -2
  170. package/out/pipeline/ConfidenceCell.d.ts.map +1 -1
  171. package/out/pipeline/ConfidenceCell.js +4 -16
  172. package/out/pipeline/ConfidenceCell.js.map +1 -1
  173. package/out/pipeline/FailureDiagnostic.d.ts +15 -0
  174. package/out/pipeline/FailureDiagnostic.d.ts.map +1 -0
  175. package/out/pipeline/FailureDiagnostic.js +21 -0
  176. package/out/pipeline/FailureDiagnostic.js.map +1 -0
  177. package/out/pipeline/PipelineExplorer.js +1 -1
  178. package/out/pipeline/QueryForm.js +1 -1
  179. package/out/pipeline/ResolvedPlace.d.ts +1 -1
  180. package/out/pipeline/ResolvedPlace.d.ts.map +1 -1
  181. package/out/pipeline/ResolvedPlace.js +4 -10
  182. package/out/pipeline/ResolvedPlace.js.map +1 -1
  183. package/out/pipeline/SpanHighlight.d.ts +23 -0
  184. package/out/pipeline/SpanHighlight.d.ts.map +1 -0
  185. package/out/pipeline/SpanHighlight.js +31 -0
  186. package/out/pipeline/SpanHighlight.js.map +1 -0
  187. package/out/pipeline/TimingPanel.d.ts +16 -0
  188. package/out/pipeline/TimingPanel.d.ts.map +1 -0
  189. package/out/pipeline/TimingPanel.js +20 -0
  190. package/out/pipeline/TimingPanel.js.map +1 -0
  191. package/out/pipeline/TreeView.d.ts +17 -0
  192. package/out/pipeline/TreeView.d.ts.map +1 -0
  193. package/out/pipeline/TreeView.js +15 -0
  194. package/out/pipeline/TreeView.js.map +1 -0
  195. package/out/pipeline/copy.d.ts +1 -1
  196. package/out/pipeline/copy.d.ts.map +1 -1
  197. package/out/pipeline/index.d.ts +33 -0
  198. package/out/pipeline/index.d.ts.map +1 -0
  199. package/out/pipeline/index.js +21 -0
  200. package/out/pipeline/index.js.map +1 -0
  201. package/out/pipeline/types.d.ts +2 -68
  202. package/out/pipeline/types.d.ts.map +1 -1
  203. package/out/pipeline/types.js +1 -1
  204. package/out/pipeline/useParsePipeline.d.ts +7 -3
  205. package/out/pipeline/useParsePipeline.d.ts.map +1 -1
  206. package/out/pipeline/useParsePipeline.js +6 -2
  207. package/out/pipeline/useParsePipeline.js.map +1 -1
  208. package/out/poi/LiveResultsBlock.js +1 -1
  209. package/out/poi/POIExplorer.js +1 -1
  210. package/out/poi/SubjectPanel.js +1 -1
  211. package/out/poi/index.d.ts +23 -0
  212. package/out/poi/index.d.ts.map +1 -0
  213. package/out/poi/index.js +15 -0
  214. package/out/poi/index.js.map +1 -0
  215. package/out/poi/runtime.d.ts.map +1 -1
  216. package/out/poi/types.d.ts +2 -1
  217. package/out/poi/types.d.ts.map +1 -1
  218. package/out/runtime/index.d.ts +9 -0
  219. package/out/runtime/index.d.ts.map +1 -0
  220. package/out/runtime/index.js +8 -0
  221. package/out/runtime/index.js.map +1 -0
  222. package/out/runtime/{useDemoRuntime.d.ts → useReleaseRuntime.d.ts} +18 -20
  223. package/out/runtime/useReleaseRuntime.d.ts.map +1 -0
  224. package/out/runtime/{useDemoRuntime.js → useReleaseRuntime.js} +4 -4
  225. package/out/runtime/useReleaseRuntime.js.map +1 -0
  226. package/package.json +222 -31
  227. package/styleframe.config.ts +329 -0
  228. package/styles.css +1995 -663
  229. package/tokens/index.css +116 -0
  230. package/tokens/tokens.json +528 -0
  231. package/tokens/tokens.resolver.json +185 -0
  232. package/lib/map/DemoControls.tsx +0 -194
  233. package/out/map/DemoControls.d.ts +0 -64
  234. package/out/map/DemoControls.d.ts.map +0 -1
  235. package/out/map/DemoControls.js +0 -30
  236. package/out/map/DemoControls.js.map +0 -1
  237. package/out/map/DemoMap.d.ts.map +0 -1
  238. package/out/map/DemoMap.js.map +0 -1
  239. package/out/map/GeocoderDemo.d.ts +0 -57
  240. package/out/map/GeocoderDemo.d.ts.map +0 -1
  241. package/out/map/GeocoderDemo.js +0 -98
  242. package/out/map/GeocoderDemo.js.map +0 -1
  243. package/out/map/useDemoGeocode.d.ts.map +0 -1
  244. package/out/map/useDemoGeocode.js.map +0 -1
  245. package/out/runtime/useDemoRuntime.d.ts.map +0 -1
  246. package/out/runtime/useDemoRuntime.js.map +0 -1
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `useMapLabelPick` — clicking a place label on the map searches for it, the way the reference map apps behave.
7
+ *
8
+ * A label on a map looks like a link, so a visitor clicks it. Without this the click reaches the map's pan handler
9
+ * and nothing happens, which reads as the label being decoration.
10
+ *
11
+ * It reads the label's OWN name from the rendered feature rather than reverse-geocoding the click point: the name
12
+ * is what the visitor pointed at, and a lookup by coordinate answers whatever is nearest instead, which on a dense
13
+ * basemap is regularly not the thing under the cursor.
14
+ *
15
+ * THE QUERY IS SCOPED, AND THE HOVER QUERY IS THROTTLED. `queryRenderedFeatures` with no `layers` walks the whole
16
+ * style: measured at 64.3 ms per call returning 4,819 features over the 79-layer basemap at zoom 14 in Manhattan,
17
+ * against 4.9 ms and 44 features scoped to that style's 11 label layers. At one call per pointer move the unscoped
18
+ * form is the map's whole frame budget, so the layer list is resolved once per style and the hover query runs at
19
+ * most once per animation frame. The click query is not throttled — there is one of those per click.
20
+ */
21
+
22
+ import { useEffect, useEffectEvent } from "react"
23
+ import type { MapInstance, MapLayerMouseEvent } from "react-map-gl/maplibre"
24
+
25
+ /**
26
+ * The layers whose features carry a place name. Protomaps names its label layers `<theme>_label` and its settlement
27
+ * layers `places_*`; anything else in the style is geometry, not a label a visitor can read and point at.
28
+ */
29
+ const LABEL_LAYER = /_label|^places_/
30
+
31
+ /**
32
+ * Properties a label carries its text under, in the order they are trusted. `name` is protomaps' own; the localized
33
+ * variants appear on styles built for a specific script.
34
+ */
35
+ const NAME_KEYS = ["name", "name:en", "name_en"] as const
36
+
37
+ /**
38
+ * The style's label layers, by id.
39
+ *
40
+ * A style with none answers an EMPTY ARRAY, and the caller must treat that as "no labels to pick" rather than passing
41
+ * it to `queryRenderedFeatures` — an empty `layers` option is not the same as an absent one there, and the difference
42
+ * between "this style has no labels" and "query everything" is the 64 ms this hook exists to avoid.
43
+ */
44
+ function labelLayerIDs(map: MapInstance): string[] {
45
+ const layers = map.getStyle()?.layers ?? []
46
+
47
+ return layers.map((layer) => layer.id).filter((id) => LABEL_LAYER.test(id))
48
+ }
49
+
50
+ function labelNameAt(map: MapInstance, point: MapLayerMouseEvent["point"], layers: string[]): string | null {
51
+ if (!layers.length) return null
52
+
53
+ // `queryRenderedFeatures` answers in paint order with the topmost first, which is the label drawn over the others
54
+ // and therefore the one a click landed on.
55
+ for (const feature of map.queryRenderedFeatures(point, { layers })) {
56
+ for (const key of NAME_KEYS) {
57
+ const value = feature.properties?.[key]
58
+
59
+ if (typeof value === "string" && value.trim()) return value
60
+ }
61
+ }
62
+
63
+ return null
64
+ }
65
+
66
+ export function useMapLabelPick(map: MapInstance | null, onPick: (name: string) => void): void {
67
+ // The subscription depends on the MAP alone. `useGeocode` returns a fresh object every render, so a callback built
68
+ // from it is new every render too — with `onPick` in the dependency list these map listeners were torn down and
69
+ // re-added on every keystroke in the search field. `useEffectEvent` is the shape for exactly this: an event
70
+ // handler that always sees the latest props without being a reactive dependency.
71
+ const pick = useEffectEvent((name: string) => onPick(name))
72
+
73
+ useEffect(() => {
74
+ if (!map) return
75
+
76
+ // Recomputed when the style swaps (a theme change, a version switch) and not once per pointer move.
77
+ let layers = labelLayerIDs(map)
78
+
79
+ const readLayers = () => {
80
+ layers = labelLayerIDs(map)
81
+ }
82
+
83
+ const onClick = (event: MapLayerMouseEvent) => {
84
+ const name = labelNameAt(map, event.point, layers)
85
+
86
+ if (name) {
87
+ pick(name)
88
+ }
89
+ }
90
+
91
+ let frame = 0
92
+
93
+ const onMove = (event: MapLayerMouseEvent) => {
94
+ if (frame) return
95
+
96
+ frame = requestAnimationFrame(() => {
97
+ frame = 0
98
+
99
+ const canvas = map.getCanvas()
100
+
101
+ // The drag cursor belongs to the pan gesture; overriding it mid-drag would fight the map for the pointer.
102
+ if (canvas.style.cursor === "grabbing") return
103
+
104
+ canvas.style.cursor = labelNameAt(map, event.point, layers) ? "pointer" : ""
105
+ })
106
+ }
107
+
108
+ map.on("styledata", readLayers)
109
+ map.on("click", onClick)
110
+ map.on("mousemove", onMove)
111
+
112
+ return () => {
113
+ if (frame) {
114
+ cancelAnimationFrame(frame)
115
+ }
116
+
117
+ map.off("styledata", readLayers)
118
+ map.off("click", onClick)
119
+ map.off("mousemove", onMove)
120
+ map.getCanvas().style.cursor = ""
121
+ }
122
+ }, [map])
123
+ }
@@ -7,10 +7,10 @@
7
7
  * Presentational; the selected index is owned by the caller (the pipeline hook).
8
8
  */
9
9
 
10
+ import type { ResolvedPlaceView } from "@mailwoman/core/pipeline/client-result"
10
11
  import type { ReactNode } from "react"
11
12
 
12
13
  import { cx } from "#common/cx"
13
- import type { ResolvedPlaceView } from "#pipeline/types"
14
14
 
15
15
  export interface CandidatePickerProps {
16
16
  candidates: ResolvedPlaceView[]
@@ -6,10 +6,9 @@
6
6
  * `ComponentTable` — the tag / value / confidence table of decoded components. Presentational.
7
7
  */
8
8
 
9
+ import type { ParsedComponent } from "@mailwoman/core/pipeline/client-result"
9
10
  import type { ReactNode } from "react"
10
11
 
11
- import type { ParsedComponent } from "#pipeline/types"
12
-
13
12
  import { ConfidenceCell } from "./ConfidenceCell.tsx"
14
13
 
15
14
  export interface ComponentTableProps {
@@ -3,28 +3,13 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `ConfidenceCell` — a compact confidence bar + value for the component table. Tiers the fill colour
7
- * at 0.8 / 0.5. Presentational.
6
+ * `ConfidenceCell` — a compact confidence bar + value for the component table. The fill colour follows the shared
7
+ * confidence tiers. Presentational.
8
8
  */
9
9
 
10
+ import { confidenceTierOrMid } from "#common/confidence-tiers"
10
11
  import { cx } from "#common/cx"
11
12
 
12
- /**
13
- * At or above this the cell is drawn as high-confidence. Presentation only — see docs/src/shared/confidence-tiers.ts.
14
- */
15
- const HIGH_CONFIDENCE_MIN = 0.8
16
-
17
- /**
18
- * At or above this the cell is drawn as medium-confidence; below it, low.
19
- */
20
- const MID_CONFIDENCE_MIN = 0.5
21
-
22
- function tier(confidence?: number): "high" | "mid" | "low" {
23
- if (confidence == null) return "mid"
24
-
25
- return confidence >= HIGH_CONFIDENCE_MIN ? "high" : confidence >= MID_CONFIDENCE_MIN ? "mid" : "low"
26
- }
27
-
28
13
  export interface ConfidenceCellProps {
29
14
  confidence?: number
30
15
  }
@@ -36,7 +21,10 @@ export function ConfidenceCell({ confidence }: ConfidenceCellProps) {
36
21
 
37
22
  return (
38
23
  <div className="mw-conf">
39
- <div className={cx("mw-conf__bar", `mw-conf__bar--${tier(confidence)}`)} style={{ width: `${pct}%` }} />
24
+ <div
25
+ className={cx("mw-conf__bar", `mw-conf__bar--${confidenceTierOrMid(confidence)}`)}
26
+ style={{ width: `${pct}%` }}
27
+ />
40
28
  <span className="mw-conf__value">{confidence.toFixed(2)}</span>
41
29
  </div>
42
30
  )
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `FailureDiagnostic` — what to say when nothing resolved. It reads the parsed components and names the missing
7
+ * piece a person can add (a city, a state), so the answer is a next step rather than "no hit".
8
+ */
9
+
10
+ import type { ParsedComponent } from "@mailwoman/core/pipeline/client-result"
11
+ import type { ReactNode } from "react"
12
+
13
+ export interface FailureDiagnosticProps {
14
+ nodes: ParsedComponent[]
15
+ }
16
+
17
+ export function FailureDiagnostic({ nodes }: FailureDiagnosticProps): ReactNode {
18
+ const hasLocality = nodes.some((n) => n.tag === "locality")
19
+ const hasPostcode = nodes.some((n) => n.tag === "postcode")
20
+ const hasRegion = nodes.some((n) => n.tag === "region")
21
+
22
+ const hints: string[] = []
23
+
24
+ if (!hasLocality && !hasPostcode) {
25
+ hints.push(
26
+ "The parser found no city or postcode in this input. Try adding one — e.g. append ', Chicago, IL 60613'."
27
+ )
28
+ }
29
+
30
+ if (hasPostcode && !hasLocality) {
31
+ hints.push(
32
+ "Only a postcode was extracted. WOF ships placeholder coordinates (0, 0) for about 22% of US postcodes, and the cascade drops those."
33
+ )
34
+ }
35
+
36
+ if (hasLocality && !hasRegion) {
37
+ hints.push(
38
+ "No state in the parse. US localities share names across states (Springfield, Portland, …); add a state to disambiguate."
39
+ )
40
+ }
41
+
42
+ if (!hints.length) {
43
+ hints.push("The parsed components look reasonable, but the gazetteer does not index this entry.")
44
+ }
45
+
46
+ return (
47
+ <div className="mw-failure">
48
+ <h2>No gazetteer hit</h2>
49
+ <ul>
50
+ {hints.map((hint, i) => (
51
+ <li key={i}>{hint}</li>
52
+ ))}
53
+ </ul>
54
+ </div>
55
+ )
56
+ }
@@ -7,10 +7,9 @@
7
7
  * optional dual-role note. Presentational.
8
8
  */
9
9
 
10
+ import type { DualRoleView, ResolvedPlaceView } from "@mailwoman/core/pipeline/client-result"
10
11
  import { Fragment, type ReactNode } from "react"
11
12
 
12
- import type { DualRoleView, ResolvedPlaceView } from "#pipeline/types"
13
-
14
13
  export interface ResolvedPlaceProps {
15
14
  place: ResolvedPlaceView
16
15
  dualRoles?: DualRoleView[]
@@ -25,8 +24,22 @@ export function ResolvedPlace({ place, dualRoles }: ResolvedPlaceProps): ReactNo
25
24
  <dd>{place.name}</dd>
26
25
  <dt>placetype</dt>
27
26
  <dd>{place.placetype}</dd>
28
- <dt>WOF id</dt>
29
- <dd>{place.id}</dd>
27
+ {place.tier ? (
28
+ // A street-tier answer is not a gazetteer place: it carries a precision instead of an id.
29
+ <>
30
+ <dt>precision</dt>
31
+ <dd>
32
+ {place.tier === "address_point"
33
+ ? "📍 exact address point (≤10 m)"
34
+ : `≈ interpolated · ±${place.uncertaintyM ?? "?"} m`}
35
+ </dd>
36
+ </>
37
+ ) : (
38
+ <>
39
+ <dt>WOF id</dt>
40
+ <dd>{place.id}</dd>
41
+ </>
42
+ )}
30
43
  <dt>coords</dt>
31
44
  <dd>
32
45
  {place.lat.toFixed(4)}, {place.lon.toFixed(4)}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `SpanHighlight` — a displaCy-style span ribbon over the raw input: each tagged span is a column with the text on
7
+ * top, tinted by its confidence tier, and the tag beneath. Gaps (delimiters, unparsed characters) fall through as
8
+ * plain text, so a dropped span is visible as a literal break in the colour.
9
+ */
10
+
11
+ import type { ParsedComponent } from "@mailwoman/core/pipeline/client-result"
12
+ import type { ReactNode } from "react"
13
+
14
+ import { confidenceTierOrMid } from "#common/confidence-tiers"
15
+ import { shortestSpanOwners } from "#common/text-tokens"
16
+
17
+ export interface SpanHighlightProps {
18
+ /**
19
+ * The raw text handed to the parser — `nodes[].start/end` index into this.
20
+ */
21
+ input: string
22
+ /**
23
+ * Flattened parse nodes; only those with numeric `start`/`end` are rendered.
24
+ */
25
+ nodes: ParsedComponent[]
26
+ }
27
+
28
+ interface Segment {
29
+ text: string
30
+ node: ParsedComponent | null
31
+ }
32
+
33
+ type Span = ParsedComponent & { start: number; end: number }
34
+
35
+ export function SpanHighlight({ input, nodes }: SpanHighlightProps): ReactNode {
36
+ if (!input) return null
37
+
38
+ // Keep only well-formed spans that index into the input.
39
+ const spans = nodes.filter(
40
+ (n): n is Span =>
41
+ typeof n.start === "number" &&
42
+ typeof n.end === "number" &&
43
+ n.start >= 0 &&
44
+ n.end > n.start &&
45
+ n.end <= input.length
46
+ )
47
+
48
+ if (!spans.length) return null
49
+
50
+ // Per-character owner: the most specific (shortest) span covering it, the same rule the word-level panels apply
51
+ // per word. The leaf always wins whatever nesting the tree hands over, so every character renders once.
52
+ const characters = Array.from({ length: input.length }, (_, i) => ({ start: i, end: i + 1 }))
53
+ const owner = shortestSpanOwners(characters, spans)
54
+
55
+ // Coalesce runs of the same owner into segments.
56
+ const segments: Segment[] = []
57
+ let from = 0
58
+
59
+ for (let i = 1; i <= input.length; i++) {
60
+ if (i === input.length || owner[i] !== owner[from]) {
61
+ const ownerIndex = owner[from] ?? -1
62
+
63
+ segments.push({ text: input.slice(from, i), node: ownerIndex === -1 ? null : (spans[ownerIndex] ?? null) })
64
+
65
+ from = i
66
+ }
67
+ }
68
+
69
+ return (
70
+ <div className="mw-spans">
71
+ <div className="mw-spans__legend">
72
+ <span>confidence</span>
73
+ <span className="mw-spans__swatch mw-spans__swatch--low" /> low
74
+ <span className="mw-spans__swatch mw-spans__swatch--mid" /> mid
75
+ <span className="mw-spans__swatch mw-spans__swatch--high" /> high
76
+ </div>
77
+ <div className="mw-spans__track">
78
+ {segments.map((seg, i) =>
79
+ seg.node ? (
80
+ <span
81
+ key={i}
82
+ className={`mw-spans__seg mw-spans__seg--${confidenceTierOrMid(seg.node.confidence)}`}
83
+ title={`${seg.node.tag}${seg.node.confidence != null ? ` · ${seg.node.confidence.toFixed(2)}` : ""}`}
84
+ >
85
+ <span className="mw-spans__text">{seg.text}</span>
86
+ <span className="mw-spans__tag">{seg.node.tag}</span>
87
+ </span>
88
+ ) : (
89
+ <span key={i} className="mw-spans__gap">
90
+ {seg.text}
91
+ </span>
92
+ )
93
+ )}
94
+ </div>
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `TimingPanel` — the per-stage timing breakdown: a stacked bar sized by each stage's wall-clock, plus a legend.
7
+ * Stage colours are distinct hues, never the confidence red/amber/green, because they encode the pipeline stage and
8
+ * not quality.
9
+ */
10
+
11
+ import type { StageTiming } from "@mailwoman/core/pipeline/client-result"
12
+ import type { ReactNode } from "react"
13
+
14
+ export interface TimingPanelProps {
15
+ timing: StageTiming
16
+ }
17
+
18
+ const STAGES: ReadonlyArray<{ key: keyof StageTiming; label: string }> = [
19
+ { key: "shape", label: "shape + kind" },
20
+ { key: "classify", label: "classify" },
21
+ { key: "resolve", label: "resolve" },
22
+ ]
23
+
24
+ function formatMilliseconds(ms: number): string {
25
+ return ms >= 100 ? `${Math.round(ms)} ms` : `${ms.toFixed(1)} ms`
26
+ }
27
+
28
+ export function TimingPanel({ timing }: TimingPanelProps): ReactNode {
29
+ const present = STAGES.flatMap((stage) => {
30
+ const ms = timing[stage.key]
31
+
32
+ return typeof ms === "number" ? [{ ...stage, ms }] : []
33
+ })
34
+
35
+ const total = present.reduce((sum, stage) => sum + stage.ms, 0)
36
+
37
+ if (total <= 0) return null
38
+
39
+ return (
40
+ <div className="mw-timing">
41
+ <div className="mw-timing__heading">
42
+ Timing <span className="mw-timing__total">{formatMilliseconds(total)} total</span>
43
+ </div>
44
+ <div className="mw-timing__bar">
45
+ {present.map((stage) => (
46
+ // The rendered width is floored so a sub-millisecond stage stays visible as a sliver.
47
+ <div
48
+ key={stage.key}
49
+ className={`mw-timing__seg mw-timing__seg--${stage.key}`}
50
+ style={{ width: `${Math.max((stage.ms / total) * 100, 1.5)}%` }}
51
+ title={`${stage.label}: ${formatMilliseconds(stage.ms)}`}
52
+ />
53
+ ))}
54
+ </div>
55
+ <div className="mw-timing__legend">
56
+ {present.map((stage) => (
57
+ <span key={stage.key} className="mw-timing__item">
58
+ <span className={`mw-timing__swatch mw-timing__seg--${stage.key}`} />
59
+ {stage.label} <span className="mw-timing__ms">{formatMilliseconds(stage.ms)}</span>
60
+ </span>
61
+ ))}
62
+ </div>
63
+ </div>
64
+ )
65
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `TreeView` — the parse's containment tree as nested lists with a guide rail per level. The tag tint follows the
7
+ * confidence tier; the value and score render in monospace.
8
+ */
9
+
10
+ import type { ReactNode } from "react"
11
+
12
+ import { confidenceTierOrMid } from "#common/confidence-tiers"
13
+
14
+ interface TreeNode {
15
+ tag?: string
16
+ value?: unknown
17
+ confidence?: number
18
+ children?: TreeNode[]
19
+ }
20
+
21
+ export interface TreeViewProps {
22
+ /**
23
+ * The parser's `AddressTree` (`result.tree`): the view reads `.roots` and recurses `.children`.
24
+ */
25
+ tree: unknown
26
+ }
27
+
28
+ function renderNode(node: TreeNode, path: string): ReactNode {
29
+ if (typeof node.tag !== "string") return null
30
+
31
+ const kids = Array.isArray(node.children) ? node.children : []
32
+
33
+ return (
34
+ <li key={path} className="mw-tree__node">
35
+ <span className="mw-tree__row">
36
+ <span className={`mw-tree__tag mw-tree__tag--${confidenceTierOrMid(node.confidence)}`}>{node.tag}</span>
37
+ {node.value != null && String(node.value) !== "" ? (
38
+ <span className="mw-tree__value">{String(node.value)}</span>
39
+ ) : null}
40
+ {typeof node.confidence === "number" ? (
41
+ <span className="mw-tree__conf">{node.confidence.toFixed(2)}</span>
42
+ ) : null}
43
+ </span>
44
+ {kids.length ? <ul className="mw-tree__children">{kids.map((c, i) => renderNode(c, `${path}.${i}`))}</ul> : null}
45
+ </li>
46
+ )
47
+ }
48
+
49
+ export function TreeView({ tree }: TreeViewProps): ReactNode {
50
+ const roots = (tree as { roots?: unknown[] } | null | undefined)?.roots
51
+
52
+ if (!Array.isArray(roots) || !roots.length) return null
53
+
54
+ return <ul className="mw-tree mw-tree__children">{(roots as TreeNode[]).map((n, i) => renderNode(n, String(i)))}</ul>
55
+ }
@@ -7,7 +7,7 @@
7
7
  * pretty-printed. Pure; shared by the explorer's copy button.
8
8
  */
9
9
 
10
- import type { ParseResult, ResolvedPlaceView } from "#pipeline/types"
10
+ import type { ParseResult, ResolvedPlaceView } from "@mailwoman/core/pipeline/client-result"
11
11
 
12
12
  export function buildParsePayload(result: ParseResult, selected: ResolvedPlaceView | null): string {
13
13
  return JSON.stringify(
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file Pipeline explorer components, visualizers, hooks, and contracts.
6
+ */
7
+
8
+ export { About } from "../common/About.tsx"
9
+ export { FailureDiagnostic } from "./FailureDiagnostic.tsx"
10
+ export type { FailureDiagnosticProps } from "./FailureDiagnostic.tsx"
11
+ export { SpanHighlight } from "./SpanHighlight.tsx"
12
+ export type { SpanHighlightProps } from "./SpanHighlight.tsx"
13
+ export { TimingPanel } from "./TimingPanel.tsx"
14
+ export type { TimingPanelProps } from "./TimingPanel.tsx"
15
+ export { TreeView } from "./TreeView.tsx"
16
+ export type { TreeViewProps } from "./TreeView.tsx"
17
+
18
+ export { CandidatePicker } from "./CandidatePicker.tsx"
19
+ export type { CandidatePickerProps } from "./CandidatePicker.tsx"
20
+ export { ComponentTable } from "./ComponentTable.tsx"
21
+ export type { ComponentTableProps } from "./ComponentTable.tsx"
22
+ export { ConfidenceCell } from "./ConfidenceCell.tsx"
23
+ export type { ConfidenceCellProps } from "./ConfidenceCell.tsx"
24
+ export { buildParsePayload } from "#pipeline/copy"
25
+ export { PIPELINE_DEFAULT_ADDRESS, PIPELINE_PRESETS } from "#pipeline/presets"
26
+ export { PipelineExplorer } from "./PipelineExplorer.tsx"
27
+ export type { PipelineExplorerProps } from "./PipelineExplorer.tsx"
28
+ export { QueryForm } from "./QueryForm.tsx"
29
+ export type { QueryFormProps } from "./QueryForm.tsx"
30
+ export { ResolvedPlace } from "./ResolvedPlace.tsx"
31
+ export type { ResolvedPlaceProps } from "./ResolvedPlace.tsx"
32
+
33
+ export type { PipelineLoadingState, PipelinePanels, PipelineRuntime } from "#pipeline/types"
34
+
35
+ export { useParsePipeline } from "#pipeline/useParsePipeline"
36
+ export type { UseParsePipeline, UseParsePipelineOptions } from "#pipeline/useParsePipeline"
@@ -5,86 +5,13 @@
5
5
  *
6
6
  * Types for the pipeline (parse + resolve) explorer. The model/gazetteer runtime is INJECTED as a
7
7
  * {@link PipelineRuntime}: this package owns the UI state machine + presentation, while the host
8
- * (the docs site's DemoEmbed, or any app) owns loading ONNX/WOF and executing a parse. That keeps
8
+ * (the docs site's RuntimeEmbed, or any app) owns loading ONNX/WOF and executing a parse. That keeps
9
9
  * onnxruntime-web, sql.js-httpvfs, and node builtins entirely out of this package's browser graph.
10
10
  */
11
11
 
12
+ import type { ParseResult } from "@mailwoman/core/pipeline/client-result"
12
13
  import type { ReactNode } from "react"
13
14
 
14
- import type { KindBadgeResult } from "../common/KindBadge.tsx"
15
-
16
- /**
17
- * One decoded component, as the table + span views render it. Offsets index into `ParseResult.input`.
18
- */
19
- export interface ParsedComponent {
20
- tag: string
21
- value?: unknown
22
- confidence?: number
23
- start?: number
24
- end?: number
25
- }
26
-
27
- /**
28
- * A resolved gazetteer place, projected to what the "Resolved place" panel + copy payload need.
29
- */
30
- export interface ResolvedPlaceView {
31
- id: number
32
- name: string
33
- placetype: string
34
- lat: number
35
- lon: number
36
- score: number
37
- }
38
-
39
- /**
40
- * An additional admin role a resolved place also fulfils (the dual-role / city-state relation, #402).
41
- */
42
- export interface DualRoleView {
43
- id: number
44
- name: string
45
- placetype: string
46
- relationshipType: string
47
- role: string
48
- }
49
-
50
- /**
51
- * Per-stage wall-clock (ms) for one parse. `resolve` is absent when the lookup is skipped.
52
- */
53
- export interface StageTiming {
54
- shape: number
55
- classify: number
56
- resolve?: number
57
- }
58
-
59
- /**
60
- * FST gazetteer-prior provenance, as the "FST prior" disclosure renders it.
61
- */
62
- export interface FSTProvenance {
63
- builtAt: string
64
- stateCount: number
65
- placeCount: number
66
- importanceMatches: number
67
- }
68
-
69
- /**
70
- * The presentational projection of one parse+resolve. The host's `runParse` produces it.
71
- */
72
- export interface ParseResult {
73
- input: string
74
- /**
75
- * Opaque hierarchy — handed straight to a host-injected tree/visualizer panel.
76
- */
77
- tree: unknown
78
- nodes: ParsedComponent[]
79
- kindResult?: KindBadgeResult
80
- timing?: StageTiming
81
- resolved: ResolvedPlaceView | null
82
- candidates: ResolvedPlaceView[]
83
- fstActive: boolean
84
- fstProvenance?: FSTProvenance | null
85
- dualRoles?: DualRoleView[]
86
- }
87
-
88
15
  /**
89
16
  * Bundle-load progress surfaced before the runtime is `ready`.
90
17
  */