CwLocationCard / CwSensorCard / CwDataList

The sensor-card API is now composition-first: CwLocationCard owns the location shell, CwSensorCard owns a single expandable sensor panel, and CwDataList renders the detail rows. No devices prop, no internal cloned arrays, and the nested cards stay reactive when the parent updates their props.

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
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
Shared list

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
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
Mixed online and offline
Soil Station Array
Online
All offline
Warehouse B (down)
Online
All loading
Provisioning Bay
Loading
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
Tank Monitor
Loading
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
Warehouse B
Loading
Rooftop Array
Loading
Lab Freezer
Loading
New Location (unassigned)
Loading
No sensors assigned
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>
Documentation Upgrade

Start here

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.

Copy-paste examples

These snippets intentionally show the full public API surface the live demo relies on.

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>