diff --git a/app/src/components/layout/CenterCanvas.svelte b/app/src/components/layout/CenterCanvas.svelte
index 7a05bb5..b326473 100644
--- a/app/src/components/layout/CenterCanvas.svelte
+++ b/app/src/components/layout/CenterCanvas.svelte
@@ -9,6 +9,7 @@
import VideoBackdrop from '../canvas/VideoBackdrop.svelte'
import WysiwygLayer from '../canvas/WysiwygLayer.svelte'
import PlaybackControls from '../canvas/PlaybackControls.svelte'
+ import RiderWeightPrompt from '../ui/RiderWeightPrompt.svelte'
import VideoAlignmentBar from '../canvas/VideoAlignmentBar.svelte'
import { videoStartOnAxis } from '@/lib/videoAlignment.js'
import { isLapMetric } from '@/lib/elementTypes.js'
@@ -743,11 +744,31 @@
+
+ {#if app.usesRiderWeight && app.hasActivity}
+
+
e.stopPropagation()}
+ onwheel={(e) => e.stopPropagation()}
+ >
+
+
+ {/if}
+
{#if app.config && app.hasActivity}
boolean — true when the codec has no same-machine
// measurement yet, so it offers a quick test render
testAvailableFor = null,
@@ -60,6 +64,13 @@
})
function onKeydown(e) {
+ // Enter inside a field (the rider-weight prompt) edits, it doesn't render.
+ const t = e.target
+ if (
+ e.key === 'Enter' &&
+ (t?.tagName === 'INPUT' || t?.tagName === 'SELECT')
+ )
+ return
if (e.key === 'Escape') {
e.preventDefault()
oncancel?.()
@@ -251,6 +262,15 @@
{/if}
+
+ {#if usesRiderWeight}
+
+ {/if}
+
oncancel?.()}
diff --git a/app/src/components/overlays/Settings.svelte b/app/src/components/overlays/Settings.svelte
index 3483274..3f206fc 100644
--- a/app/src/components/overlays/Settings.svelte
+++ b/app/src/components/overlays/Settings.svelte
@@ -4,6 +4,7 @@
import { X } from 'lucide-svelte'
import { formatHomePath } from '../../lib/utils.js'
import Select from '../ui/Select.svelte'
+ import RiderWeightField from '../ui/RiderWeightField.svelte'
const app = getContext('app')
@@ -87,29 +88,7 @@
Used only for the W/kg metric. Stored on this device — never saved to
templates.
-
-
(app.riderWeight = e.target.value)}
- class="min-w-0 flex-1 h-7 rounded-[6px] border-0 bg-[var(--panel2)] px-2 text-xs
- text-foreground font-mono focus:outline-none focus:ring-1 focus:ring-ring"
- />
-
- (app.riderWeightUnit = v)}
- />
-
-
+
diff --git a/app/src/components/panels/ElementList.svelte b/app/src/components/panels/ElementList.svelte
index 024c8d8..de9f037 100644
--- a/app/src/components/panels/ElementList.svelte
+++ b/app/src/components/panels/ElementList.svelte
@@ -17,6 +17,7 @@
Square,
Thermometer,
Trash2,
+ TriangleAlert,
Type,
X,
} from 'lucide-svelte'
@@ -28,6 +29,11 @@
const app = getContext('app')
+ // W/kg with no rider weight renders 0 — flag those rows so the problem shows
+ // up in the list, not just in the selected element's properties.
+ const missingRiderWeight = (metric) =>
+ metric === 'power_to_weight' && app.riderWeightKg == null
+
let addImagePending = $state(false)
// ── Drag state ───────────────────────────────────────────────────────────────
@@ -63,7 +69,7 @@
const byId = {}
for (const el of app.config.elements) {
const meta = elementMeta(el)
- byId[el.id] = { id: el.id, type: meta.kind, name: meta.name, unit: meta.unit }
+ byId[el.id] = { id: el.id, type: meta.kind, name: meta.name, unit: meta.unit, metric: el.value }
}
return [...(app.elementLayerOrder ?? [])].reverse().map((id) => byId[id]).filter(Boolean)
})
@@ -587,6 +593,11 @@
{#if el.unit}
{el.unit === 'imperial' ? 'imp' : el.unit}
{/if}
+ {#if missingRiderWeight(el.metric)}
+
+
+
+ {/if}
{el.unit === 'imperial' ? 'imp' : el.unit}
{/if}
+ {#if missingRiderWeight(el.metric)}
+
+
+
+ {/if}
update('value', v)}
/>
+ {#if item.value === 'power_to_weight'}
+
+ {/if}
{#if isSummaryMetric(item.value)}
@@ -1755,6 +1759,9 @@ Looks unrealistic for ${item.value} (expected ${issue.expected}). Enter a manual
options={METER_METRICS.map((m) => ({ value: m, label: metricLabel(m) }))}
onchange={(v) => update('value', v)}
/>
+ {#if item.value === 'power_to_weight'}
+
+ {/if}
{#if UNITS_BY_METRIC[item.value]}
Unit
@@ -2013,6 +2020,9 @@ Looks unrealistic for ${item.value} (expected ${issue.expected}). Enter a manual
options={METER_METRICS.map((m) => ({ value: m, label: metricLabel(m) }))}
onchange={(v) => update('value', v)}
/>
+ {#if item.value === 'power_to_weight'}
+
+ {/if}
{#if UNITS_BY_METRIC[item.value]}
Unit
diff --git a/app/src/components/ui/RiderWeightField.svelte b/app/src/components/ui/RiderWeightField.svelte
new file mode 100644
index 0000000..aa938b3
--- /dev/null
+++ b/app/src/components/ui/RiderWeightField.svelte
@@ -0,0 +1,115 @@
+
+
+
+
+
{
+ draft = e.target.value
+ error = null
+ }}
+ onfocus={() => (editing = true)}
+ onblur={() => {
+ editing = false
+ commit()
+ }}
+ onkeydown={onKeydown}
+ class="w-20 h-7 rounded-[6px] border-0 bg-[var(--panel2)] px-2 text-xs
+ text-foreground font-mono focus:outline-none focus:ring-1
+ {error ? 'ring-1 ring-destructive focus:ring-destructive' : 'focus:ring-ring'}"
+ />
+
+
+
+
+ {#if error}
+
{error}
+ {/if}
+
diff --git a/app/src/components/ui/RiderWeightPrompt.svelte b/app/src/components/ui/RiderWeightPrompt.svelte
new file mode 100644
index 0000000..d2b1de0
--- /dev/null
+++ b/app/src/components/ui/RiderWeightPrompt.svelte
@@ -0,0 +1,161 @@
+
+
+{#snippet box()}
+
+
+ {#if needed}
+
+ {:else}
+
+ {/if}
+
+ {headline}
+
+
+
+
+
+ {#if !compact && needed}
+
+ Stored on this device only — never saved into templates.
+
+ {/if}
+
+{/snippet}
+
+{#if showing}
+ {#if floating}
+
+
+ {@render box()}
+
+ {:else}
+
+
+ {/if}
+{/if}
diff --git a/app/src/state/appState.svelte.js b/app/src/state/appState.svelte.js
index 85e68e1..742daca 100644
--- a/app/src/state/appState.svelte.js
+++ b/app/src/state/appState.svelte.js
@@ -114,6 +114,20 @@ export function createAppState() {
Number.isFinite(storedRiderWeight) ? storedRiderWeight : null,
)
let riderWeightUnit = $state(localStorage.getItem('riderWeightUnit') ?? 'kg')
+ // Weight in kilograms for the renderer, or null when unset/invalid.
+ function resolvedRiderWeightKg() {
+ if (
+ riderWeight == null ||
+ !Number.isFinite(riderWeight) ||
+ riderWeight <= 0
+ )
+ return null
+ return riderWeightUnit === 'lb' ? riderWeight * 0.45359237 : riderWeight
+ }
+ // Whether the loaded template renders W/kg anywhere.
+ function templateUsesRiderWeight() {
+ return (config?.elements ?? []).some((e) => e.value === 'power_to_weight')
+ }
// Unit system ("metric" | "imperial") for every readout left on Auto —
// per-element unit overrides still win. An app-level preference like rider
// weight: configured once in Settings, stored on this device, injected into
@@ -1509,13 +1523,17 @@ export function createAppState() {
},
// Weight resolved to kilograms for the renderer, or null when unset.
get riderWeightKg() {
- if (
- riderWeight == null ||
- !Number.isFinite(riderWeight) ||
- riderWeight <= 0
- )
- return null
- return riderWeightUnit === 'lb' ? riderWeight * 0.45359237 : riderWeight
+ return resolvedRiderWeightKg()
+ },
+ // True when the template renders W/kg anywhere — where the weight prompts
+ // belong, whether or not a weight is currently set.
+ get usesRiderWeight() {
+ return templateUsesRiderWeight()
+ },
+ // True when the template renders W/kg but no weight is set — every such
+ // readout comes out 0.0 until the user enters one, so callers prompt.
+ get needsRiderWeight() {
+ return resolvedRiderWeightKg() == null && templateUsesRiderWeight()
},
get outputWidth() {
return outputWidth