Проверка ввода
Валидация вычисляется, а не хранится: один signal держит введённый текст, а два computed превращают его в сообщение и вердикт. Сообщение, его цвет и активность кнопки привязаны к ним — никакой ручной перепроверки при отправке.
// Validation is derived, never stored: one signal holds what was typed, two computeds turn it
// into a message and a verdict, and the message, its color and the button all bind to those.
const handle = signal("")
const problem = computed(() =>
handle.value.length < 3 ? "At least 3 characters"
: handle.value.length > 15 ? "At most 15 characters"
: /[^a-z0-9_]/.test(handle.value) ? "Only a–z, 0–9 and _"
: "")
const valid = computed(() => problem.value === "")
const screen = UIScreen([
UIText("Claim your handle").style({ fontSize: 26, fontWeight: 700, color: "white" }),
UIInput().style({
height: 52, px: 16, mt: 22, borderRadius: 14, bgColor: "#151922", border: "1px solid #262b36",
color: "white", fontSize: 17, placeholder: "your_handle", placeholderColor: "#4b5163",
autocapitalize: "none", autocorrect: false, onFocused: { borderColor: "#FF4032" },
}).onChange(value => handle.value = value),
UIText(() => (valid.value ? `le.codes/@${handle.value}` : problem.value)).style({
fontSize: 13, mt: 10, ml: 4,
color: () => (valid.value ? "#22c55e" : handle.value.length ? "#f87171" : "#8b90a0"),
}),
UIButton([ UIText("Claim").style({ fontSize: 16, fontWeight: 700, color: "white" }) ]).style({
height: 52, mt: 22, borderRadius: 14, justifyContent: "center",
bgColor: () => (valid.value ? "#FF4032" : "#20242e"),
opacity: () => (valid.value ? 1 : 0.55), onPressed: { opacity: 0.7 },
}).onClick(() => { if (valid.value) toast(`@${handle.value} is yours`) }),
]).style({ bgColor: "#0b0d12", px: 20, pt: "max(safe-top, 40px)" })
screen.open()
Нажмите «Запустить» — откроется плавающий симулятор. Он остаётся на месте, пока вы читаете: другой пример просто заменит приложение внутри.