@salesforce/webapp-template-app-react-sample-b2e-experimental 1.82.0 → 1.83.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 (47) hide show
  1. package/dist/.a4drules/skills/webapp-csp-trusted-sites/SKILL.md +90 -0
  2. package/dist/.a4drules/skills/webapp-csp-trusted-sites/implementation/metadata-format.md +281 -0
  3. package/dist/.a4drules/skills/{webapp-add-react-component → webapp-react-add-component}/SKILL.md +1 -1
  4. package/dist/.a4drules/skills/webapp-react-data-visualization/SKILL.md +72 -0
  5. package/dist/.a4drules/skills/webapp-react-data-visualization/implementation/dashboard-layout.md +189 -0
  6. package/dist/.a4drules/skills/webapp-react-data-visualization/implementation/donut-chart.md +181 -0
  7. package/dist/.a4drules/skills/webapp-react-data-visualization/implementation/stat-card.md +150 -0
  8. package/dist/.a4drules/skills/webapp-react-interactive-map/SKILL.md +92 -0
  9. package/dist/.a4drules/skills/webapp-react-interactive-map/implementation/geocoding.md +245 -0
  10. package/dist/.a4drules/skills/webapp-react-interactive-map/implementation/leaflet-map.md +279 -0
  11. package/dist/.a4drules/skills/webapp-react-weather-widget/SKILL.md +65 -0
  12. package/dist/.a4drules/skills/webapp-react-weather-widget/implementation/weather-hook.md +258 -0
  13. package/dist/.a4drules/skills/webapp-react-weather-widget/implementation/weather-ui.md +216 -0
  14. package/dist/.a4drules/skills/webapp-ui-ux/SKILL.md +268 -0
  15. package/dist/.a4drules/skills/webapp-ui-ux/data/charts.csv +26 -0
  16. package/dist/.a4drules/skills/webapp-ui-ux/data/colors.csv +97 -0
  17. package/dist/.a4drules/skills/webapp-ui-ux/data/icons.csv +101 -0
  18. package/dist/.a4drules/skills/webapp-ui-ux/data/landing.csv +31 -0
  19. package/dist/.a4drules/skills/webapp-ui-ux/data/products.csv +97 -0
  20. package/dist/.a4drules/skills/webapp-ui-ux/data/react-performance.csv +45 -0
  21. package/dist/.a4drules/skills/webapp-ui-ux/data/stacks/html-tailwind.csv +56 -0
  22. package/dist/.a4drules/skills/webapp-ui-ux/data/stacks/react.csv +54 -0
  23. package/dist/.a4drules/skills/webapp-ui-ux/data/stacks/shadcn.csv +61 -0
  24. package/dist/.a4drules/skills/webapp-ui-ux/data/styles.csv +68 -0
  25. package/dist/.a4drules/skills/webapp-ui-ux/data/typography.csv +58 -0
  26. package/dist/.a4drules/skills/webapp-ui-ux/data/ui-reasoning.csv +101 -0
  27. package/dist/.a4drules/skills/webapp-ui-ux/data/ux-guidelines.csv +100 -0
  28. package/dist/.a4drules/skills/webapp-ui-ux/data/web-interface.csv +31 -0
  29. package/dist/.a4drules/skills/webapp-ui-ux/scripts/core.js +255 -0
  30. package/dist/.a4drules/skills/webapp-ui-ux/scripts/design_system.js +861 -0
  31. package/dist/.a4drules/skills/webapp-ui-ux/scripts/search.js +98 -0
  32. package/dist/.a4drules/skills/webapp-unsplash-images/SKILL.md +71 -0
  33. package/dist/.a4drules/skills/webapp-unsplash-images/implementation/usage.md +159 -0
  34. package/dist/.a4drules/webapp-no-node-e.md +54 -15
  35. package/dist/.a4drules/webapp-react.md +9 -10
  36. package/dist/.a4drules/webapp-skills-first.md +26 -0
  37. package/dist/.a4drules/webapp.md +8 -0
  38. package/dist/CHANGELOG.md +11 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +4 -4
  40. package/dist/package.json +1 -1
  41. package/package.json +3 -3
  42. package/dist/.a4drules/webapp-images.md +0 -15
  43. /package/dist/.a4drules/skills/{webapp-add-react-component → webapp-react-add-component}/implementation/component.md +0 -0
  44. /package/dist/.a4drules/skills/{webapp-add-react-component → webapp-react-add-component}/implementation/header-footer.md +0 -0
  45. /package/dist/.a4drules/skills/{webapp-add-react-component → webapp-react-add-component}/implementation/page.md +0 -0
  46. /package/dist/.a4drules/{webapp-code-quality.md → webapp-react-code-quality.md} +0 -0
  47. /package/dist/.a4drules/{webapp-typescript.md → webapp-react-typescript.md} +0 -0
@@ -0,0 +1,279 @@
1
+ # Leaflet Map — Implementation Guide
2
+
3
+ ## Dependencies
4
+
5
+ Add to the project:
6
+
7
+ ```bash
8
+ npm install leaflet react-leaflet
9
+ npm install -D @types/leaflet
10
+ ```
11
+
12
+ Import the Leaflet CSS in the component file (not in global CSS — keeps it co-located):
13
+
14
+ ```tsx
15
+ import "leaflet/dist/leaflet.css";
16
+ ```
17
+
18
+ ---
19
+
20
+ ## TypeScript declaration (if @types/leaflet is unavailable)
21
+
22
+ If the project cannot install `@types/leaflet`, create a declaration file:
23
+
24
+ ```ts
25
+ // types/leaflet.d.ts
26
+ declare module "leaflet" {
27
+ const L: unknown;
28
+ export default L;
29
+ }
30
+ declare module "leaflet/dist/leaflet.css";
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Reusable map component
36
+
37
+ Create a generic map component at `components/MapView.tsx` (or similar):
38
+
39
+ ```tsx
40
+ import { useMemo, useState, useEffect } from "react";
41
+ import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
42
+ import L from "leaflet";
43
+ import "leaflet/dist/leaflet.css";
44
+
45
+ const Leaflet = L as {
46
+ divIcon: (opts: {
47
+ className?: string;
48
+ html?: string;
49
+ iconSize?: [number, number];
50
+ iconAnchor?: [number, number];
51
+ popupAnchor?: [number, number];
52
+ }) => unknown;
53
+ };
54
+
55
+ const pinIcon = Leaflet.divIcon({
56
+ className: "map-pin",
57
+ html: `<span class="map-pin-shape" aria-hidden="true"></span>`,
58
+ iconSize: [28, 40],
59
+ iconAnchor: [14, 40],
60
+ popupAnchor: [0, -40],
61
+ });
62
+
63
+ export interface MapMarker {
64
+ lat: number;
65
+ lng: number;
66
+ label?: string;
67
+ }
68
+
69
+ interface MapViewProps {
70
+ center: [number, number];
71
+ zoom?: number;
72
+ markers?: MapMarker[];
73
+ className?: string;
74
+ }
75
+
76
+ function MapCenterUpdater({ center, zoom = 13 }: { center: [number, number]; zoom?: number }) {
77
+ const map = useMap() as { setView: (center: [number, number], zoom: number) => void };
78
+ useEffect(() => {
79
+ map.setView(center, zoom);
80
+ }, [map, center[0], center[1], zoom]);
81
+ return null;
82
+ }
83
+
84
+ export default function MapView({
85
+ center,
86
+ zoom = 13,
87
+ markers = [],
88
+ className = "h-[400px] w-full rounded-xl overflow-hidden",
89
+ }: MapViewProps) {
90
+ const [mounted, setMounted] = useState(false);
91
+ useEffect(() => {
92
+ setMounted(true);
93
+ }, []);
94
+
95
+ const effectiveCenter = useMemo((): [number, number] => {
96
+ if (markers.length > 0) {
97
+ const sum = markers.reduce((acc, m) => [acc[0] + m.lat, acc[1] + m.lng], [0, 0]);
98
+ return [sum[0] / markers.length, sum[1] / markers.length];
99
+ }
100
+ return center;
101
+ }, [center, markers]);
102
+
103
+ if (!mounted || typeof window === "undefined") {
104
+ return (
105
+ <div
106
+ className={className + " bg-muted flex items-center justify-center text-muted-foreground text-sm"}
107
+ aria-hidden
108
+ >
109
+ Loading map…
110
+ </div>
111
+ );
112
+ }
113
+
114
+ return (
115
+ <div className={className} aria-label="Map">
116
+ <MapContainer
117
+ center={effectiveCenter}
118
+ zoom={zoom}
119
+ scrollWheelZoom
120
+ className="h-full w-full"
121
+ style={{ minHeight: 200 }}
122
+ >
123
+ <MapCenterUpdater center={effectiveCenter} zoom={zoom} />
124
+ <TileLayer
125
+ attribution='Data by &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
126
+ url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
127
+ maxZoom={19}
128
+ />
129
+ {markers.map((m, i) => (
130
+ <Marker key={`${m.lat}-${m.lng}-${i}`} position={[m.lat, m.lng]} icon={pinIcon}>
131
+ <Popup>{m.label ?? "Location"}</Popup>
132
+ </Marker>
133
+ ))}
134
+ </MapContainer>
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Tile provider: OpenStreetMap
143
+
144
+ The default tile URL is free and requires no API key:
145
+
146
+ ```
147
+ https://tile.openstreetmap.org/{z}/{x}/{y}.png
148
+ ```
149
+
150
+ | Property | Value |
151
+ |----------|-------|
152
+ | Cost | Free |
153
+ | API key | None |
154
+ | Max zoom | 19 |
155
+ | Attribution | Required — `© OpenStreetMap` |
156
+
157
+ CSP `img-src` must include `https://tile.openstreetmap.org` if CSP is enforced.
158
+
159
+ ---
160
+
161
+ ## Custom pin icon via CSS
162
+
163
+ The `divIcon` approach avoids external marker image dependencies. Add these styles to your global CSS or a co-located CSS file:
164
+
165
+ ```css
166
+ .map-pin {
167
+ background: transparent !important;
168
+ border: none !important;
169
+ }
170
+
171
+ .map-pin-shape {
172
+ display: block;
173
+ width: 28px;
174
+ height: 40px;
175
+ background: hsl(var(--primary));
176
+ border-radius: 50% 50% 50% 0;
177
+ transform: rotate(-45deg);
178
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
179
+ }
180
+
181
+ .map-pin-shape::after {
182
+ content: "";
183
+ display: block;
184
+ width: 12px;
185
+ height: 12px;
186
+ background: white;
187
+ border-radius: 50%;
188
+ position: absolute;
189
+ top: 50%;
190
+ left: 50%;
191
+ transform: translate(-50%, -50%);
192
+ }
193
+ ```
194
+
195
+ ---
196
+
197
+ ## SSR safety
198
+
199
+ Leaflet requires `window` and `document`. The component guards against SSR with:
200
+
201
+ 1. A `mounted` state that only becomes `true` in `useEffect` (client-side).
202
+ 2. A `typeof window === "undefined"` check.
203
+ 3. A loading placeholder rendered during SSR / before mount.
204
+
205
+ This prevents "window is not defined" errors in SSR or static builds.
206
+
207
+ ---
208
+
209
+ ## MapCenterUpdater pattern
210
+
211
+ `react-leaflet`'s `MapContainer` does not re-center when the `center` prop changes after initial render. The `MapCenterUpdater` child component uses `useMap()` to call `map.setView()` reactively:
212
+
213
+ ```tsx
214
+ function MapCenterUpdater({ center, zoom = 13 }: { center: [number, number]; zoom?: number }) {
215
+ const map = useMap();
216
+ useEffect(() => {
217
+ map.setView(center, zoom);
218
+ }, [map, center[0], center[1], zoom]);
219
+ return null;
220
+ }
221
+ ```
222
+
223
+ Place it as a direct child of `<MapContainer>`.
224
+
225
+ ---
226
+
227
+ ## Multi-marker center calculation
228
+
229
+ When rendering multiple markers, auto-center the map on their centroid:
230
+
231
+ ```tsx
232
+ const effectiveCenter = useMemo((): [number, number] => {
233
+ if (markers.length > 0) {
234
+ const sum = markers.reduce((acc, m) => [acc[0] + m.lat, acc[1] + m.lng], [0, 0]);
235
+ return [sum[0] / markers.length, sum[1] / markers.length];
236
+ }
237
+ return fallbackCenter;
238
+ }, [markers, fallbackCenter]);
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Split-panel layout (map + list)
244
+
245
+ For a search page with map on the left and scrollable results on the right:
246
+
247
+ ```tsx
248
+ <div className="flex h-[calc(100vh-4rem)] min-h-[500px] flex-col">
249
+ {/* Search bar */}
250
+ <div className="shrink-0 border-b bg-background px-4 py-3">
251
+ {/* search input */}
252
+ </div>
253
+ {/* Map + List */}
254
+ <div className="flex min-h-0 flex-1 flex-col lg:flex-row">
255
+ <div className="h-64 shrink-0 lg:h-full lg:min-h-0 lg:w-2/3" aria-label="Map">
256
+ <MapView center={center} markers={markers} className="h-full w-full" />
257
+ </div>
258
+ <aside className="flex w-full flex-col border-t lg:w-1/3 lg:border-l lg:border-t-0">
259
+ <div className="flex-1 overflow-y-auto p-4">
260
+ {/* list items */}
261
+ </div>
262
+ </aside>
263
+ </div>
264
+ </div>
265
+ ```
266
+
267
+ Key points:
268
+ - `h-[calc(100vh-4rem)]` fills viewport minus header.
269
+ - `lg:flex-row` stacks vertically on mobile, side-by-side on desktop.
270
+ - List panel uses `overflow-y-auto` for independent scrolling.
271
+
272
+ ---
273
+
274
+ ## Accessibility
275
+
276
+ - Wrap the map `<div>` with `aria-label="Map"` or a descriptive label.
277
+ - Pin icon inner HTML uses `aria-hidden="true"`.
278
+ - Popups contain text labels for each marker.
279
+ - Provide a text-based alternative (address list) for screen readers.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: webapp-react-weather-widget
3
+ description: Adds a weather widget to React pages using the free Open-Meteo API. Use when the user asks to add weather, show a forecast, display current conditions, add a weather card, or integrate weather data into the web application.
4
+ ---
5
+
6
+ # Weather Widget
7
+
8
+ ## When to Use
9
+
10
+ Use this skill when:
11
+ - Adding current weather conditions to a dashboard or sidebar
12
+ - Displaying an hourly or daily forecast
13
+ - Showing location-based weather for a property, event, or destination
14
+
15
+ ---
16
+
17
+ ## Step 1 — Determine widget scope
18
+
19
+ Identify what weather data the user needs:
20
+
21
+ - **Current conditions only** — temperature, description, wind, humidity
22
+ - **Current + hourly forecast** — conditions + next 6–12 hours
23
+ - **Current + daily forecast** — conditions + multi-day outlook
24
+
25
+ If unclear, ask:
26
+
27
+ > "Should the weather widget show just current conditions, or include an hourly/daily forecast too?"
28
+
29
+ ---
30
+
31
+ ## Step 2 — Determine location source
32
+
33
+ The widget needs a latitude/longitude. Identify where this comes from:
34
+
35
+ - **Fixed default** — hardcoded city (e.g. San Francisco: `37.7749, -122.4194`)
36
+ - **User's location** — browser Geolocation API
37
+ - **Address-based** — geocode an address to lat/lng (see the `webapp-react-interactive-map` skill for geocoding)
38
+ - **Prop-driven** — parent component passes lat/lng
39
+
40
+ ---
41
+
42
+ ## Step 3 — Implementation
43
+
44
+ Read the corresponding guides:
45
+
46
+ - **Weather data hook** — read `implementation/weather-hook.md` for the `useWeather` custom hook and Open-Meteo API integration.
47
+ - **Weather UI component** — read `implementation/weather-ui.md` for rendering weather data in a card.
48
+
49
+ ---
50
+
51
+ ## Verification
52
+
53
+ Before completing:
54
+
55
+ 1. Widget shows real weather data (not mocked).
56
+ 2. Loading and error states are handled gracefully.
57
+ 3. Temperature displays in the correct unit (°F or °C).
58
+ 4. Run from the web app directory:
59
+
60
+ ```bash
61
+ cd force-app/main/default/webapplications/<appName> && npm run lint && npm run build
62
+ ```
63
+
64
+ - **Lint:** MUST result in 0 errors.
65
+ - **Build:** MUST succeed.
@@ -0,0 +1,258 @@
1
+ # Weather Hook — Implementation Guide
2
+
3
+ ## Recommended API: Open-Meteo
4
+
5
+ | Property | Value |
6
+ |----------|-------|
7
+ | Endpoint | `https://api.open-meteo.com/v1/forecast` |
8
+ | Cost | Free for non-commercial use; no signup |
9
+ | API key | **None required** |
10
+ | Rate limit | 10,000 requests/day |
11
+ | Data | Current conditions, hourly, daily, historical |
12
+ | Weather codes | WMO standard codes |
13
+
14
+ ### Why Open-Meteo over alternatives
15
+
16
+ | Provider | API key | Free tier | Notes |
17
+ |----------|---------|-----------|-------|
18
+ | **Open-Meteo** | No | 10K/day | Best for prototypes and low-traffic; no auth required |
19
+ | OpenWeatherMap | Yes | 1K calls/day | Popular but requires signup and API key |
20
+ | WeatherAPI.com | Yes | 1M calls/month | Good free tier but requires key management |
21
+ | Visual Crossing | Yes | 1K calls/day | Historical data strength |
22
+
23
+ Open-Meteo is the default choice because it requires zero configuration.
24
+
25
+ ---
26
+
27
+ ## WMO weather codes
28
+
29
+ Open-Meteo returns standard WMO weather codes. Map them to human-readable labels:
30
+
31
+ ```ts
32
+ const WEATHER_LABELS: Record<number, string> = {
33
+ 0: "Clear",
34
+ 1: "Mainly clear",
35
+ 2: "Partly cloudy",
36
+ 3: "Overcast",
37
+ 45: "Foggy",
38
+ 48: "Depositing rime fog",
39
+ 51: "Light drizzle",
40
+ 53: "Drizzle",
41
+ 55: "Dense drizzle",
42
+ 61: "Slight rain",
43
+ 63: "Rain",
44
+ 65: "Heavy rain",
45
+ 71: "Slight snow",
46
+ 73: "Snow",
47
+ 75: "Heavy snow",
48
+ 77: "Snow grains",
49
+ 80: "Slight rain showers",
50
+ 81: "Rain showers",
51
+ 82: "Heavy rain showers",
52
+ 85: "Slight snow showers",
53
+ 86: "Heavy snow showers",
54
+ 95: "Thunderstorm",
55
+ 96: "Thunderstorm + hail",
56
+ 99: "Thunderstorm + heavy hail",
57
+ };
58
+
59
+ function weatherLabel(code: number): string {
60
+ return WEATHER_LABELS[code] ?? "Unknown";
61
+ }
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Temperature conversion
67
+
68
+ Open-Meteo returns Celsius by default. Convert to Fahrenheit when needed:
69
+
70
+ ```ts
71
+ function celsiusToFahrenheit(c: number): number {
72
+ return Math.round((c * 9) / 5 + 32);
73
+ }
74
+ ```
75
+
76
+ Alternatively, request Fahrenheit directly via `&temperature_unit=fahrenheit` in the query string.
77
+
78
+ ---
79
+
80
+ ## TypeScript interfaces
81
+
82
+ ```ts
83
+ export interface WeatherCurrent {
84
+ description: string;
85
+ tempF: number;
86
+ humidity: number;
87
+ windSpeedKmh: number;
88
+ windSpeedMph: number;
89
+ }
90
+
91
+ export interface WeatherHour {
92
+ time: string;
93
+ tempF: number;
94
+ }
95
+
96
+ export interface WeatherData {
97
+ current: WeatherCurrent;
98
+ hourly: WeatherHour[];
99
+ timezone: string;
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## The useWeather hook
106
+
107
+ Create at `hooks/useWeather.ts`:
108
+
109
+ ```ts
110
+ import { useState, useEffect } from "react";
111
+
112
+ const DEFAULT_LAT = 37.7749;
113
+ const DEFAULT_LNG = -122.4194;
114
+
115
+ async function fetchWeather(lat: number, lng: number): Promise<WeatherData> {
116
+ const url = new URL("https://api.open-meteo.com/v1/forecast");
117
+ url.searchParams.set("latitude", String(lat));
118
+ url.searchParams.set("longitude", String(lng));
119
+ url.searchParams.set("current", "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m");
120
+ url.searchParams.set("hourly", "temperature_2m");
121
+ url.searchParams.set("timezone", "auto");
122
+ url.searchParams.set("forecast_days", "1");
123
+
124
+ const res = await fetch(url.toString());
125
+ if (!res.ok) throw new Error(`Weather API error: ${res.status}`);
126
+ const data = await res.json();
127
+
128
+ const cur = data.current ?? {};
129
+ const tempC = cur.temperature_2m ?? 0;
130
+ const humidity = cur.relative_humidity_2m ?? 0;
131
+ const windKmh = cur.wind_speed_10m ?? 0;
132
+ const windMph = Math.round(windKmh * 0.621371 * 10) / 10;
133
+ const code = cur.weather_code ?? 0;
134
+
135
+ const hourly: WeatherHour[] = [];
136
+ const times: string[] = data.hourly?.time ?? [];
137
+ const temps: (number | null)[] = data.hourly?.temperature_2m ?? [];
138
+ const now = new Date();
139
+
140
+ for (let i = 0; i < times.length; i++) {
141
+ const t = times[i];
142
+ const temp = temps[i];
143
+ if (t && temp != null) {
144
+ const d = new Date(t);
145
+ if (d >= now) {
146
+ hourly.push({
147
+ time: d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }),
148
+ tempF: celsiusToFahrenheit(temp),
149
+ });
150
+ }
151
+ }
152
+ if (hourly.length >= 6) break;
153
+ }
154
+
155
+ return {
156
+ current: {
157
+ description: weatherLabel(code),
158
+ tempF: celsiusToFahrenheit(tempC),
159
+ humidity,
160
+ windSpeedKmh: windKmh,
161
+ windSpeedMph: windMph,
162
+ },
163
+ hourly,
164
+ timezone: data.timezone ?? "auto",
165
+ };
166
+ }
167
+
168
+ export function useWeather(lat?: number | null, lng?: number | null) {
169
+ const latitude = lat ?? DEFAULT_LAT;
170
+ const longitude = lng ?? DEFAULT_LNG;
171
+
172
+ const [data, setData] = useState<WeatherData | null>(null);
173
+ const [loading, setLoading] = useState(true);
174
+ const [error, setError] = useState<string | null>(null);
175
+
176
+ useEffect(() => {
177
+ let cancelled = false;
178
+ setLoading(true);
179
+ setError(null);
180
+ fetchWeather(latitude, longitude)
181
+ .then((d) => {
182
+ if (!cancelled) setData(d);
183
+ })
184
+ .catch((e) => {
185
+ if (!cancelled) setError(e instanceof Error ? e.message : "Failed to load weather");
186
+ })
187
+ .finally(() => {
188
+ if (!cancelled) setLoading(false);
189
+ });
190
+ return () => {
191
+ cancelled = true;
192
+ };
193
+ }, [latitude, longitude]);
194
+
195
+ return { data, loading, error };
196
+ }
197
+ ```
198
+
199
+ ### Hook design patterns
200
+
201
+ | Pattern | Why |
202
+ |---------|-----|
203
+ | Cancellation flag (`cancelled`) | Prevents state updates on unmounted components |
204
+ | Default coordinates | Widget works immediately without user location |
205
+ | Dependency array `[latitude, longitude]` | Re-fetches only when location changes |
206
+ | Separate `loading` and `error` states | Enables distinct UI for each state |
207
+
208
+ ---
209
+
210
+ ## Open-Meteo API parameters reference
211
+
212
+ ### Current weather variables
213
+
214
+ | Variable | Description |
215
+ |----------|-------------|
216
+ | `temperature_2m` | Air temperature at 2m height (°C) |
217
+ | `relative_humidity_2m` | Relative humidity (%) |
218
+ | `weather_code` | WMO weather code |
219
+ | `wind_speed_10m` | Wind speed at 10m height (km/h) |
220
+ | `apparent_temperature` | Feels-like temperature (°C) |
221
+ | `precipitation` | Precipitation sum (mm) |
222
+
223
+ ### Hourly variables
224
+
225
+ | Variable | Description |
226
+ |----------|-------------|
227
+ | `temperature_2m` | Temperature each hour |
228
+ | `precipitation_probability` | Chance of rain (%) |
229
+ | `weather_code` | Condition each hour |
230
+
231
+ ### Daily variables
232
+
233
+ | Variable | Description |
234
+ |----------|-------------|
235
+ | `temperature_2m_max` | Daily high |
236
+ | `temperature_2m_min` | Daily low |
237
+ | `weather_code` | Dominant condition |
238
+ | `sunrise` | Sunrise time (ISO) |
239
+ | `sunset` | Sunset time (ISO) |
240
+
241
+ ### Useful query parameters
242
+
243
+ | Param | Example | Purpose |
244
+ |-------|---------|---------|
245
+ | `timezone` | `auto` or `America/Los_Angeles` | Localizes times |
246
+ | `forecast_days` | `1`–`16` | Number of days |
247
+ | `temperature_unit` | `fahrenheit` | Direct °F responses |
248
+ | `wind_speed_unit` | `mph` | Direct mph responses |
249
+
250
+ ---
251
+
252
+ ## CSP considerations
253
+
254
+ If CSP is enforced, add Open-Meteo to `connect-src`:
255
+
256
+ ```
257
+ connect-src 'self' https://api.open-meteo.com;
258
+ ```