@mailwoman/react 0.0.1 → 7.5.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 (143) hide show
  1. package/README.md +34 -4
  2. package/common/ClientOnly.tsx +30 -0
  3. package/common/CopyButton.tsx +44 -0
  4. package/common/KindBadge.tsx +44 -0
  5. package/common/LoadingIndicator.tsx +104 -0
  6. package/common/PresetChips.tsx +43 -0
  7. package/common/cx.ts +34 -0
  8. package/common/useClipboard.ts +73 -0
  9. package/common/useDebouncedValue.ts +23 -0
  10. package/index.ts +86 -4
  11. package/out/common/ClientOnly.d.ts +22 -0
  12. package/out/common/ClientOnly.d.ts.map +1 -0
  13. package/out/common/ClientOnly.js +20 -0
  14. package/out/common/ClientOnly.js.map +1 -0
  15. package/out/common/CopyButton.d.ts +21 -0
  16. package/out/common/CopyButton.d.ts.map +1 -0
  17. package/out/common/CopyButton.js +7 -0
  18. package/out/common/CopyButton.js.map +1 -0
  19. package/out/common/KindBadge.d.ts +24 -0
  20. package/out/common/KindBadge.d.ts.map +1 -0
  21. package/out/common/KindBadge.js +6 -0
  22. package/out/common/KindBadge.js.map +1 -0
  23. package/out/common/LoadingIndicator.d.ts +27 -0
  24. package/out/common/LoadingIndicator.d.ts.map +1 -0
  25. package/out/common/LoadingIndicator.js +41 -0
  26. package/out/common/LoadingIndicator.js.map +1 -0
  27. package/out/common/PresetChips.d.ts +22 -0
  28. package/out/common/PresetChips.d.ts.map +1 -0
  29. package/out/common/PresetChips.js +5 -0
  30. package/out/common/PresetChips.js.map +1 -0
  31. package/out/common/cx.d.ts +13 -0
  32. package/out/common/cx.d.ts.map +1 -0
  33. package/out/common/cx.js +28 -0
  34. package/out/common/cx.js.map +1 -0
  35. package/out/common/useClipboard.d.ts +16 -0
  36. package/out/common/useClipboard.d.ts.map +1 -0
  37. package/out/common/useClipboard.js +56 -0
  38. package/out/common/useClipboard.js.map +1 -0
  39. package/out/common/useDebouncedValue.d.ts +11 -0
  40. package/out/common/useDebouncedValue.d.ts.map +1 -0
  41. package/out/common/useDebouncedValue.js +19 -0
  42. package/out/common/useDebouncedValue.js.map +1 -0
  43. package/out/index.d.ts +57 -4
  44. package/out/index.d.ts.map +1 -1
  45. package/out/index.js +37 -4
  46. package/out/index.js.map +1 -1
  47. package/out/pipeline/CandidatePicker.d.ts +17 -0
  48. package/out/pipeline/CandidatePicker.d.ts.map +1 -0
  49. package/out/pipeline/CandidatePicker.js +6 -0
  50. package/out/pipeline/CandidatePicker.js.map +1 -0
  51. package/out/pipeline/ComponentTable.d.ts +14 -0
  52. package/out/pipeline/ComponentTable.d.ts.map +1 -0
  53. package/out/pipeline/ComponentTable.js +6 -0
  54. package/out/pipeline/ComponentTable.js.map +1 -0
  55. package/out/pipeline/ConfidenceCell.d.ts +13 -0
  56. package/out/pipeline/ConfidenceCell.d.ts.map +1 -0
  57. package/out/pipeline/ConfidenceCell.js +22 -0
  58. package/out/pipeline/ConfidenceCell.js.map +1 -0
  59. package/out/pipeline/PipelineExplorer.d.ts +25 -0
  60. package/out/pipeline/PipelineExplorer.d.ts.map +1 -0
  61. package/out/pipeline/PipelineExplorer.js +27 -0
  62. package/out/pipeline/PipelineExplorer.js.map +1 -0
  63. package/out/pipeline/QueryForm.d.ts +19 -0
  64. package/out/pipeline/QueryForm.d.ts.map +1 -0
  65. package/out/pipeline/QueryForm.js +9 -0
  66. package/out/pipeline/QueryForm.js.map +1 -0
  67. package/out/pipeline/ResolvedPlace.d.ts +16 -0
  68. package/out/pipeline/ResolvedPlace.d.ts.map +1 -0
  69. package/out/pipeline/ResolvedPlace.js +14 -0
  70. package/out/pipeline/ResolvedPlace.js.map +1 -0
  71. package/out/pipeline/copy.d.ts +11 -0
  72. package/out/pipeline/copy.d.ts.map +1 -0
  73. package/out/pipeline/copy.js +31 -0
  74. package/out/pipeline/copy.js.map +1 -0
  75. package/out/pipeline/presets.d.ts +12 -0
  76. package/out/pipeline/presets.d.ts.map +1 -0
  77. package/out/pipeline/presets.js +21 -0
  78. package/out/pipeline/presets.js.map +1 -0
  79. package/out/pipeline/types.d.ts +110 -0
  80. package/out/pipeline/types.d.ts.map +1 -0
  81. package/out/pipeline/types.js +12 -0
  82. package/out/pipeline/types.js.map +1 -0
  83. package/out/pipeline/useParsePipeline.d.ts +34 -0
  84. package/out/pipeline/useParsePipeline.d.ts.map +1 -0
  85. package/out/pipeline/useParsePipeline.js +55 -0
  86. package/out/pipeline/useParsePipeline.js.map +1 -0
  87. package/out/poi/AbstainPanel.d.ts +14 -0
  88. package/out/poi/AbstainPanel.d.ts.map +1 -0
  89. package/out/poi/AbstainPanel.js +5 -0
  90. package/out/poi/AbstainPanel.js.map +1 -0
  91. package/out/poi/LiveResultsBlock.d.ts +21 -0
  92. package/out/poi/LiveResultsBlock.d.ts.map +1 -0
  93. package/out/poi/LiveResultsBlock.js +7 -0
  94. package/out/poi/LiveResultsBlock.js.map +1 -0
  95. package/out/poi/OverpassBlock.d.ts +15 -0
  96. package/out/poi/OverpassBlock.d.ts.map +1 -0
  97. package/out/poi/OverpassBlock.js +11 -0
  98. package/out/poi/OverpassBlock.js.map +1 -0
  99. package/out/poi/POIExplorer.d.ts +31 -0
  100. package/out/poi/POIExplorer.d.ts.map +1 -0
  101. package/out/poi/POIExplorer.js +43 -0
  102. package/out/poi/POIExplorer.js.map +1 -0
  103. package/out/poi/QueryInput.d.ts +17 -0
  104. package/out/poi/QueryInput.d.ts.map +1 -0
  105. package/out/poi/QueryInput.js +5 -0
  106. package/out/poi/QueryInput.js.map +1 -0
  107. package/out/poi/SubjectPanel.d.ts +16 -0
  108. package/out/poi/SubjectPanel.d.ts.map +1 -0
  109. package/out/poi/SubjectPanel.js +12 -0
  110. package/out/poi/SubjectPanel.js.map +1 -0
  111. package/out/poi/runtime.d.ts +43 -0
  112. package/out/poi/runtime.d.ts.map +1 -0
  113. package/out/poi/runtime.js +67 -0
  114. package/out/poi/runtime.js.map +1 -0
  115. package/out/poi/types.d.ts +113 -0
  116. package/out/poi/types.d.ts.map +1 -0
  117. package/out/poi/types.js +12 -0
  118. package/out/poi/types.js.map +1 -0
  119. package/out/poi/usePOISearch.d.ts +42 -0
  120. package/out/poi/usePOISearch.d.ts.map +1 -0
  121. package/out/poi/usePOISearch.js +158 -0
  122. package/out/poi/usePOISearch.js.map +1 -0
  123. package/package.json +52 -8
  124. package/pipeline/CandidatePicker.tsx +45 -0
  125. package/pipeline/ComponentTable.tsx +41 -0
  126. package/pipeline/ConfidenceCell.tsx +33 -0
  127. package/pipeline/PipelineExplorer.tsx +148 -0
  128. package/pipeline/QueryForm.tsx +52 -0
  129. package/pipeline/ResolvedPlace.tsx +50 -0
  130. package/pipeline/copy.ts +37 -0
  131. package/pipeline/presets.ts +24 -0
  132. package/pipeline/types.ts +118 -0
  133. package/pipeline/useParsePipeline.ts +84 -0
  134. package/poi/AbstainPanel.tsx +22 -0
  135. package/poi/LiveResultsBlock.tsx +70 -0
  136. package/poi/OverpassBlock.tsx +37 -0
  137. package/poi/POIExplorer.tsx +133 -0
  138. package/poi/QueryInput.tsx +26 -0
  139. package/poi/SubjectPanel.tsx +79 -0
  140. package/poi/runtime.ts +78 -0
  141. package/poi/types.ts +112 -0
  142. package/poi/usePOISearch.ts +234 -0
  143. package/styles.css +648 -0
package/README.md CHANGED
@@ -1,11 +1,41 @@
1
1
  # @mailwoman/react
2
2
 
3
3
  React components + headless hooks for [mailwoman](https://mailwoman.sister.software) — the
4
- parse/geocode/POI explorers from the docs site, packaged for use in any React app.
4
+ parse/geocode/POI explorers from the docs site, decomposed and packaged for use in any React app.
5
5
 
6
- > **v0.0.1 is a name-reservation placeholder** published to configure npm Trusted
7
- > Publishing. The first functional release ships with the next mailwoman minor and will
8
- > include Storybook + browser-mode component tests.
6
+ ## What's in the box
7
+
8
+ Two composed explorers, plus the small presentational units and headless hooks they decompose into:
9
+
10
+ - **`POIExplorer`** — a self-contained POI-intent tester (classify → subject → OverpassQL). The intent
11
+ path runs entirely offline over `@mailwoman/kind-classifier`, `@mailwoman/poi-taxonomy`, and
12
+ `@mailwoman/query-shape` — no weights, no network. A live poi.db search is opt-in via an injected
13
+ `runLiveSearch` probe, so the httpvfs/worker machinery never enters this package's graph.
14
+ - **`PipelineExplorer`** — a parse + resolve tester driven by an **injected `PipelineRuntime`**. The
15
+ host supplies `runParse` (compute shape → classify → resolve) and the heavy visualizers as `panels`,
16
+ keeping onnxruntime-web, sql.js-httpvfs, and node builtins out of this package entirely.
17
+
18
+ The headless hooks — `usePOISearch`, `useParsePipeline` — own the state machines; the presentational
19
+ units (`QueryInput`, `SubjectPanel`, `OverpassBlock`, `LiveResultsBlock`, `ComponentTable`,
20
+ `ResolvedPlace`, `CandidatePicker`, `KindBadge`, `LoadingIndicator`, …) are pure and prop-driven.
21
+
22
+ ## Usage
23
+
24
+ ```tsx
25
+ import { POIExplorer, PipelineExplorer } from "@mailwoman/react"
26
+ import "@mailwoman/react/styles.css"
27
+ ```
28
+
29
+ Styling ships as a standalone stylesheet (`@mailwoman/react/styles.css`) — plain, `mw-`-prefixed, and
30
+ Infima-token-aware, so it looks right both inside Docusaurus and standalone. No component imports CSS,
31
+ so the bare package import stays node-safe.
32
+
33
+ ## Development
34
+
35
+ - `yarn workspace @mailwoman/react storybook` — Storybook (Vite) for every unit + the composed
36
+ explorers, with mocked runtimes (no network/db).
37
+ - `yarn workspace @mailwoman/react test:browser` — Vitest browser-mode component + hook tests
38
+ (Playwright / headless Chromium).
9
39
 
10
40
  ## License
11
41
 
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `ClientOnly` — a portable SSR boundary. The docs explorers wrapped Docusaurus's `<BrowserOnly>`;
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 is derived from a `useEffect` that fires only in the browser; this is a legitimate
12
+ * external-sync effect (bridging the server/client rendering boundary), not derived state.
13
+ */
14
+
15
+ import { type ReactNode, useEffect, useState } from "react"
16
+
17
+ export interface ClientOnlyProps {
18
+ /** Rendered once mounted in the browser. A thunk so its (browser-only) imports never run on the server. */
19
+ children: () => ReactNode
20
+ /** Rendered on the server and until the first client mount. */
21
+ fallback?: ReactNode
22
+ }
23
+
24
+ export function ClientOnly({ children, fallback = null }: ClientOnlyProps): ReactNode {
25
+ const [mounted, setMounted] = useState(false)
26
+
27
+ useEffect(() => setMounted(true), [])
28
+
29
+ return mounted ? children() : fallback
30
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `CopyButton` — a small presentational button that copies a string and shows a transient checkmark.
7
+ * Wraps {@link useClipboard}. Used by both explorers (OverpassQL copy, parse-JSON copy).
8
+ */
9
+
10
+ import type { ReactNode } from "react"
11
+
12
+ import { useClipboard } from "./useClipboard.ts"
13
+
14
+ export interface CopyButtonProps {
15
+ /** The text to copy. If a thunk, it's evaluated at click time (so callers can copy the latest value). */
16
+ value: string | (() => string)
17
+ /** Label in the idle state. @default "Copy" */
18
+ label?: ReactNode
19
+ /** Label shown briefly after a successful copy. @default "✓ Copied" */
20
+ copiedLabel?: ReactNode
21
+ className?: string
22
+ disabled?: boolean
23
+ }
24
+
25
+ export function CopyButton({
26
+ value,
27
+ label = "Copy",
28
+ copiedLabel = "✓ Copied",
29
+ className = "mw-btn",
30
+ disabled,
31
+ }: CopyButtonProps): ReactNode {
32
+ const { copied, copy } = useClipboard()
33
+
34
+ return (
35
+ <button
36
+ type="button"
37
+ className={className}
38
+ disabled={disabled}
39
+ onClick={() => copy(typeof value === "function" ? value() : value)}
40
+ >
41
+ {copied ? copiedLabel : label}
42
+ </button>
43
+ )
44
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `KindBadge` — compact display of the Stage 2.5 kind-classifier verdict: the top kind + confidence
7
+ * as a pill, expanding to the alternatives on click. Shared by both explorers. The result type is
8
+ * kept structural (`kind: string`) so both the POI query classifier's `QueryKindResult` and the
9
+ * pipeline's projection satisfy it without a hard type dependency.
10
+ */
11
+
12
+ import type { ReactNode } from "react"
13
+
14
+ export interface KindBadgeResult {
15
+ kind: string
16
+ confidence: number
17
+ alternatives: ReadonlyArray<{ kind: string; confidence: number }>
18
+ }
19
+
20
+ export interface KindBadgeProps {
21
+ kindResult: KindBadgeResult
22
+ }
23
+
24
+ const formatPct = (n: number): string => `${Math.round(n * 100)}%`
25
+
26
+ export function KindBadge({ kindResult }: KindBadgeProps): ReactNode {
27
+ return (
28
+ <details className="mw-kind">
29
+ <summary className="mw-kind__summary">
30
+ <strong>Kind:</strong> <code>{kindResult.kind}</code>{" "}
31
+ <span className="mw-kind__confidence">({formatPct(kindResult.confidence)})</span>
32
+ </summary>
33
+ {kindResult.alternatives.length > 0 ? (
34
+ <ul className="mw-kind__alternatives">
35
+ {kindResult.alternatives.map((alt) => (
36
+ <li key={alt.kind}>
37
+ <code>{alt.kind}</code> <span className="mw-kind__confidence">({formatPct(alt.confidence)})</span>
38
+ </li>
39
+ ))}
40
+ </ul>
41
+ ) : null}
42
+ </details>
43
+ )
44
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * LoadingIndicator — pure-CSS loading states for three UX patterns: `pulse` (skeleton bars),
7
+ * `spinner` (rotation ring), and `staged` (a step list with checkmarks). SSR-safe: every animation
8
+ * is CSS-driven, so the component renders identically on server and client. Ported from the docs
9
+ * component onto plain (`mw-`-prefixed) class names.
10
+ */
11
+
12
+ import { memo, type ReactNode } from "react"
13
+
14
+ import { cx } from "./cx.ts"
15
+
16
+ export type LoadingMode = "pulse" | "spinner" | "staged"
17
+
18
+ export interface LoadingIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
19
+ /** @default "spinner" */
20
+ mode?: LoadingMode
21
+ /** Optional label; in staged mode it renders above the step list. */
22
+ label?: string
23
+ /** Number of skeleton bars (pulse mode). @default 3 */
24
+ barCount?: number
25
+ /** Spinner ring size. @default "medium" */
26
+ size?: "small" | "medium" | "large"
27
+ /** Step labels (staged mode) — one row each. */
28
+ steps?: string[]
29
+ /** 0-based index of the active step; earlier steps are complete, later ones pending. @default -1 */
30
+ activeStep?: number
31
+ }
32
+
33
+ const SIZE_CLASS: Record<NonNullable<LoadingIndicatorProps["size"]>, string> = {
34
+ small: "mw-spinner--small",
35
+ medium: "",
36
+ large: "mw-spinner--large",
37
+ }
38
+
39
+ export const LoadingIndicator = memo<LoadingIndicatorProps>(function LoadingIndicator({
40
+ mode = "spinner",
41
+ label,
42
+ barCount = 3,
43
+ size = "medium",
44
+ steps,
45
+ activeStep = -1,
46
+ className,
47
+ ...rest
48
+ }) {
49
+ return (
50
+ <div className={cx("mw-loading", className)} role="status" aria-label={label ?? "Loading"} {...rest}>
51
+ {mode === "pulse" ? <PulseBars count={barCount} /> : null}
52
+ {mode === "spinner" ? <SpinnerRing size={size} /> : null}
53
+ {mode === "staged" ? <StagedProgress steps={steps} activeStep={activeStep} /> : null}
54
+ {label ? <span className="mw-loading__label">{label}</span> : null}
55
+ </div>
56
+ )
57
+ })
58
+
59
+ const PulseBars = memo<{ count: number }>(function PulseBars({ count }) {
60
+ return (
61
+ <div className="mw-pulse">
62
+ {Array.from({ length: count }, (_, i) => (
63
+ <div key={i} className="mw-pulse__bar" />
64
+ ))}
65
+ </div>
66
+ )
67
+ })
68
+
69
+ const SpinnerRing = memo<{ size: NonNullable<LoadingIndicatorProps["size"]> }>(function SpinnerRing({ size }) {
70
+ return <div className={cx("mw-spinner", SIZE_CLASS[size])} />
71
+ })
72
+
73
+ const StagedProgress = memo<{ steps?: string[]; activeStep: number }>(function StagedProgress({ steps, activeStep }) {
74
+ if (!steps || steps.length === 0) return null
75
+
76
+ return (
77
+ <ul className="mw-staged">
78
+ {steps.map((step, i): ReactNode => {
79
+ const isComplete = i < activeStep
80
+ const isActive = i === activeStep
81
+
82
+ return (
83
+ <li
84
+ key={i}
85
+ className={cx("mw-staged__step", {
86
+ "mw-staged__step--complete": isComplete,
87
+ "mw-staged__step--active": isActive,
88
+ "mw-staged__step--pending": !isComplete && !isActive,
89
+ })}
90
+ >
91
+ <span className="mw-staged__icon">
92
+ {isComplete ? (
93
+ <span className="mw-staged__check" />
94
+ ) : isActive ? (
95
+ <span className="mw-staged__dot" />
96
+ ) : null}
97
+ </span>
98
+ <span>{step}</span>
99
+ </li>
100
+ )
101
+ })}
102
+ </ul>
103
+ )
104
+ })
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `PresetChips` — the "Try:" row of example buttons shared by both explorers. Presentational: it
7
+ * renders a labelled list and calls `onPick` with the chosen preset's value.
8
+ */
9
+
10
+ import type { ReactNode } from "react"
11
+
12
+ export interface Preset {
13
+ label: string
14
+ value: string
15
+ }
16
+
17
+ export interface PresetChipsProps {
18
+ presets: ReadonlyArray<Preset>
19
+ onPick: (value: string) => void
20
+ disabled?: boolean
21
+ /** Leading label. @default "Try:" */
22
+ caption?: string
23
+ }
24
+
25
+ export function PresetChips({ presets, onPick, disabled, caption = "Try:" }: PresetChipsProps): ReactNode {
26
+ return (
27
+ <div className="mw-presets">
28
+ <span className="mw-presets__label">{caption}</span>
29
+ {presets.map((preset) => (
30
+ <button
31
+ key={preset.label}
32
+ type="button"
33
+ className="mw-chip"
34
+ disabled={disabled}
35
+ onClick={() => onPick(preset.value)}
36
+ title={preset.value}
37
+ >
38
+ {preset.label}
39
+ </button>
40
+ ))}
41
+ </div>
42
+ )
43
+ }
package/common/cx.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Tiny classname joiner — the only utility the presentational units need. Kept in-package (rather
7
+ * than pulling `classnames`) so `@mailwoman/react` ships zero runtime deps beyond its `@mailwoman/*`
8
+ * siblings and the React peer.
9
+ */
10
+
11
+ export type ClassValue = string | number | false | null | undefined | Record<string, boolean>
12
+
13
+ /** Join truthy class tokens; object entries contribute their key when the value is truthy. */
14
+ export function cx(...values: ClassValue[]): string {
15
+ const out: string[] = []
16
+
17
+ for (const value of values) {
18
+ if (!value) continue
19
+
20
+ if (typeof value === "string" || typeof value === "number") {
21
+ out.push(String(value))
22
+
23
+ continue
24
+ }
25
+
26
+ for (const [key, on] of Object.entries(value)) {
27
+ if (on) {
28
+ out.push(key)
29
+ }
30
+ }
31
+ }
32
+
33
+ return out.join(" ")
34
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `useClipboard` — headless copy-to-clipboard with the async Clipboard API and a legacy
7
+ * `execCommand` fallback (for insecure contexts / older browsers), plus a transient "copied" flag
8
+ * that auto-resets. Extracted from the two explorers' duplicated `onCopy` handlers.
9
+ */
10
+
11
+ import { useCallback, useEffect, useRef, useState } from "react"
12
+
13
+ /** Best-effort clipboard write: async Clipboard API first, hidden-textarea `execCommand` fallback. */
14
+ async function writeToClipboard(value: string): Promise<void> {
15
+ try {
16
+ await navigator.clipboard.writeText(value)
17
+
18
+ return
19
+ } catch {
20
+ // Fall through to the legacy path below.
21
+ }
22
+
23
+ const textarea = document.createElement("textarea")
24
+ textarea.value = value
25
+ textarea.style.position = "fixed"
26
+ textarea.style.opacity = "0"
27
+ document.body.appendChild(textarea)
28
+ textarea.select()
29
+
30
+ try {
31
+ document.execCommand("copy")
32
+ } catch {
33
+ // Nothing else to try — leave `copied` false via the caller's error path.
34
+ } finally {
35
+ document.body.removeChild(textarea)
36
+ }
37
+ }
38
+
39
+ export interface UseClipboard {
40
+ copied: boolean
41
+ copy: (value: string) => Promise<void>
42
+ }
43
+
44
+ /** @param resetMs - How long the `copied` flag stays true after a successful copy. */
45
+ export function useClipboard(resetMs = 1500): UseClipboard {
46
+ const [copied, setCopied] = useState(false)
47
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
48
+
49
+ // Clear a pending reset timer on unmount so it never fires against a torn-down component.
50
+ useEffect(
51
+ () => () => {
52
+ if (timer.current) {
53
+ clearTimeout(timer.current)
54
+ }
55
+ },
56
+ []
57
+ )
58
+
59
+ const copy = useCallback(
60
+ async (value: string) => {
61
+ await writeToClipboard(value)
62
+ setCopied(true)
63
+
64
+ if (timer.current) {
65
+ clearTimeout(timer.current)
66
+ }
67
+ timer.current = setTimeout(() => setCopied(false), resetMs)
68
+ },
69
+ [resetMs]
70
+ )
71
+
72
+ return { copied, copy }
73
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `useDebouncedValue` — returns a copy of `value` that only updates after `delayMs` of quiet. Used by
7
+ * the POI explorer to avoid re-classifying on every keystroke. The timer is a genuine external-sync
8
+ * effect (a scheduled clock), so it belongs in an effect.
9
+ */
10
+
11
+ import { useEffect, useState } from "react"
12
+
13
+ export function useDebouncedValue<T>(value: T, delayMs: number): T {
14
+ const [debounced, setDebounced] = useState(value)
15
+
16
+ useEffect(() => {
17
+ const id = setTimeout(() => setDebounced(value), delayMs)
18
+
19
+ return () => clearTimeout(id)
20
+ }, [value, delayMs])
21
+
22
+ return debounced
23
+ }
package/index.ts CHANGED
@@ -5,9 +5,91 @@
5
5
  *
6
6
  * `@mailwoman/react` — React components + headless hooks for mailwoman.
7
7
  *
8
- * v0.0.1 is a name-reservation placeholder published to configure npm Trusted Publishing.
9
- * The first functional release extracts the docs-site explorers (parse, geocode, POI) into
10
- * decomposed, Storybook-tested components with headless hooks.
8
+ * Two composed explorers plus the small presentational units and headless hooks they decompose into:
9
+ *
10
+ * - `POIExplorer` — a self-contained POI-intent tester (classify → subject → OverpassQL), with an
11
+ * optional injected live poi.db search. No weights, no network on the intent path.
12
+ * - `PipelineExplorer` — a parse+resolve tester driven by an INJECTED `PipelineRuntime`, so the
13
+ * model/gazetteer plumbing (ONNX, httpvfs, node builtins) stays in the host, never in this graph.
14
+ *
15
+ * Styling ships separately as `@mailwoman/react/styles.css` (plain, `mw-`-prefixed, Infima-token
16
+ * aware) — no CSS is imported by the component modules, so the bare package import is node-safe.
11
17
  */
12
18
 
13
- export const MAILWOMAN_REACT_STATUS = "placeholder" as const
19
+ // ── Common primitives ──────────────────────────────────────────────────────
20
+ export { ClientOnly } from "./common/ClientOnly.tsx"
21
+ export type { ClientOnlyProps } from "./common/ClientOnly.tsx"
22
+ export { CopyButton } from "./common/CopyButton.tsx"
23
+ export type { CopyButtonProps } from "./common/CopyButton.tsx"
24
+ export { cx } from "./common/cx.ts"
25
+ export type { ClassValue } from "./common/cx.ts"
26
+ export { KindBadge } from "./common/KindBadge.tsx"
27
+ export type { KindBadgeProps, KindBadgeResult } from "./common/KindBadge.tsx"
28
+ export { LoadingIndicator } from "./common/LoadingIndicator.tsx"
29
+ export type { LoadingIndicatorProps, LoadingMode } from "./common/LoadingIndicator.tsx"
30
+ export { PresetChips } from "./common/PresetChips.tsx"
31
+ export type { Preset, PresetChipsProps } from "./common/PresetChips.tsx"
32
+ export { useClipboard } from "./common/useClipboard.ts"
33
+ export type { UseClipboard } from "./common/useClipboard.ts"
34
+ export { useDebouncedValue } from "./common/useDebouncedValue.ts"
35
+
36
+ // ── POI explorer ───────────────────────────────────────────────────────────
37
+ export { AbstainPanel } from "./poi/AbstainPanel.tsx"
38
+ export type { AbstainPanelProps } from "./poi/AbstainPanel.tsx"
39
+ export { LiveResultsBlock } from "./poi/LiveResultsBlock.tsx"
40
+ export type { LiveResultsBlockProps } from "./poi/LiveResultsBlock.tsx"
41
+ export { OverpassBlock } from "./poi/OverpassBlock.tsx"
42
+ export type { OverpassBlockProps } from "./poi/OverpassBlock.tsx"
43
+ export { POIExplorer } from "./poi/POIExplorer.tsx"
44
+ export type { POIExplorerProps } from "./poi/POIExplorer.tsx"
45
+ export { QueryInput } from "./poi/QueryInput.tsx"
46
+ export type { QueryInputProps } from "./poi/QueryInput.tsx"
47
+ export { formatDistance, loadPOIRuntime, POI_DEFAULT_TEXT, POI_PRESETS } from "./poi/runtime.ts"
48
+ export { SubjectPanel } from "./poi/SubjectPanel.tsx"
49
+ export type { SubjectPanelProps } from "./poi/SubjectPanel.tsx"
50
+ export type {
51
+ CategoryRecord,
52
+ LiveSearchState,
53
+ LoadPOIRuntime,
54
+ POIBrandSubject,
55
+ POICategorySubject,
56
+ POIExplorerResult,
57
+ POILiveSearch,
58
+ POILiveSearchResult,
59
+ POIRuntime,
60
+ POISearchHit,
61
+ POISubject,
62
+ POISubjectBase,
63
+ TaxonomyLookup,
64
+ } from "./poi/types.ts"
65
+ export { usePOISearch } from "./poi/usePOISearch.ts"
66
+ export type { UsePOISearch, UsePOISearchOptions } from "./poi/usePOISearch.ts"
67
+
68
+ // ── Pipeline explorer ──────────────────────────────────────────────────────
69
+ export { CandidatePicker } from "./pipeline/CandidatePicker.tsx"
70
+ export type { CandidatePickerProps } from "./pipeline/CandidatePicker.tsx"
71
+ export { ComponentTable } from "./pipeline/ComponentTable.tsx"
72
+ export type { ComponentTableProps } from "./pipeline/ComponentTable.tsx"
73
+ export { ConfidenceCell } from "./pipeline/ConfidenceCell.tsx"
74
+ export type { ConfidenceCellProps } from "./pipeline/ConfidenceCell.tsx"
75
+ export { buildParsePayload } from "./pipeline/copy.ts"
76
+ export { PIPELINE_DEFAULT_ADDRESS, PIPELINE_PRESETS } from "./pipeline/presets.ts"
77
+ export { PipelineExplorer } from "./pipeline/PipelineExplorer.tsx"
78
+ export type { PipelineExplorerProps } from "./pipeline/PipelineExplorer.tsx"
79
+ export { QueryForm } from "./pipeline/QueryForm.tsx"
80
+ export type { QueryFormProps } from "./pipeline/QueryForm.tsx"
81
+ export { ResolvedPlace } from "./pipeline/ResolvedPlace.tsx"
82
+ export type { ResolvedPlaceProps } from "./pipeline/ResolvedPlace.tsx"
83
+ export type {
84
+ DualRoleView,
85
+ FSTProvenance,
86
+ ParsedComponent,
87
+ ParseResult,
88
+ PipelineLoadingState,
89
+ PipelinePanels,
90
+ PipelineRuntime,
91
+ ResolvedPlaceView,
92
+ StageTiming,
93
+ } from "./pipeline/types.ts"
94
+ export { useParsePipeline } from "./pipeline/useParsePipeline.ts"
95
+ export type { UseParsePipeline, UseParsePipelineOptions } from "./pipeline/useParsePipeline.ts"
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `ClientOnly` — a portable SSR boundary. The docs explorers wrapped Docusaurus's `<BrowserOnly>`;
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 is derived from a `useEffect` that fires only in the browser; this is a legitimate
12
+ * external-sync effect (bridging the server/client rendering boundary), not derived state.
13
+ */
14
+ import { type ReactNode } from "react";
15
+ export interface ClientOnlyProps {
16
+ /** Rendered once mounted in the browser. A thunk so its (browser-only) imports never run on the server. */
17
+ children: () => ReactNode;
18
+ /** Rendered on the server and until the first client mount. */
19
+ fallback?: ReactNode;
20
+ }
21
+ export declare function ClientOnly({ children, fallback }: ClientOnlyProps): ReactNode;
22
+ //# sourceMappingURL=ClientOnly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientOnly.d.ts","sourceRoot":"","sources":["../../common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAA;AAE3D,MAAM,WAAW,eAAe;IAC/B,2GAA2G;IAC3G,QAAQ,EAAE,MAAM,SAAS,CAAA;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAe,EAAE,EAAE,eAAe,GAAG,SAAS,CAMpF"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `ClientOnly` — a portable SSR boundary. The docs explorers wrapped Docusaurus's `<BrowserOnly>`;
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 is derived from a `useEffect` that fires only in the browser; this is a legitimate
12
+ * external-sync effect (bridging the server/client rendering boundary), not derived state.
13
+ */
14
+ import { useEffect, useState } from "react";
15
+ export function ClientOnly({ children, fallback = null }) {
16
+ const [mounted, setMounted] = useState(false);
17
+ useEffect(() => setMounted(true), []);
18
+ return mounted ? children() : fallback;
19
+ }
20
+ //# sourceMappingURL=ClientOnly.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientOnly.js","sourceRoot":"","sources":["../../common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAkB,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAS3D,MAAM,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,EAAmB;IACxE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAE7C,SAAS,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAErC,OAAO,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAA;AACvC,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `CopyButton` — a small presentational button that copies a string and shows a transient checkmark.
7
+ * Wraps {@link useClipboard}. Used by both explorers (OverpassQL copy, parse-JSON copy).
8
+ */
9
+ import type { ReactNode } from "react";
10
+ export interface CopyButtonProps {
11
+ /** The text to copy. If a thunk, it's evaluated at click time (so callers can copy the latest value). */
12
+ value: string | (() => string);
13
+ /** Label in the idle state. @default "Copy" */
14
+ label?: ReactNode;
15
+ /** Label shown briefly after a successful copy. @default "✓ Copied" */
16
+ copiedLabel?: ReactNode;
17
+ className?: string;
18
+ disabled?: boolean;
19
+ }
20
+ export declare function CopyButton({ value, label, copiedLabel, className, disabled, }: CopyButtonProps): ReactNode;
21
+ //# sourceMappingURL=CopyButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopyButton.d.ts","sourceRoot":"","sources":["../../common/CopyButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,MAAM,WAAW,eAAe;IAC/B,yGAAyG;IACzG,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IAC9B,+CAA+C;IAC/C,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,uEAAuE;IACvE,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"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useClipboard } from "./useClipboard.js";
3
+ export function CopyButton({ value, label = "Copy", copiedLabel = "✓ Copied", className = "mw-btn", disabled, }) {
4
+ const { copied, copy } = useClipboard();
5
+ return (_jsx("button", { type: "button", className: className, disabled: disabled, onClick: () => copy(typeof value === "function" ? value() : value), children: copied ? copiedLabel : label }));
6
+ }
7
+ //# sourceMappingURL=CopyButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopyButton.js","sourceRoot":"","sources":["../../common/CopyButton.tsx"],"names":[],"mappings":";AAWA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAahD,MAAM,UAAU,UAAU,CAAC,EAC1B,KAAK,EACL,KAAK,GAAG,MAAM,EACd,WAAW,GAAG,UAAU,EACxB,SAAS,GAAG,QAAQ,EACpB,QAAQ,GACS;IACjB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,YAAY,EAAE,CAAA;IAEvC,OAAO,CACN,iBACC,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,YAEjE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,GACrB,CACT,CAAA;AACF,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `KindBadge` — compact display of the Stage 2.5 kind-classifier verdict: the top kind + confidence
7
+ * as a pill, expanding to the alternatives on click. Shared by both explorers. The result type is
8
+ * kept structural (`kind: string`) so both the POI query classifier's `QueryKindResult` and the
9
+ * pipeline's projection satisfy it without a hard type dependency.
10
+ */
11
+ import type { ReactNode } from "react";
12
+ export interface KindBadgeResult {
13
+ kind: string;
14
+ confidence: number;
15
+ alternatives: ReadonlyArray<{
16
+ kind: string;
17
+ confidence: number;
18
+ }>;
19
+ }
20
+ export interface KindBadgeProps {
21
+ kindResult: KindBadgeResult;
22
+ }
23
+ export declare function KindBadge({ kindResult }: KindBadgeProps): ReactNode;
24
+ //# sourceMappingURL=KindBadge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KindBadge.d.ts","sourceRoot":"","sources":["../../common/KindBadge.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjE;AAED,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,eAAe,CAAA;CAC3B;AAID,wBAAgB,SAAS,CAAC,EAAE,UAAU,EAAE,EAAE,cAAc,GAAG,SAAS,CAkBnE"}