+
{percent(r.calm)}
diff --git a/web/src/components/map/measured-spark.tsx b/web/src/components/map/measured-spark.tsx
index fc9d7ad..3f9e26b 100644
--- a/web/src/components/map/measured-spark.tsx
+++ b/web/src/components/map/measured-spark.tsx
@@ -5,6 +5,7 @@ import { spark } from '@/lib/spark';
import { formatWind, UNIT_LABEL } from '@/lib/units';
import { useNow } from '@/lib/use-now';
import { m } from '@/paraglide/messages';
+import { NON_SCALING_STROKE } from '@/ui/svg-style';
const HOURS = 6;
const W = 240;
@@ -34,7 +35,7 @@ export function MeasuredSpark({ slug, unit }: { slug: string; unit: WindUnit })
d={s.line}
className="fill-none stroke-chart-wind"
strokeWidth={1.5}
- vectorEffect="non-scaling-stroke"
+ style={NON_SCALING_STROKE}
strokeLinejoin="round"
/>
{s.gusts.map((g) => (
diff --git a/web/src/components/water-tile.tsx b/web/src/components/water-tile.tsx
index 27003a3..5ead450 100644
--- a/web/src/components/water-tile.tsx
+++ b/web/src/components/water-tile.tsx
@@ -10,6 +10,7 @@ import { cn } from '@/lib/utils';
import { levelTrend, type Trend } from '@/lib/water';
import { m } from '@/paraglide/messages';
import { Card, CardHeader, CardMeta, CardTitle } from '@/ui/card';
+import { NON_SCALING_STROKE } from '@/ui/svg-style';
/** The gauge near a place (within 15 km): level and its week, flood state, flow, current. */
export function WaterTile({ lat, lon, timeZone }: { lat: number; lon: number; timeZone: string }) {
@@ -190,7 +191,7 @@ function History({ points }: { points: LevelPoint[] }) {
strokeWidth="2"
strokeLinejoin="round"
strokeLinecap="round"
- vectorEffect="non-scaling-stroke"
+ style={NON_SCALING_STROKE}
/>
diff --git a/web/src/ui/svg-style.ts b/web/src/ui/svg-style.ts
new file mode 100644
index 0000000..b38af03
--- /dev/null
+++ b/web/src/ui/svg-style.ts
@@ -0,0 +1,11 @@
+import type { CSSProperties } from 'react';
+
+// SVG presentation attributes as CSS, which React and TanStack Redact both write as `style`.
+// Redact 0.3.1 hyphenates only clip*, fill* and stroke* attributes (attributeName() in
+// src/core/attributes.ts); `textAnchor` and `vectorEffect` reach the DOM in camelCase and do nothing.
+
+/** Text centred on its x. */
+export const TEXT_CENTRED: CSSProperties = { textAnchor: 'middle' };
+
+/** A stroke as wide on screen however the viewBox is stretched. */
+export const NON_SCALING_STROKE: CSSProperties = { vectorEffect: 'non-scaling-stroke' };
diff --git a/web/src/ui/wind-compass.tsx b/web/src/ui/wind-compass.tsx
index 2293bff..bd2c3a5 100644
--- a/web/src/ui/wind-compass.tsx
+++ b/web/src/ui/wind-compass.tsx
@@ -4,6 +4,7 @@ import { inSector } from '@/lib/favorites';
import { compass } from '@/lib/format';
import { cn } from '@/lib/utils';
import { m } from '@/paraglide/messages';
+import { TEXT_CENTRED } from '@/ui/svg-style';
const C = 60;
/** Every 10°, longer every 90° */
@@ -82,7 +83,7 @@ export function WindCompass({ dir, calm, range, sector, className }: Props) {
x={x}
y={y}
dy="0.35em"
- textAnchor="middle"
+ style={TEXT_CENTRED}
className={cn('font-semibold text-[11px]', d === 0 ? 'fill-foreground' : 'fill-muted-foreground')}
>
{points[i * 4]}
@@ -114,7 +115,7 @@ export function WindCompass({ dir, calm, range, sector, className }: Props) {
)}
{dir === null && (
-
+
{calm ? '0' : 'VRB'}
)}
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 9eec5a4..ffd81cd 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -2,6 +2,7 @@
import { fileURLToPath } from 'node:url';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
import tailwindcss from '@tailwindcss/vite';
+import { redact } from '@tanstack/redact/vite';
import { tanstackRouter } from '@tanstack/router-plugin/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
@@ -20,6 +21,10 @@ export default defineConfig({
// src/routes/* → src/routeTree.gen.ts; has to run before the React plugin.
tanstackRouter({ target: 'react', autoCodeSplitting: true }),
react(),
+ // TanStack Redact in place of React (https://github.com/TanStack/redact): the code keeps
+ // importing `react` and `react-dom`, the plugin points those imports at the smaller runtime.
+ // It renders synchronously; nothing here relies on concurrent rendering.
+ redact(),
tailwindcss(),
],
resolve: {