CwSensorCard

Sensor cards are built by composition, from three components: CwLocationCard owns the location shell, CwSensorCard owns a single expandable sensor panel, and CwDataList renders the detail rows. Nested cards stay reactive when the parent updates their props, and the location header rolls up the status of every sensor inside it.

Interactive

One location shell, one sensor panel, one data list

The outer location card reacts to the nested sensor status automatically. The sensor panel exposes expand/collapse and freshness callbacks without taking ownership of the input data.

Sensor freshness: no timer

Row freshness map: {}

Greenhouse A

Loading

Details

  • Temperature
    -22.93 °C
  • Humidity
    79.61 %
  • Last Update
    ago
Error state

Surface a hardware fault with the hasError attribute

Setting hasError hides the normal primary/secondary readings and shows a pulsing red error badge in their place, so a fault is impossible to miss. The string you pass becomes the badge text — pass a localized message (true falls back to a generic SENSOR ERROR).

Greenhouse C

Loading

Details

  • Temperature
    -5.10 °C
  • Humidity
    88.00 %
CwDataList

The extracted detail-list component

Use CwDataList for the standard sensor detail rows inside an expanded panel, or anywhere else you need the same metadata layout.

Sensor detail rows

Rendered with CwDataList

  • Temperature
    22.40 °C
  • Humidity
    67.30 %
  • Last Update
    ago
CwLocationCard · aggregate status

Nested sensor panels roll up to the location header

The location shell now derives its own header status from the sensor cards rendered inside it. Mixed sensor states resolve to warning.

All online

Research Lab

Online

Details

  • Temperature
    21.50 °C
  • Humidity
    58.20 %
  • Last Update
    ago

Details

  • Temperature
    19.80 °C
  • Humidity
    62.10 %
  • Last Update
    ago
Mixed online and offline

Soil Station Array

Online

Details

  • Temperature
    14.30 °C
  • Humidity
    38.90 %
  • Last Update
    ago

Details

  • Temperature
    15.25 °C
  • Humidity
    41.20 %
  • Last Update
    ago

Details

  • Temperature
    13.70 °C
  • Humidity
    44.50 %
  • Last Update
    ago
All offline

Warehouse B (down)

Online

Details

  • Temperature
    0.00 °C
  • Last Update
    ago

Details

  • Temperature
    0.00 °C
  • Last Update
    ago
All loading

Provisioning Bay

Loading

Details

  • Temperature
    0.00 °C

Details

  • Temperature
    0.00 °C
Custom rows

Override the default rows when the sensor needs richer metadata

Weather stations and tank monitors often need row labels that do not map cleanly to the default temperature and humidity assumptions. That is where passing explicit detailRows into CwDataList pays off.

Weather Station Alpha

Loading

Details

  • Temperature
    22.40 °C
  • Humidity
    67.30 %
  • Rain Amount
    3.20 mm
  • Wind Speed
    12.50 km/h
  • Lux Level
    48500 lx
  • Air Pressure
    1,013.25 hPa
  • Relay State
    On
  • Last Update
    ago

Tank Monitor

Loading

Details

  • Water Level
    74.50 %
  • Last Update
    ago
Collapsed matrix

Compact cards and the empty state

Short sensor panels stay compact by default, and an empty location card now stands on its own without pretending to be a sensor panel.

Field Station 1

Loading

Details

  • Temperature
    24.50 °C
  • Humidity
    65.20 %
  • Last Update
    ago

Warehouse B

Loading

Details

  • Temperature
    -5.10 °C
  • Humidity
    88.00 %
  • Last Update
    ago

Rooftop Array

Loading

Details

  • Temperature
    0.00 °C
  • Humidity
    0.00 %

Lab Freezer

Loading

Details

  • Temperature
    -18.25 °C
  • Humidity
    72.40 %
  • Last Update
    ago

New Location (unassigned)

Loading
No sensors assigned
States & localisation

Loading, stale readings and translated labels

loading shows skeleton readings. When lastSeenAt is older than expireAfterMinutes, the readings dim and a “Last seen … ago” line appears. Every visible string can be replaced through labels.

Loading

Loading

No detailed readings available yet.

Stale reading

Loading

No detailed readings available yet.

温室 B

警告

詳細データはまだありません。

Single sensor inside a location card Copy code
<CwLocationCard title="Greenhouse A">
	<CwSensorCard
		label="CW-SS-TMEPNCO2-18"
		status="online"
		primaryValue={-22.93}
		primaryUnit="°C"
		primary_icon="thermo"
		secondaryValue={79.61}
		secondaryUnit="%"
		secondary_icon="drop"
		lastSeenAt={new Date(Date.now() - 120_000)}
		expireAfterMinutes={10}
		onExpand={(label) => console.log('expanded:', label)}
		onCollapse={(label) => console.log('collapsed:', label)}
		onExpire={(label) => console.log('overdue:', label)}
	>
		<CwDataList
			rows={[
				{ id: 'temperature', label: 'Temperature', value: '-22.93', unit: '°C', icon: 'thermo' },
				{ id: 'humidity', label: 'Humidity', value: '79.61', unit: '%', icon: 'drop' },
				{ id: 'updated', label: 'Last Update', icon: 'timer', lastSeenAt: new Date(Date.now() - 120_000), expireAfterMinutes: 10 }
			]}
		/>
	</CwSensorCard>
</CwLocationCard>
Callbacks and bindable sensor freshness Copy code
<script lang="ts">
	let sensorWithinTimeout = $state<boolean | null>(null);
</script>

<CwLocationCard title="Greenhouse A">
	<CwSensorCard
		label="CW-SS-TMEPNCO2-18"
		status="online"
		primaryValue={22.93}
		primaryUnit="°C"
		secondaryValue={79.61}
		secondaryUnit="%"
		lastSeenAt={new Date(Date.now() - 120_000)}
		expireAfterMinutes={10}
		onExpand={(label) => console.log('expanded:', label)}
		onCollapse={(label) => console.log('collapsed:', label)}
		onExpire={(label) => console.log('expired:', label)}
		bind:withinTimeout={sensorWithinTimeout}
	>
		<CwDataList
			rows={[
				{ id: 'temperature', label: 'Temperature', value: '22.93', unit: '°C', icon: 'thermo' },
				{ id: 'humidity', label: 'Humidity', value: '79.61', unit: '%', icon: 'drop' },
				{ id: 'updated', label: 'Last Update', icon: 'timer', lastSeenAt: new Date(Date.now() - 120_000), expireAfterMinutes: 10 }
			]}
		/>
	</CwSensorCard>
</CwLocationCard>

<pre>{JSON.stringify({ sensorWithinTimeout }, null, 2)}</pre>
Composed location with multiple sensors Copy code
<script lang="ts">
	import { buildCwSensorCardDetailRows } from '$lib/components/cwSensorCardRows.js';

	const sensors = [
		{
			label: 'CW-Temp-01',
			primaryValue: 21.5,
			primaryUnit: '°C',
			secondaryValue: 58.2,
			secondaryUnit: '%',
			lastSeenAt: new Date(Date.now() - 60_000),
			expireAfterMinutes: 10
		},
		{
			label: 'CW-Temp-02',
			primaryValue: 19.8,
			primaryUnit: '°C',
			secondaryValue: 62.1,
			secondaryUnit: '%',
			lastSeenAt: new Date(Date.now() - 300_000),
			expireAfterMinutes: 15
		}
	];
</script>

<CwLocationCard title="Research Lab">
	{#each sensors as sensor (sensor.label)}
		<CwSensorCard
			label={sensor.label}
			status={sensor.status ?? 'online'}
			primaryValue={sensor.primaryValue}
			primaryUnit={sensor.primaryUnit ?? '°C'}
			secondaryValue={sensor.secondaryValue}
			secondaryUnit={sensor.secondaryUnit ?? '%'}
			lastSeenAt={sensor.lastSeenAt}
			expireAfterMinutes={sensor.expireAfterMinutes}
		>
			<CwDataList rows={buildCwSensorCardDetailRows(sensor)} />
		</CwSensorCard>
	{/each}
</CwLocationCard>
Sensor details with CwDataList Copy code
<CwDataList
	rows={[
		{ id: 'temperature', label: 'Temperature', value: '22.40', unit: '°C', icon: 'thermo' },
		{ id: 'humidity', label: 'Humidity', value: '67.30', unit: '%', icon: 'drop' },
		{ id: 'updated', label: 'Last Update', icon: 'timer', lastSeenAt: new Date(Date.now() - 120_000), expireAfterMinutes: 10 }
	]}
/>
Custom rows for specialized sensors Copy code
<script lang="ts">
	const weatherDevice = {
		label: 'CW-Weather-01',
		primaryValue: 22.4,
		primaryUnit: '°C',
		primary_icon: 'thermo',
		secondaryValue: 67.3,
		secondaryUnit: '%',
		secondary_icon: 'drop',
		lastSeenAt: new Date(Date.now() - 60_000),
		expireAfterMinutes: 10,
		detailRows: [
			{ id: 'wind', label: 'Wind Speed', value: '12.50', unit: 'km/h', icon: '💨' },
			{ id: 'lux', label: 'Lux Level', value: '48500', unit: 'lx', icon: '☀️' },
			{ id: 'updated', label: 'Last Update', icon: 'timer', lastSeenAt: new Date(Date.now() - 60_000), expireAfterMinutes: 10 }
		]
	};
</script>

<CwLocationCard title="Weather Station Alpha">
	<CwSensorCard
		label={weatherDevice.label}
		status="online"
		primaryValue={weatherDevice.primaryValue}
		primaryUnit={weatherDevice.primaryUnit}
		primary_icon={weatherDevice.primary_icon}
		secondaryValue={weatherDevice.secondaryValue}
		secondaryUnit={weatherDevice.secondaryUnit}
		secondary_icon={weatherDevice.secondary_icon}
		lastSeenAt={weatherDevice.lastSeenAt}
		expireAfterMinutes={weatherDevice.expireAfterMinutes}
		defaultExpanded
	>
		<CwDataList rows={weatherDevice.detailRows} />
	</CwSensorCard>
</CwLocationCard>
API

Usage and API reference

CwLocationCard, CwSensorCard, and CwDataList now split the old monolithic sensor-card API into three focused pieces. Compose them in markup so location state, sensor state, and detail rows stay reactive without pushing a devices array into one component.

How to think about it

  1. Start with the location shell Wrap each sensor stack in `CwLocationCard`. The location header derives aggregate status from nested `CwSensorCard` children, but you can still pass a fallback `status` for empty locations.
  2. Render one sensor per CwSensorCard Each expandable panel now owns exactly one sensor. Pass the readings and freshness props directly to that card, and use `defaultExpanded`, `storageKey`, or `bind:open` when you need expand-state control.
  3. Feed the expanded body with CwDataList Pass `rows` into `CwDataList` for the standard detail layout. Use explicit row arrays when the sensor needs custom metadata, or fall back to `buildCwSensorCardDetailRows(sensor)` for common temperature and humidity cases.
  4. Bind freshness where you need it Use `onNavigate` on the location shell, `onExpand`, `onCollapse`, `onExpire`, and `bind:withinTimeout` on the sensor card, and `bind:rowWithinTimeoutMap` on the data list when the host app needs row-level freshness state.

Composition props

The intended structure is `CwLocationCard` -> `CwSensorCard` -> `CwDataList`. Use `CwCardDataRowItem` directly only when you need row-level control outside the shared list wrapper.

APITypeDetails
CwLocationCard.title

Default: Location

stringLocation or site label shown in the outer shell header.
CwLocationCard.status

Default: loading

CwStatusDotStatusFallback location status used when no nested sensor cards are registered.
CwLocationCard.onNavigate
(target: string) => voidCalled from the location header action button.
CwSensorCard.label
stringSensor or device label shown in the expandable panel header.
CwSensorCard.status

Default: loading

CwStatusDotStatusBase sensor status before freshness overrides are applied.
CwSensorCard.primaryValue / primaryUnit / primary_icon
number / string / string | SnippetMain reading shown in the sensor panel header. If the icon is omitted, the card infers a sensible built-in icon from the unit.
CwSensorCard.secondaryValue / secondaryUnit / secondary_icon
number / string / string | SnippetOptional supporting reading rendered next to the primary reading.
CwSensorCard.lastSeenAt / lastSeen / LastSeen / lastUpdated
Date | string | numberFreshness timestamp used for overdue detection on the sensor card. Prefer `lastSeenAt`; `lastUpdated` remains supported for compatibility.
CwSensorCard.expireAfterMinutes / alarmTimeoutMinutes / AlarmTimeoutMinutes / expectedUpdateAfterMinutes
numberThreshold used by the sensor-card freshness timer. Prefer `expireAfterMinutes`; `expectedUpdateAfterMinutes` remains supported for compatibility.
CwSensorCard.open / defaultExpanded / storageKey
boolean / boolean / stringUse these to control or persist the expanded state for an individual sensor panel.
CwSensorCard.onExpand / onCollapse / onExpire / alarmCallback / AlarmCallback
(label: string) => voidLifecycle callbacks for panel toggles and freshness expiry. `alarmCallback` and `AlarmCallback` are accepted as aliases for expiry handling.
bind:CwSensorCard.withinTimeout
boolean | nullBindable freshness state for the sensor card itself. `true` means fresh, `false` means expired, and `null` means no timer inputs were supplied.
CwDataList.rows
CwDataListRow[]Detail rows rendered inside the expanded sensor body.
bind:CwDataList.rowWithinTimeoutMap
Record<string, boolean | null>Bindable row freshness state keyed by row id. Rows without timer inputs are omitted from the map.
class
stringEach component also accepts an extra `class` prop on its own root element.
loading

Default: false

booleanShows a skeleton reading and sets `aria-busy` while data loads.
labels
CwSensorCard labelsStrings for the error, details, empty value and "last seen" text.
detailsHeadingLevel
1–6Heading level for the details section.

Copy-paste examples

Each snippet is a complete, working example you can paste into a Svelte 5 component.

Single sensor inside a location shell

The default composed structure for one sensor per location.

Single sensor inside a location shell Copy code
<CwLocationCard title="Greenhouse A">
	<CwSensorCard
		label="CW-SS-TMEPNCO2-18"
		status="online"
		primaryValue={-22.93}
		primaryUnit="°C"
		secondaryValue={79.61}
		secondaryUnit="%"
		lastSeenAt={new Date(Date.now() - 120_000)}
		expireAfterMinutes={10}
	>
		<CwDataList rows={rows} />
	</CwSensorCard>
</CwLocationCard>
Bindable sensor freshness

Use this when the host application needs sensor-level expiry state.

Bindable sensor freshness Copy code
<script lang="ts">
	let sensorWithinTimeout = $state<boolean | null>(null);
</script>

<CwSensorCard
	label="CW-SS-TMEPNCO2-18"
	status="online"
	primaryValue={22.93}
	primaryUnit="°C"
	secondaryValue={79.61}
	secondaryUnit="%"
	lastSeenAt={new Date(Date.now() - 120_000)}
	expireAfterMinutes={10}
	onExpire={(label) => console.log('expired:', label)}
	bind:withinTimeout={sensorWithinTimeout}
/>
Multiple sensors under one location

Render one `CwSensorCard` per sensor and let the location shell aggregate the status.

Multiple sensors under one location Copy code
<CwLocationCard title="Research Lab">
	{#each sensors as sensor (sensor.label)}
		<CwSensorCard label={sensor.label} status={sensor.status ?? 'online'} primaryValue={sensor.primaryValue}>
			<CwDataList rows={buildCwSensorCardDetailRows(sensor)} />
		</CwSensorCard>
	{/each}
</CwLocationCard>
Custom detail rows

Pass explicit rows into `CwDataList` when the sensor needs custom metadata.

Custom detail rows Copy code
<script lang="ts">
	const weatherDevice = {
		label: 'CW-Weather-01',
		primaryValue: 22.4,
		primaryUnit: '°C',
		secondaryValue: 67.3,
		secondaryUnit: '%',
		lastUpdated: new Date(Date.now() - 60_000),
		expectedUpdateAfterMinutes: 10,
		detailRows: [
			{ id: 'wind', label: 'Wind Speed', value: '12.50', unit: 'km/h', icon: '💨' },
			{ id: 'lux', label: 'Lux Level', value: '48500', unit: 'lx', icon: '☀️' },
			{ id: 'updated', label: 'Last Update', icon: 'timer', lastUpdated: new Date(Date.now() - 60_000), expectedUpdateAfter: 10 }
		]
	};
</script>

<CwSensorCard label={weatherDevice.label} defaultExpanded>
	<CwDataList rows={weatherDevice.detailRows} />
</CwSensorCard>