Skip to content

React UI#249

Open
bkeepers wants to merge 50 commits intomainfrom
react-ui
Open

React UI#249
bkeepers wants to merge 50 commits intomainfrom
react-ui

Conversation

@bkeepers
Copy link
Contributor

@bkeepers bkeepers commented Feb 28, 2026

TODO

image

@neaps/react

React components for tide predictions powered by Neaps.

Installation

npm install @neaps/react

Peer dependencies:

npm install react react-dom
# Optional — needed for <StationsMap>
npm install maplibre-gl react-map-gl

Quick Start

Wrap your app with <NeapsProvider> and point it at a running @neaps/api instance:

import { NeapsProvider, TideStation } from "@neaps/react";
import "@neaps/react/styles.css";

function App() {
  return (
    <NeapsProvider baseUrl="https://api.example.com">
      <TideStation id="noaa/8443970" />
    </NeapsProvider>
  );
}

Components

Provider

<NeapsProvider> configures the API base URL, default units, and datum for all child components.

<NeapsProvider baseUrl="https://api.example.com" units="feet" datum="MLLW">
  {children}
</NeapsProvider>

<TideStation>

All-in-one display for a single station — name, graph, and table.

<TideStation id="noaa/8443970" />

<TideConditions>

Current water level, rising/falling indicator, and next extreme. Used internally by <TideStation> but also available standalone.

<TideConditions timeline={timeline} extremes={extremes} units="meters" />

<TideGraph>

Tide level chart. Pass data directly or fetch by station ID.

// Fetch mode
<TideGraph id="noaa/8443970" />

// Data mode
<TideGraph timeline={data} timezone="America/New_York" units="feet" />

<TideTable>

High/low tide extremes in a table. Pass data directly or fetch by station ID.

<TideTable id="noaa/8443970" days={3} />

<StationSearch>

Autocomplete search input for finding stations.

<StationSearch onSelect={(station) => console.log(station)} />

<NearbyStations>

List of stations near a given station or coordinates.

<NearbyStations stationId="noaa/8443970" maxResults={5} />
<NearbyStations latitude={42.35} longitude={-71.05} />

<StationsMap>

Interactive map showing tide stations within the visible viewport. Requires maplibre-gl and react-map-gl. Stations are fetched by bounding box as the user pans and zooms.

<StationsMap
  mapStyle="https://tiles.example.com/style.json"
  onStationSelect={(station) => console.log(station)}
/>

Hooks

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]])
  • useExtremes({ id, start?, end?, days? }) — fetch high/low extremes
  • useTimeline({ id, start?, end? }) — fetch tide level timeline
  • useNearbyStations({ stationId } | { latitude, longitude }) — fetch nearby stations

Styling

Components are styled with Tailwind CSS v4 and CSS custom properties for theming.

With Tailwind

Add @neaps/react to your Tailwind content paths so its classes are included in your build:

/* app.css */
@import "tailwindcss";
@source "../node_modules/@neaps/react/dist";

Import the theme variables:

@import "@neaps/react/styles.css";

Without Tailwind

Import the pre-built stylesheet which includes all resolved Tailwind utilities:

import "@neaps/react/styles.css";

Theme Variables

Override CSS custom properties to match your brand:

:root {
  --neaps-primary: #2563eb;
  --neaps-high: #3b82f6; /* High tide color */
  --neaps-low: #f59e0b; /* Low tide color */
  --neaps-bg: #ffffff;
  --neaps-bg-subtle: #f8fafc;
  --neaps-text: #0f172a;
  --neaps-text-muted: #64748b;
  --neaps-border: #e2e8f0;
}

Dark Mode

Dark mode activates when a parent element has the dark class or the user's system preference is prefers-color-scheme: dark. Override dark mode colors:

.dark {
  --neaps-primary: #60a5fa;
  --neaps-bg: #0f172a;
  --neaps-text: #f1f5f9;
  /* ... */
}

@bkeepers bkeepers force-pushed the react-ui branch 2 times, most recently from aaa1a13 to 4ef1d35 Compare February 28, 2026 20:54
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 1, 2026

Open in StackBlitz

@neaps/api

npm i https://pkg.pr.new/openwatersio/neaps/@neaps/api@249

@neaps/cli

npm i https://pkg.pr.new/openwatersio/neaps/@neaps/cli@249

neaps

npm i https://pkg.pr.new/openwatersio/neaps@249

@neaps/react

npm i https://pkg.pr.new/openwatersio/neaps/@neaps/react@249

@neaps/tide-predictor

npm i https://pkg.pr.new/openwatersio/neaps/@neaps/tide-predictor@249

commit: 638b20d

@bkeepers bkeepers marked this pull request as ready for review March 7, 2026 20:38
@bkeepers bkeepers requested a review from Copilot March 7, 2026 20:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/react component 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]]`)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
- `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"`)

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +10
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);
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
const { data: results = [] } = useStations(
debouncedQuery.length >= 2 ? { query: debouncedQuery } : {},
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const { data: results = [] } = useStations(
debouncedQuery.length >= 2 ? { query: debouncedQuery } : {},
const shouldSearch = debouncedQuery.length >= 2;
const { data: results = [] } = useStations(
shouldSearch ? { query: debouncedQuery } : { query: "" },
{ enabled: shouldSearch },

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +106
onSelect({
id: recent.id,
name: recent.name,
region: recent.region,
country: recent.country,
latitude: 0,
longitude: 0,
continent: "",
timezone: "",
type: "reference",
});
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +102
<span className="text-sm text-(--neaps-text-muted)">
{time.toLocaleString(locale, {
timeStyle: "short",
})}
</span>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +206 to +207
if (!timeline.data || !extremes.data) return null;

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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>
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +45
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;
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
* origin/main:
  chore(deps-dev): Bump tsdown from 0.20.3 to 0.21.0 (#254)
  Update T3, R3, 3N2, and 3L2 constituent definitions from TICON manual (#253)
  Fix incorrect division for milleseconds in JD function (#257)
  chore(deps-dev): Bump @changesets/changelog-github from 0.5.2 to 0.6.0 (#255)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants