CwAlertPointsEditor

Interactive number-line builder for alert points, thresholds, and ranges. The unit switch is visual-only, while the bound value and normalized output stay in Celsius.

All built-in labels, validation messages, empty states, and preview sentences can now be passed through the text prop. Rule names are still part of the bound value.points[] data, so translate those in your own state as well.

Each non-range rule also exposes an optional reset field for hysteresis. The preview draws that reset as the opposite side of the rule: > resets with <, >= resets with <=, and = resets anywhere that is not the trigger value.

Editor with reset on every point

The reset lane uses the same colour as the trigger and a reset icon marker. The high-temp reset below is fully covered by the cold-edge trigger, so it surfaces a validation error.

Unit
Cold edge < 0 C Reset > 4 C
Exact target Equals 5 C Reset != 5 C
High temp alarm > 10 C Reset < 0 C

1 reset will never happen because every reset value is covered by another alert rule.

Editor without reset on any point

The same starting points, but with the reset property omitted. The editor hides the reset input entirely — the existing API stays unchanged.

Unit
Alert Point 1 Equals 1.12 C
Alert Point 2 Range 5 to 10 C
High temp alarm > 30 C

Reactive output — with reset

Normalized object preview

The reset field is forwarded as a Celsius number (or null when blank).

{
  "center": 0,
  "points": [
    {
      "id": "with-reset-1",
      "name": "Cold edge",
      "color": "#f7903b",
      "condition": "lessThan",
      "reset": 4,
      "value": 0
    },
    {
      "id": "with-reset-2",
      "name": "Exact target",
      "color": "#42edf0",
      "condition": "equals",
      "reset": null,
      "value": 5
    },
    {
      "id": "with-reset-3",
      "name": "High temp alarm",
      "color": "#e35c8d",
      "condition": "greaterThan",
      "reset": 0,
      "value": 10
    }
  ]
}

Reactive output — no reset

Normalized object preview

No reset field is emitted because the source data never declared one.

{
  "center": 0,
  "points": [
    {
      "id": "no-reset-1",
      "name": "Alert Point 1",
      "color": "#f7903b",
      "condition": "equals",
      "value": 1.12
    },
    {
      "id": "no-reset-2",
      "name": "Alert Point 2",
      "color": "#42edf0",
      "condition": "range",
      "min": 5,
      "max": 10
    },
    {
      "id": "no-reset-3",
      "name": "High temp alarm",
      "color": "#e35c8d",
      "condition": "greaterThan",
      "value": 30
    }
  ]
}

Copy-paste starter

Basic usage

Bind the full editor state, then normalize the Celsius-backed strings before sending them to an API.

CwAlertPointsEditor example Copy code
<script lang="ts">
	import { CwAlertPointsEditor } from '@cropwatchdevelopment/cwui';
	import type { CwAlertPointsValue } from '@cropwatchdevelopment/cwui';

	let alertPoints = $state<CwAlertPointsValue>({
		unit: 'C',
		center: '0',
		points: []
	});
</script>

<CwAlertPointsEditor bind:value={alertPoints} />

Hysteresis

Optional reset value for rules

Add reset on any non-range rule to record the value at which a downstream alarm engine should clear the alert.

CwAlertPointsEditor reset example Copy code
<script lang="ts">
	import { CwAlertPointsEditor } from '@cropwatchdevelopment/cwui';
	import type { CwAlertPointsValue } from '@cropwatchdevelopment/cwui';

	// 'reset' is optional — provide it to capture a hysteresis threshold
	// for downstream rule evaluation. Existing rules without 'reset' still work.
	let alertPoints = $state<CwAlertPointsValue>({
		unit: 'C',
		center: '0',
		points: [
			{
				id: 'high-temp',
				name: 'High temp alarm',
				color: '#e35c8d',
				condition: 'greaterThanOrEqual',
				value: '100',
				min: '',
				max: '',
				reset: '22'  // alert clears at or below 22 °C
			}
		]
	});
</script>

<CwAlertPointsEditor bind:value={alertPoints} />

Translation-ready

Pass every UI string through your i18n layer

Use the text prop for the component copy, and keep translated rule names in value.points[].name.

CwAlertPointsEditor i18n example Copy code
<script lang="ts">
	import { CwAlertPointsEditor } from '@cropwatchdevelopment/cwui';
	import type {
		CwAlertPointsEditorText,
		CwAlertPointsValue
	} from '@cropwatchdevelopment/cwui';
	import { t } from '$lib/i18n';

	let alertPoints = $state<CwAlertPointsValue>({
		unit: 'C',
		center: '0',
		points: [
			{
				id: 'alert-demo-1',
				name: t('alerts.points.coldEdge'),
				color: '#f7903b',
				condition: 'lessThanOrEqual',
				value: '-2',
				min: '',
				max: ''
			}
		]
	});

	const alertPointsText: CwAlertPointsEditorText = {
		unitFieldLabel: t('alerts.labels.unit'),
		centerFieldLabel: t('alerts.labels.center'),
		nameFieldLabel: t('alerts.labels.name'),
		conditionFieldLabel: t('alerts.labels.condition'),
		valueFieldLabel: t('alerts.labels.value'),
		minValueFieldLabel: t('alerts.labels.min'),
		maxValueFieldLabel: t('alerts.labels.max'),
		colorFieldLabel: t('alerts.labels.color'),
		addAlertPointButton: t('alerts.actions.add'),
		removePointButton: t('alerts.actions.remove'),
		emptyTitle: t('alerts.empty.title'),
		emptyDescription: t('alerts.empty.description'),
		fieldLabelWithUnit: (label, unit) =>
			t('alerts.format.fieldWithUnit', { label, unit }),
		requiredFieldError: (label) =>
			t('alerts.validation.required', { label }),
		invalidPreviewNote: (count) =>
			t('alerts.preview.invalidCount', { count }),
		overlapPreviewNote: (count) =>
			t('alerts.preview.overlapCount', { count }),
		resetNeverHappensPreviewNote: (count) =>
			t('alerts.preview.resetNeverHappensCount', { count }),
		resetNeverHappensError: t('alerts.validation.resetNeverHappens'),
		pointDescriptionEquals: (value, unit) =>
			t('alerts.preview.equals', { value, unit }),
		pointDescriptionRange: (min, max, unit) =>
			t('alerts.preview.range', { min, max, unit }),
		pointDescriptionLessThan: (value, unit) =>
			t('alerts.preview.lessThan', { value, unit }),
		pointDescriptionLessThanOrEqual: (value, unit) =>
			t('alerts.preview.lessThanOrEqual', { value, unit }),
		pointDescriptionGreaterThan: (value, unit) =>
			t('alerts.preview.greaterThan', { value, unit }),
		pointDescriptionGreaterThanOrEqual: (value, unit) =>
			t('alerts.preview.greaterThanOrEqual', { value, unit }),
		resetDescriptionWaitingForValue: t('alerts.preview.resetWaiting'),
		resetDescriptionNotEquals: (value, unit) =>
			t('alerts.preview.resetNotEquals', { value, unit }),
		resetDescriptionLessThan: (value, unit) =>
			t('alerts.preview.resetLessThan', { value, unit }),
		resetDescriptionLessThanOrEqual: (value, unit) =>
			t('alerts.preview.resetLessThanOrEqual', { value, unit }),
		resetDescriptionGreaterThan: (value, unit) =>
			t('alerts.preview.resetGreaterThan', { value, unit }),
		resetDescriptionGreaterThanOrEqual: (value, unit) =>
			t('alerts.preview.resetGreaterThanOrEqual', { value, unit }),
		overlapError: (labels) =>
			t('alerts.validation.overlap', { labels: labels.join(', ') })
	};
</script>

<CwAlertPointsEditor bind:value={alertPoints} text={alertPointsText} />
Documentation Upgrade

Start here

CwAlertPointsEditor is a number-line editor for threshold rules. It keeps the center point anchored, lets users add exact points, open-ended thresholds, and ranges, and keeps the bound values normalized in Celsius while the selected unit stays visual-only.

How to think about it

  1. Treat the bound value as editable form state Numeric fields stay as strings while the user types so partial negatives and decimals do not get wiped out mid-edit.
  2. Route built-in copy through the `text` prop Pass labels, validation messages, preview sentences, and empty-state copy into `text` so your translation library controls every built-in string.
  3. Use the condition dropdown to decide the shape `equals` draws a single point, `range` requires both `min` and `max`, and the less-than/greater-than variants render one-sided ranges.
  4. Add an optional reset for hysteresis-aware rules Every non-range condition exposes an optional `reset` input alongside the threshold value. The preview draws resets as the opposite side of the rule and warns when another alert rule fully covers the reset range.
  5. Normalize before you submit When you need a payload for storage or an API, convert the Celsius-backed `center`, `value`, `min`, `max`, and `reset` fields into numbers or `null`.
  6. Let the scale breathe around the center The number line expands symmetrically around the configured center point so negative values always stay left and positive values stay right.

Props and bound value shape

The component is intentionally optimized for editing. Numeric inputs are stored as strings inside the bound object.

APITypeDetails
value
CwAlertPointsValueBindable editor state containing the visual `unit`, the Celsius-backed `center`, and the editable `points[]` collection.
text
CwAlertPointsEditorTextOptional copy overrides for labels, validation text, preview sentences, empty states, and condition labels. Dynamic fields accept formatter functions so an external translation library can interpolate counts, units, and lists.
value.unit
'C' | 'F' | 'K'Visual-only unit rendered beside the center control and used in helper copy. The bound numeric fields remain in Celsius.
value.center
stringMidpoint of the number line. Kept as a string while editing, stored in Celsius, and typically normalized to a number before saving.
value.points
CwAlertPointRule[]Editable list of alert rules. Each rule includes `id`, `name`, `color`, `condition`, `value`, `min`, `max`, and an optional `reset`.
point.condition
'equals' | 'range' | 'lessThan' | 'lessThanOrEqual' | 'greaterThan' | 'greaterThanOrEqual'Controls whether the preview draws a point, a closed range, or a one-sided threshold.
point.color
stringHex colour used for the point or range and preserved in the bound output object.
point.reset
string (optional)Optional hysteresis threshold for downstream rule engines. Stored in Celsius like the other numeric fields and shown as the opposite reset range on the number line. Omit the field entirely on rules that do not need it — the editor will not add it.
onchange
(value: CwAlertPointsValue) => voidOptional callback fired whenever the bound editor state changes.
class
stringOptional class hook applied to the outer wrapper.

Copy-paste examples

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

Bind the editor state

This is the minimal setup for interactive editing inside your own page or drawer.

Bind the editor state Copy code
<script lang="ts">
	import { CwAlertPointsEditor } from '@cropwatchdevelopment/cwui';
	import type { CwAlertPointsValue } from '@cropwatchdevelopment/cwui';

	let alertPoints = $state<CwAlertPointsValue>({
		unit: 'C',
		center: '0',
		points: []
	});
</script>

<CwAlertPointsEditor bind:value={alertPoints} />
Capture a hysteresis reset on a rule

`reset` is optional, so existing payloads keep working. Add it to any non-range rule to record the value at which a downstream alarm engine should clear the alert.

Capture a hysteresis reset on a rule Copy code
<script lang="ts">
	import { CwAlertPointsEditor } from '@cropwatchdevelopment/cwui';
	import type { CwAlertPointsValue } from '@cropwatchdevelopment/cwui';

	let alertPoints = $state<CwAlertPointsValue>({
		unit: 'C',
		center: '0',
		points: [
			{
				id: 'high-temp',
				name: 'High temp alarm',
				color: '#e35c8d',
				condition: 'greaterThanOrEqual',
				value: '100',
				min: '',
				max: '',
				reset: '22'  // alert clears once the value is at or below 22 °C
			}
		]
	});
</script>

<CwAlertPointsEditor bind:value={alertPoints} />
Translate the editor copy

Use the `text` prop for built-in UI strings and keep translated point names in your bound value.

Translate the editor copy Copy code
<script lang="ts">
	import { CwAlertPointsEditor } from '@cropwatchdevelopment/cwui';
	import type {
		CwAlertPointsEditorText,
		CwAlertPointsValue
	} from '@cropwatchdevelopment/cwui';
	import { t } from '$lib/i18n';

	let alertPoints = $state<CwAlertPointsValue>({
		unit: 'C',
		center: '0',
		points: [
			{
				id: 'alert-1',
				name: t('alerts.points.coldEdge'),
				color: '#f7903b',
				condition: 'lessThanOrEqual',
				value: '-2',
				min: '',
				max: ''
			}
		]
	});

	const alertPointsText: CwAlertPointsEditorText = {
		unitFieldLabel: t('alerts.labels.unit'),
		centerFieldLabel: t('alerts.labels.center'),
		nameFieldLabel: t('alerts.labels.name'),
		conditionFieldLabel: t('alerts.labels.condition'),
		valueFieldLabel: t('alerts.labels.value'),
		minValueFieldLabel: t('alerts.labels.min'),
		maxValueFieldLabel: t('alerts.labels.max'),
		colorFieldLabel: t('alerts.labels.color'),
		addAlertPointButton: t('alerts.actions.add'),
		removePointButton: t('alerts.actions.remove'),
		emptyTitle: t('alerts.empty.title'),
		emptyDescription: t('alerts.empty.description'),
		fieldLabelWithUnit: (label, unit) =>
			t('alerts.format.fieldWithUnit', { label, unit }),
		requiredFieldError: (label) =>
			t('alerts.validation.required', { label }),
		invalidPreviewNote: (count) =>
			t('alerts.preview.invalidCount', { count }),
		overlapPreviewNote: (count) =>
			t('alerts.preview.overlapCount', { count }),
		resetNeverHappensPreviewNote: (count) =>
			t('alerts.preview.resetNeverHappensCount', { count }),
		resetNeverHappensError: t('alerts.validation.resetNeverHappens'),
		pointDescriptionEquals: (value, unit) =>
			t('alerts.preview.equals', { value, unit }),
		pointDescriptionRange: (min, max, unit) =>
			t('alerts.preview.range', { min, max, unit }),
		resetDescriptionWaitingForValue: t('alerts.preview.resetWaiting'),
		resetDescriptionNotEquals: (value, unit) =>
			t('alerts.preview.resetNotEquals', { value, unit }),
		resetDescriptionLessThan: (value, unit) =>
			t('alerts.preview.resetLessThan', { value, unit }),
		resetDescriptionLessThanOrEqual: (value, unit) =>
			t('alerts.preview.resetLessThanOrEqual', { value, unit }),
		resetDescriptionGreaterThan: (value, unit) =>
			t('alerts.preview.resetGreaterThan', { value, unit }),
		resetDescriptionGreaterThanOrEqual: (value, unit) =>
			t('alerts.preview.resetGreaterThanOrEqual', { value, unit }),
		overlapError: (labels) =>
			t('alerts.validation.overlap', { labels: labels.join(', ') })
	};
</script>

<CwAlertPointsEditor bind:value={alertPoints} text={alertPointsText} />
Normalize before API submit

Convert the Celsius-backed string fields into numbers once the user is done building the rule set. Forward `reset` only when the user supplied it.

Normalize before API submit Copy code
<script lang="ts">
	let alertPoints = $state({
		unit: 'C',
		center: '0',
		points: [
			{
				id: 'alert-1',
				name: 'Cold edge',
				color: '#f7903b',
				condition: 'lessThanOrEqual',
				value: '-2',
				min: '',
				max: '',
				reset: '0'
			}
		]
	});

	function toNumber(raw: string) {
		const trimmed = raw.trim();
		if (!trimmed) return null;
		const parsed = Number(trimmed);
		return Number.isFinite(parsed) ? parsed : null;
	}

	const normalized = $derived({
		center: toNumber(alertPoints.center) ?? 0,
		points: alertPoints.points.map((point) => {
			const base = point.condition === 'range'
				? { ...point, min: toNumber(point.min), max: toNumber(point.max) }
				: { ...point, value: toNumber(point.value) };

			return point.reset === undefined
				? base
				: { ...base, reset: toNumber(point.reset) };
		})
	});
</script>