Conversation
aaa1a13 to
4ef1d35
Compare
@neaps/api
@neaps/cli
neaps
@neaps/react
@neaps/tide-predictor
commit: |
bf6214a to
d84bf68
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a new @neaps/react package providing React UI components/hooks for tide predictions (powered by @neaps/api), along with Storybook integration and a browser-based Vitest setup.
Changes:
- Add
packages/reactcomponent library (provider, hooks, components, styles, Storybook stories) and extensive test suite. - Add shared workspace alias resolver (
aliases.ts) and update package Vitest/Vite configs to use it. - Update CI to run Playwright-based browser tests and publish PR builds via
pkg-pr-new.
Reviewed changes
Copilot reviewed 98 out of 99 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Excludes Storybook stories from coverage collection. |
| tsconfig.json | Enables React JSX transform at repo root. |
| packages/react/vitest.config.ts | Adds browser (Playwright) Vitest project config for @neaps/react. |
| packages/react/vite.config.ts | Adds Vite resolve aliases for @neaps/react workspace dev. |
| packages/react/tsdown.config.ts | Adds tsdown build config (CJS/ESM + dts + css copy) for @neaps/react. |
| packages/react/tsconfig.json | React package TS config (DOM libs + react-jsx). |
| packages/react/test/use-current-level.test.ts | Unit tests for timeline interpolation helper. |
| packages/react/test/sun.test.ts | Tests for daylight/night interval utilities. |
| packages/react/test/setup.ts | Test cleanup + localStorage cleanup after each test. |
| packages/react/test/provider.test.tsx | Tests for provider defaults, updates, and persistence. |
| packages/react/test/integration/TideStation.test.tsx | Integration coverage for TideStation rendering/loading/error. |
| packages/react/test/integration/StationSearch.test.tsx | Integration coverage for StationSearch query + selection. |
| packages/react/test/integration/NearbyStations.test.tsx | Integration coverage for NearbyStations loading + selection. |
| packages/react/test/hooks/use-timeline.test.tsx | Hook tests for timeline fetching and error cases. |
| packages/react/test/hooks/use-stations.test.tsx | Hook tests for stations listing/search/proximity. |
| packages/react/test/hooks/use-station.test.tsx | Hook tests for station fetching and disabled behavior. |
| packages/react/test/hooks/use-nearby-stations.test.tsx | Hook tests for nearby-stations behavior and shape. |
| packages/react/test/hooks/use-extremes.test.tsx | Hook tests for extremes fetching and error cases. |
| packages/react/test/helpers.tsx | Adds shared QueryClient + NeapsProvider wrapper for tests. |
| packages/react/test/globalSetup.ts | Starts an in-process @neaps/api for browser tests and provides base URL. |
| packages/react/test/format.test.ts | Tests for formatting/date-key utilities. |
| packages/react/test/defaults.test.ts | Tests for default units/range helpers. |
| packages/react/test/components/YAxisOverlay.test.tsx | Tests for Y-axis overlay rendering/formatting. |
| packages/react/test/components/TideTableFetcher.test.tsx | Tests TideTable grouping/highlighting behaviors. |
| packages/react/test/components/TideTable.test.tsx | Tests TideTable structure and formatting. |
| packages/react/test/components/TideStationHeader.test.tsx | Tests station header rendering + className. |
| packages/react/test/components/TideStation.test.tsx | Component tests for TideStation loading/rendering. |
| packages/react/test/components/TideSettings.test.tsx | Tests settings UI presence and selection changes. |
| packages/react/test/components/TideGraphFull.test.tsx | Broader tests for TideGraph/TideGraphChart rendering cases. |
| packages/react/test/components/TideGraph.test.tsx | Basic TideGraphChart SVG rendering tests. |
| packages/react/test/components/TideCycleGraph.test.tsx | Tests TideCycleGraph rendering/empty behavior. |
| packages/react/test/components/TideConditions.test.tsx | Tests TideConditions and WaterLevelAtTime rendering/states. |
| packages/react/test/components/StationSearch.test.tsx | Tests StationSearch ARIA + recent-search behaviors. |
| packages/react/test/components/StationDisclaimers.test.tsx | Tests disclaimers render/null behavior. |
| packages/react/test/components/NightBands.test.tsx | Tests night-band rendering with/without coordinates. |
| packages/react/test/components/NearbyStations.test.tsx | Tests NearbyStations basic loading/list behavior. |
| packages/react/test/client.test.ts | Tests client URL-building and error handling. |
| packages/react/test/a11y.test.tsx | Adds axe-core accessibility checks for key components. |
| packages/react/src/utils/sun.ts | Adds sunrise/sunset-based daylight/night calculations. |
| packages/react/src/utils/format.ts | Adds formatting helpers for levels/time/date/distance/date-key. |
| packages/react/src/utils/defaults.ts | Adds defaults for units and default date range. |
| packages/react/src/types.ts | Adds public @neaps/react types (station, responses, units, etc.). |
| packages/react/src/styles.css | Adds theme variables and utility styles used by components. |
| packages/react/src/query-keys.ts | Centralizes TanStack Query key builders for library hooks. |
| packages/react/src/provider.tsx | Adds NeapsProvider + persisted config + QueryClient handling. |
| packages/react/src/prefetch.ts | Adds server-side prefetch helpers for dehydrated QueryClient usage. |
| packages/react/src/index.ts | Exposes package public API (types, provider, hooks, components, utils). |
| packages/react/src/hooks/use-timeline.ts | Adds timeline hook wrapping fetchers with provider defaults. |
| packages/react/src/hooks/use-tide-scales.ts | Adds shared scale construction for tide charts. |
| packages/react/src/hooks/use-tide-chunks.ts | Adds chunked timeline/extremes loading for scrollable graphs. |
| packages/react/src/hooks/use-theme-colors.ts | Adds DOM theme color resolution for canvas/map consumers. |
| packages/react/src/hooks/use-stations.ts | Adds stations query hook. |
| packages/react/src/hooks/use-station.ts | Adds station query hook. |
| packages/react/src/hooks/use-nearby-stations.ts | Adds nearby-stations hook via stations endpoint. |
| packages/react/src/hooks/use-extremes.ts | Adds extremes hook wrapping fetchers with provider defaults. |
| packages/react/src/hooks/use-debounced-callback.ts | Adds generic debounced callback hook. |
| packages/react/src/hooks/use-dark-mode.ts | Adds dark-mode tracking via .dark + media query. |
| packages/react/src/hooks/use-current-level.ts | Adds current-level interpolation hook + helper. |
| packages/react/src/hooks/use-container-width.ts | Adds ResizeObserver width hook. |
| packages/react/src/hooks/index.ts | Re-exports all hooks from a central barrel file. |
| packages/react/src/constants.ts | Adds shared constants (half tide cycle). |
| packages/react/src/components/index.ts | Re-exports components from a central barrel file. |
| packages/react/src/components/TideTable.tsx | Implements TideTable (fetch mode + data mode) UI. |
| packages/react/src/components/TideTable.stories.tsx | Storybook stories for TideTable states/layouts. |
| packages/react/src/components/TideStationHeader.tsx | Implements station header (name/region/country/coords). |
| packages/react/src/components/TideStation.tsx | Implements all-in-one station widget composed of subcomponents. |
| packages/react/src/components/TideStation.stories.tsx | Storybook stories for TideStation layouts and states. |
| packages/react/src/components/TideSettings.tsx | Implements units/datum/timezone settings UI persisted via provider. |
| packages/react/src/components/TideGraph/index.ts | Barrel for TideGraph subcomponents. |
| packages/react/src/components/TideGraph/constants.ts | Shared graph layout constants. |
| packages/react/src/components/TideGraph/YAxisOverlay.tsx | Fixed-position y-axis overlay for scrollable graph. |
| packages/react/src/components/TideGraph/TideGraphChart.tsx | Core SVG chart rendering (axes, bands, tooltip, extremes). |
| packages/react/src/components/TideGraph/TideGraph.tsx | Scrollable, chunk-loading tide graph wrapper with “Now” button. |
| packages/react/src/components/TideGraph/TideGraph.stories.tsx | Storybook stories for TideGraph sizing/density/states. |
| packages/react/src/components/TideGraph/NightBands.tsx | Renders night-interval background bands. |
| packages/react/src/components/TideCycleGraph.tsx | Compact cycle view graph around “now”. |
| packages/react/src/components/TideConditions.tsx | Current level + next extreme display, fetch or data-prop modes. |
| packages/react/src/components/TideConditions.stories.tsx | Storybook stories for TideConditions states. |
| packages/react/src/components/StationsMap.stories.tsx | Storybook stories for StationsMap usage. |
| packages/react/src/components/StationSearch.tsx | Implements station autocomplete + recent-search dropdown. |
| packages/react/src/components/StationSearch.stories.tsx | Storybook stories for StationSearch states. |
| packages/react/src/components/StationDisclaimers.tsx | Renders station disclaimer text. |
| packages/react/src/components/NearbyStations.tsx | Implements nearby station list by stationId or coordinates. |
| packages/react/src/components/NearbyStations.stories.tsx | Storybook stories for NearbyStations. |
| packages/react/src/client.ts | Implements API client helpers and URL construction. |
| packages/react/package.json | New package manifest for publishing @neaps/react. |
| packages/react/README.md | Package README with install/usage/styling docs. |
| packages/react/.storybook/theme.ts | Storybook theme config for Neaps branding. |
| packages/react/.storybook/storybook.css | Storybook CSS setup (Tailwind + theme vars). |
| packages/react/.storybook/preview.tsx | Storybook preview decorators (provider + theme switching). |
| packages/react/.storybook/manager.ts | Storybook manager theme selection by system preference. |
| packages/react/.storybook/main.ts | Storybook main config + dev server plugin for local API. |
| packages/neaps/vitest.config.ts | Switches vitest alias config to shared aliases() helper. |
| packages/cli/vitest.config.ts | Switches vitest alias config to shared aliases() helper. |
| packages/api/vitest.config.ts | Switches vitest alias config to shared aliases() helper. |
| package.json | Adds packages/react workspace and pkg-pr-new dev dependency. |
| aliases.ts | Adds shared workspace alias resolver (package name → src entry). |
| .github/workflows/ci.yml | Updates CI name, adds Playwright install, adds pkg.pr.new publish job. |
Comments suppressed due to low confidence (1)
.github/workflows/ci.yml:58
- Typo in comment: "intentonally" should be "intentionally".
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| All hooks must be used within a `<NeapsProvider>`. | ||
|
|
||
| - `useStation(id)` — fetch a single station | ||
| - `useStations({ query?, bbox?, latitude?, longitude? })` — search/list stations (supports bounding box as `[[minLon, minLat], [maxLon, maxLat]]`) |
There was a problem hiding this comment.
The README says useStations supports bbox as a [[minLon, minLat], [maxLon, maxLat]] tuple, but the actual API types expect bbox?: string ("minLon,minLat,maxLon,maxLat") and the client simply stringifies params. Update the docs to match the implemented shape (or update the implementation/types if the tuple form is intended).
| - `useStations({ query?, bbox?, latitude?, longitude? })` — search/list stations (supports bounding box as `[[minLon, minLat], [maxLon, maxLat]]`) | |
| - `useStations({ query?, bbox?, latitude?, longitude? })` — search/list stations (supports bounding box as `"minLon,minLat,maxLon,maxLat"`) |
| export default function setup({ provide }: TestProject) { | ||
| const app = createApp(); | ||
| const server = app.listen(0); | ||
| const address = server.address(); | ||
| const port = typeof address === "object" && address ? address.port : 0; | ||
| const baseUrl = `http://localhost:${port}`; | ||
| provide("apiBaseUrl", baseUrl); |
There was a problem hiding this comment.
setup() calls app.listen(0) and immediately reads server.address(). In Node, server.address() can be null until the server is actually listening, which can result in apiBaseUrl being set to http://localhost:0 and flaky/failed tests. Make the global setup async and wait for the listening event (or pass a callback to listen) before computing the port; similarly, close the server using the callback/awaitable form to ensure teardown completes cleanly.
| const { data: results = [] } = useStations( | ||
| debouncedQuery.length >= 2 ? { query: debouncedQuery } : {}, |
There was a problem hiding this comment.
When debouncedQuery.length < 2, this calls useStations({}), which will issue a request for the full stations list. That can be extremely large and will happen while typing/when the input is focused, creating unnecessary network load and UI lag. Consider disabling the query unless debouncedQuery.length >= 2 (e.g., via the hook's enabled option) and avoid fetching all stations by default here.
| const { data: results = [] } = useStations( | |
| debouncedQuery.length >= 2 ? { query: debouncedQuery } : {}, | |
| const shouldSearch = debouncedQuery.length >= 2; | |
| const { data: results = [] } = useStations( | |
| shouldSearch ? { query: debouncedQuery } : { query: "" }, | |
| { enabled: shouldSearch }, |
| onSelect({ | ||
| id: recent.id, | ||
| name: recent.name, | ||
| region: recent.region, | ||
| country: recent.country, | ||
| latitude: 0, | ||
| longitude: 0, | ||
| continent: "", | ||
| timezone: "", | ||
| type: "reference", | ||
| }); |
There was a problem hiding this comment.
Selecting a recent search synthesizes a StationSummary with placeholder latitude/longitude/continent/timezone values. Downstream consumers of onSelect may rely on these fields (e.g., to center a map or compute distances), so this produces incorrect behavior for recent selections. Either persist the full StationSummary in neaps-recent-searches, or re-fetch the station by id before calling onSelect (or change the callback contract so recent selections can be handled separately).
| <span className="text-sm text-(--neaps-text-muted)"> | ||
| {time.toLocaleString(locale, { | ||
| timeStyle: "short", | ||
| })} | ||
| </span> |
There was a problem hiding this comment.
WaterLevelAtTime formats the time without specifying the timeZone, so the rendered time will be in the viewer's local timezone rather than the station/config timezone used elsewhere in this component. Pass the timezone through and include timeZone: timezone in the toLocaleString options to keep the UI consistent.
| if (!timeline.data || !extremes.data) return null; | ||
|
|
There was a problem hiding this comment.
In fetch mode, errors are not handled: if either query fails, timeline.data / extremes.data will be undefined and the component returns null, leaving a blank UI with no feedback. Add explicit timeline.error / extremes.error handling (similar to other components) and consider rendering a "No tide data" state when responses are empty.
| if (!timeline.data || !extremes.data) return null; | |
| const error = timeline.error ?? extremes.error; | |
| if (error) { | |
| return ( | |
| <div className={`text-(--neaps-text) ${className ?? ""}`}> | |
| <div className="min-h-60 border border-(--neaps-border) rounded-md flex flex-col items-center justify-center text-sm text-(--neaps-text-muted)"> | |
| <span>Error loading tide data.</span> | |
| {"message" in error && error.message ? ( | |
| <span className="mt-1 text-xs">{String(error.message)}</span> | |
| ) : null} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if ( | |
| !timeline.data || | |
| !extremes.data || | |
| !timeline.data.timeline?.length || | |
| !extremes.data.extremes?.length | |
| ) { | |
| return ( | |
| <div className={`text-(--neaps-text) ${className ?? ""}`}> | |
| <div className="min-h-60 border border-(--neaps-border) rounded-md flex items-center justify-center text-sm text-(--neaps-text-muted)"> | |
| No tide data available. | |
| </div> | |
| </div> | |
| ); | |
| } |
| function readCSSVar(name: string, fallback: string): string { | ||
| if (typeof document === "undefined") return fallback; | ||
| const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); | ||
| if (!raw) return fallback; | ||
| return formatHex(raw) ?? fallback; | ||
| } |
There was a problem hiding this comment.
Reading a CSS custom property via getComputedStyle(...).getPropertyValue('--neaps-*') returns the raw token stream (often containing var(...) / light-dark(...)), not a resolved color value. As a result, theme overrides in styles.css are unlikely to convert to a usable hex string here, and the hook will effectively fall back to the hardcoded defaults. Consider resolving the variable by applying color: var(--neaps-...) to a temporary element (or a dedicated hidden element) and reading its computed color, then converting that to hex.
TODO
@neaps/react
React components for tide predictions powered by Neaps.
Installation
Peer dependencies:
npm install react react-dom # Optional — needed for <StationsMap> npm install maplibre-gl react-map-glQuick Start
Wrap your app with
<NeapsProvider>and point it at a running@neaps/apiinstance:Components
Provider
<NeapsProvider>configures the API base URL, default units, and datum for all child components.<TideStation>All-in-one display for a single station — name, graph, and table.
<TideConditions>Current water level, rising/falling indicator, and next extreme. Used internally by
<TideStation>but also available standalone.<TideGraph>Tide level chart. Pass data directly or fetch by station ID.
<TideTable>High/low tide extremes in a table. Pass data directly or fetch by station ID.
<StationSearch>Autocomplete search input for finding stations.
<NearbyStations>List of stations near a given station or coordinates.
<StationsMap>Interactive map showing tide stations within the visible viewport. Requires
maplibre-glandreact-map-gl. Stations are fetched by bounding box as the user pans and zooms.Hooks
All hooks must be used within a
<NeapsProvider>.useStation(id)— fetch a single stationuseStations({ query?, bbox?, latitude?, longitude? })— search/list stations (supports bounding box as[[minLon, minLat], [maxLon, maxLat]])useExtremes({ id, start?, end?, days? })— fetch high/low extremesuseTimeline({ id, start?, end? })— fetch tide level timelineuseNearbyStations({ stationId } | { latitude, longitude })— fetch nearby stationsStyling
Components are styled with Tailwind CSS v4 and CSS custom properties for theming.
With Tailwind
Add
@neaps/reactto your Tailwind content paths so its classes are included in your build:Import the theme variables:
Without Tailwind
Import the pre-built stylesheet which includes all resolved Tailwind utilities:
Theme Variables
Override CSS custom properties to match your brand:
Dark Mode
Dark mode activates when a parent element has the
darkclass or the user's system preference isprefers-color-scheme: dark. Override dark mode colors: