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.
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.
<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.
<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} />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
- 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.
- 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.
- 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.
- 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`.
- 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.
| API | Type | Details |
|---|---|---|
value | CwAlertPointsValue | Bindable editor state containing the visual `unit`, the Celsius-backed `center`, and the editable `points[]` collection. |
text | CwAlertPointsEditorText | Optional 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 | string | Midpoint 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 | string | Hex colour used for the point or range and preserved in the bound output object. |
onchange | (value: CwAlertPointsValue) => void | Optional callback fired whenever the bound editor state changes. |
class | string | Optional 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.
<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.
<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.
<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>