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.

Unit
Alert Point 1 Equals 1.12 C
Alert Point 2 Range 5 to 10 C

Reactive output

Normalized object preview

Numeric fields are converted from the editable strings into Celsius numbers or null.

{
  "center": 0,
  "points": [
    {
      "id": "alert-demo-1",
      "name": "Alert Point 1",
      "color": "#f7903b",
      "condition": "equals",
      "value": 1.12
    },
    {
      "id": "alert-demo-2",
      "name": "Alert Point 2",
      "color": "#42edf0",
      "condition": "range",
      "min": 5,
      "max": 10
    }
  ]
}

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} />

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 }),
		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 }),
		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. Normalize before you submit When you need a payload for storage or an API, convert the Celsius-backed `center`, `value`, `min`, and `max` fields into numbers or `null`.
  5. 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`, and `max`.
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.
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} />
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 }),
		pointDescriptionEquals: (value, unit) =>
			t('alerts.preview.equals', { value, unit }),
		pointDescriptionRange: (min, max, unit) =>
			t('alerts.preview.range', { min, max, 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.

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: ''
			}
		]
	});

	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) =>
			point.condition === 'range'
				? { ...point, min: toNumber(point.min), max: toNumber(point.max) }
				: { ...point, value: toNumber(point.value) }
		)
	});
</script>