Список задач
Задачи лежат в signal, а колонка принимает функцию в позиции детей — поэтому перестраивается сама при изменении массива. Строки различаются по ссылке на объект: переключение одной задачи создаёт новый массив, переиспользуя остальные элементы, поэтому перерисовывается только эта строка.
// A container takes a FUNCTION in children position: the list re-renders itself whenever a
// signal it read changes. Rows are keyed by object reference, so toggling one task — a new
// array that reuses every other item — remounts that row alone.
type Task = { title: string, done: boolean }
const tasks = signal<Task[]>([
{ title: "Read the conventions", done: true },
{ title: "Fork an example", done: false },
{ title: "Run it on a phone", done: false },
{ title: "Ship something small", done: false },
])
const left = computed(() => tasks.value.filter(t => !t.done).length)
const toggle = (task: Task) =>
tasks.value = tasks.value.map(t => (t === task ? { ...t, done: !t.done } : t))
const Row = (task: Task) => UIButton([
UIColumn([]).style({ width: 22, height: 22, borderRadius: 11, border: 2,
borderColor: task.done ? "#22c55e" : "#39404f", bgColor: task.done ? "#22c55e" : "transparent" }),
UIText(task.title).style({ fontSize: 16, color: task.done ? "#5c6272" : "white",
textDecoration: task.done ? "line-through" : "none" }),
]).style({ height: 58, px: 16, gap: 14, borderRadius: 14, bgColor: "#151922", justifyContent: "flex-start" })
.onClick(() => toggle(task))
const screen = UIScreen([
UIText("Today").style({ fontSize: 30, fontWeight: 700, color: "white" }),
UIText(() => `${left.value} left`).style({ fontSize: 14, color: "#8b90a0", mb: 6 }),
UIScrollable(() => tasks.value.map(Row)).style({ flexGrow: 1, gap: 8 }),
]).style({ bgColor: "#0b0d12", px: 16, pt: "max(safe-top, 24px)", pb: "comfort-bottom" })
screen.open()
Нажмите «Запустить» — откроется плавающий симулятор. Он остаётся на месте, пока вы читаете: другой пример просто заменит приложение внутри.