@wherabouts/react-ui 0.1.0 → 0.1.1

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.
@@ -0,0 +1,115 @@
1
+ # ForwardGeocodeInput
2
+
3
+ ## Summary
4
+
5
+ Resolves a free-text address string to coordinates (forward geocoding) as the `query` prop changes. Controlled: the parent owns `query` and receives geocode results via `onResult`. Renders a read-only display input showing the resolved `latitude, longitude` pair.
6
+
7
+ ## When to use / when not
8
+
9
+ **Use it when:**
10
+
11
+ - You have a text query (from a search box, form field, or programmatic source) and need the corresponding coordinates without building your own geocode loop.
12
+ - You want a lightweight read-only coordinate display driven by an external input.
13
+ - You need to react to resolved coordinates (e.g. drop a map pin, store lat/lng alongside a form submission).
14
+
15
+ **Don't use it when:**
16
+
17
+ - You need the user to type and search interactively in a single input — use `AddressAutocomplete` instead (it handles debounce, suggestions, and selection in one component).
18
+ - You have coordinates and want the human-readable address — use `ReverseGeocodeInput` instead.
19
+ - You need editable lat/lng fields — wire your own inputs to `useForwardGeocode` directly.
20
+
21
+ ## Import & minimal example
22
+
23
+ ```tsx
24
+ import { createWheraboutsClient } from "@wherabouts/sdk";
25
+ import { ForwardGeocodeInput } from "@wherabouts/react-ui";
26
+
27
+ const client = createWheraboutsClient({ apiKey: "..." });
28
+
29
+ function MyComponent() {
30
+ const [query, setQuery] = React.useState("");
31
+
32
+ return (
33
+ <>
34
+ <input value={query} onChange={(e) => setQuery(e.target.value)} />
35
+ <ForwardGeocodeInput
36
+ client={client}
37
+ query={query}
38
+ onResult={(r) => console.log(r.latitude, r.longitude)}
39
+ />
40
+ </>
41
+ );
42
+ }
43
+ ```
44
+
45
+ ## Worked examples
46
+
47
+ ### 1. Store resolved coordinates on form submit
48
+
49
+ ```tsx
50
+ const [query, setQuery] = React.useState("");
51
+ const [coords, setCoords] = React.useState<{
52
+ lat: number | null;
53
+ lng: number | null;
54
+ }>({ lat: null, lng: null });
55
+
56
+ <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Enter address" />
57
+ <ForwardGeocodeInput
58
+ client={client}
59
+ query={query}
60
+ onResult={(r) => setCoords({ lat: r.latitude, lng: r.longitude })}
61
+ placeholder="Resolved coordinates"
62
+ />
63
+ ```
64
+
65
+ ### 2. Debounce the query to reduce API requests
66
+
67
+ The component fires a geocode request on every `query` change. If `query` comes from a fast-changing input (e.g. keystrokes), debounce before passing to `query`:
68
+
69
+ ```tsx
70
+ // Replace with your preferred debounce hook (e.g. `use-debounce`).
71
+ import { useDebouncedValue } from "your-debounce-hook";
72
+
73
+ const [raw, setRaw] = React.useState("");
74
+ const debouncedQuery = useDebouncedValue(raw, 300);
75
+
76
+ <input value={raw} onChange={(e) => setRaw(e.target.value)} />
77
+ <ForwardGeocodeInput client={client} query={debouncedQuery} onResult={handleResult} />
78
+ ```
79
+
80
+ ### 3. Skip geocoding when query is empty
81
+
82
+ Pass `query={null}` (or an empty string) to suppress the network request entirely:
83
+
84
+ ```tsx
85
+ <ForwardGeocodeInput client={client} query={query || null} onResult={handleResult} />
86
+ ```
87
+
88
+ ## Props
89
+
90
+ | Prop | Type | Default | Description |
91
+ |---|---|---|---|
92
+ | `client` | `WheraboutsClient` | **Required** | SDK client created with `createWheraboutsClient`. |
93
+ | `query` | `string \| null` | **Required** | Address text to geocode. `null` or empty string skips the request. |
94
+ | `onResult` | `(r: { latitude: number \| null; longitude: number \| null; formattedAddress: string \| null }) => void` | — | Called whenever the resolved result changes. All fields are `null` when no result is available. |
95
+ | `placeholder` | `string` | `"Coordinates will appear here"` | Placeholder text for the read-only display input. |
96
+ | `id` | `string` | — | `id` forwarded to the input element. |
97
+ | `className` | `string` | — | Additional CSS class applied to the input element. |
98
+ | `disabled` | `boolean` | `false` | Disables the input. |
99
+
100
+ > **Controlled component:** `ForwardGeocodeInput` does not own `query`. Your component must supply and update it. The component only renders the resolved coordinate string; it does not accept user typing.
101
+
102
+ ## Accessibility
103
+
104
+ - The component renders a native `<input type="text" readOnly>` — screen readers announce it as a text field with the current value.
105
+ - Supply a visible `<label>` or `aria-label` / `aria-labelledby` targeting the `id` prop so assistive technologies identify the field.
106
+ - The `disabled` prop produces the standard disabled input state, which is communicated to screen readers automatically.
107
+ - Because the input is read-only, keyboard users cannot accidentally modify the displayed value.
108
+
109
+ ## Recipes & edge cases
110
+
111
+ - **`null` query = no request:** Pass `query={null}` to explicitly suppress geocoding (e.g. before the user has typed anything). An empty string `""` also produces no result.
112
+ - **Debouncing the query:** The component fires on every `query` change. Debounce upstream (e.g. 300 ms) if `query` comes from keystroke events to avoid excessive API calls.
113
+ - **`onResult` on every render cycle:** `onResult` is called inside a `useEffect` that runs whenever the resolved `data` changes. If you pass an inline callback, wrap it in `useCallback` to avoid firing on every parent render.
114
+ - **All-null result:** When the geocode fails or returns no match, `onResult` is still called with `{ latitude: null, longitude: null, formattedAddress: null }`. Guard against nulls before using the values.
115
+ - **Display format:** The component renders `"lat.toFixed(4), lng.toFixed(4)"` in the input. If you need raw numeric values, consume them from the `onResult` callback instead.
@@ -0,0 +1,124 @@
1
+ # ReverseGeocodeInput
2
+
3
+ ## Summary
4
+
5
+ Resolves a `latitude`/`longitude` pair to the nearest address (reverse geocoding). No request is made until both coordinates are non-null.
6
+
7
+ ## When to use / when not
8
+
9
+ **Use it when:**
10
+
11
+ - You have coordinates (e.g. from a GPS fix, map click, or device location) and need the human-readable address for display or storage.
12
+ - You want a lightweight read-only address display that reacts to changing coordinates.
13
+ - You need to react to the resolved address (e.g. prefill a form field, log the location label, show the nearest street address).
14
+
15
+ **Don't use it when:**
16
+
17
+ - You need the user to type and search interactively in a single input — use `AddressAutocomplete` instead (it handles debounce, suggestions, and selection in one component).
18
+ - You have a text query and want coordinates — use `ForwardGeocodeInput` instead.
19
+ - You need editable address fields — wire your own inputs to `useReverseGeocode` directly.
20
+
21
+ ## Import & minimal example
22
+
23
+ ```tsx
24
+ import { createWheraboutsClient } from "@wherabouts/sdk";
25
+ import { ReverseGeocodeInput } from "@wherabouts/react-ui";
26
+
27
+ const client = createWheraboutsClient({ apiKey: "..." });
28
+
29
+ function MyComponent() {
30
+ const [coords, setCoords] = React.useState<{
31
+ lat: number | null;
32
+ lng: number | null;
33
+ }>({ lat: null, lng: null });
34
+
35
+ return (
36
+ <ReverseGeocodeInput
37
+ client={client}
38
+ latitude={coords.lat}
39
+ longitude={coords.lng}
40
+ onResult={(r) => console.log(r.address, r.distance)}
41
+ />
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## Worked examples
47
+
48
+ ### 1. Show address when user clicks a map
49
+
50
+ ```tsx
51
+ const [lat, setLat] = React.useState<number | null>(null);
52
+ const [lng, setLng] = React.useState<number | null>(null);
53
+ const [label, setLabel] = React.useState<string>("");
54
+
55
+ // Imagine mapInstance.on("click", ...) sets lat/lng
56
+ <ReverseGeocodeInput
57
+ client={client}
58
+ latitude={lat}
59
+ longitude={lng}
60
+ onResult={(r) => setLabel(r.address ?? "Unknown location")}
61
+ placeholder="Click the map to resolve an address"
62
+ />
63
+ <p>Selected: {label || "—"}</p>
64
+ ```
65
+
66
+ ### 2. Display distance alongside the resolved address
67
+
68
+ ```tsx
69
+ const [distance, setDistance] = React.useState<number | null>(null);
70
+
71
+ <ReverseGeocodeInput
72
+ client={client}
73
+ latitude={-27.4698}
74
+ longitude={153.0251}
75
+ onResult={(r) => setDistance(r.distance)}
76
+ />
77
+ {distance !== null && (
78
+ <p>Distance from nearest address: {distance.toFixed(1)} m</p>
79
+ )}
80
+ ```
81
+
82
+ ### 3. Hold the request until both coordinates are available
83
+
84
+ Pass `null` for either coordinate to suppress the network request entirely. The component renders its placeholder until a valid pair arrives:
85
+
86
+ ```tsx
87
+ <ReverseGeocodeInput
88
+ client={client}
89
+ latitude={gpsReady ? lat : null}
90
+ longitude={gpsReady ? lng : null}
91
+ onResult={handleResult}
92
+ placeholder="Waiting for GPS fix…"
93
+ />
94
+ ```
95
+
96
+ ## Props
97
+
98
+ | Prop | Type | Default | Description |
99
+ |---|---|---|---|
100
+ | `client` | `WheraboutsClient` | **Required** | SDK client created with `createWheraboutsClient`. |
101
+ | `latitude` | `number \| null` | **Required** | Latitude to reverse-geocode. `null` suppresses the request. |
102
+ | `longitude` | `number \| null` | **Required** | Longitude to reverse-geocode. `null` suppresses the request. |
103
+ | `onResult` | `(r: { address: string \| null; distance: number \| null }) => void` | — | Called whenever the resolved address changes. Both fields are `null` when no result is available. |
104
+ | `placeholder` | `string` | `"Address will appear here"` | Placeholder text for the read-only display input. |
105
+ | `id` | `string` | — | `id` forwarded to the input element. |
106
+ | `className` | `string` | — | Additional CSS class applied to the input element. |
107
+ | `disabled` | `boolean` | `false` | Disables the input. |
108
+
109
+ > **Null-coordinate handling:** No geocode request is made until **both** `latitude` and `longitude` are non-null. The component renders its placeholder while either coordinate is `null`.
110
+
111
+ ## Accessibility
112
+
113
+ - The component renders a native `<input type="text" readOnly>` — screen readers announce it as a text field with the current value.
114
+ - Supply a visible `<label>` or `aria-label` / `aria-labelledby` targeting the `id` prop so assistive technologies identify the field.
115
+ - The `disabled` prop produces the standard disabled input state, which is communicated to screen readers automatically.
116
+ - Because the input is read-only, keyboard users cannot accidentally modify the displayed value.
117
+
118
+ ## Recipes & edge cases
119
+
120
+ - **Both coordinates must be non-null:** If either `latitude` or `longitude` is `null`, no request is fired and the input shows the placeholder. Use this deliberately to gate the request on GPS availability or user interaction.
121
+ - **`onResult` on every render cycle:** `onResult` is called inside a `useEffect` that runs whenever the resolved data changes. If you pass an inline callback, wrap it in `useCallback` to avoid firing on every parent render.
122
+ - **All-null result:** When reverse geocoding returns no match, `onResult` is still called with `{ address: null, distance: null }`. Guard against nulls before using the values.
123
+ - **`address` is `formattedAddress` from the API:** The `address` field in `onResult` is the formatted address string from the nearest result (or `null` if unavailable). It is also what the read-only input displays.
124
+ - **`distance` is in metres:** The `distance` field reports how far the resolved address is from the supplied coordinates, in metres.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wherabouts/react-ui",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "Production-ready React components for Wherabouts SDK — address autocomplete, geocoding, and form fields with accessibility and customization.",
6
6
  "license": "UNLICENSED",
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
+ "docs",
20
21
  "README.md",
21
22
  "CHANGELOG.md"
22
23
  ],
@@ -25,7 +26,9 @@
25
26
  "dev": "tsup --watch",
26
27
  "test": "vitest run",
27
28
  "test:watch": "vitest",
28
- "prepublishOnly": "pnpm build"
29
+ "prepublishOnly": "pnpm build",
30
+ "storybook": "storybook dev -p 6006 --no-open",
31
+ "build-storybook": "storybook build"
29
32
  },
30
33
  "peerDependencies": {
31
34
  "react": ">=19.0.0",
@@ -34,6 +37,8 @@
34
37
  "@wherabouts/react": ">=0.2.0"
35
38
  },
36
39
  "devDependencies": {
40
+ "@storybook/addon-a11y": "^9.0.0",
41
+ "@storybook/react-vite": "^9.0.0",
37
42
  "@testing-library/jest-dom": "^6.1.5",
38
43
  "@testing-library/react": "^15.0.0",
39
44
  "@testing-library/user-event": "^14.5.1",
@@ -46,9 +51,11 @@
46
51
  "jsdom": "^23.0.1",
47
52
  "react": "catalog:",
48
53
  "react-dom": "catalog:",
54
+ "storybook": "^9.0.0",
49
55
  "tailwind-merge": "^2.3.0",
50
56
  "tsup": "^8.0.2",
51
57
  "typescript": "^5.4.4",
58
+ "vite": "^7.0.2",
52
59
  "vitest": "^1.2.0"
53
60
  }
54
61
  }