// The attribute bar: one wrapping row of chips that doubles as the task's // summary and as the entry point to every optional field. // // The old form stacked seven labelled control groups down the screen, so // capturing "call Sam" meant scrolling past a calendar, a repeat picker and an // estimate picker to reach the save button. Here each field collapses to a // single chip: unset chips read as a quiet placeholder, set chips show their // value in the accent. Exactly one panel is open at a time, so the screen // never grows past roughly a phone height. import { useState } from "react"; import { Pressable, StyleSheet, Text, View } from "react-native"; import { haptic } from "../lib/haptics"; import { C, radius, space, type } from "../lib/theme"; import Icon, { type IconName } from "./Icon"; export type Attr = { key: string; icon: IconName; /** Chip label while the field is unset. */ placeholder: string; /** Chip label once set — this is what makes the bar readable as a summary. */ value?: string | null; /** Chip accent when set. Defaults to the app accent. */ tone?: string; /** Revealed under the bar when the chip is tapped. */ panel: React.ReactNode; /** Offered as "Clear" in the open panel's header. Omit for required fields. */ onClear?: () => void; }; export default function AttrBar({ attrs }: { attrs: Attr[] }) { const [open, setOpen] = useState(null); const active = attrs.find((a) => a.key === open) ?? null; return ( {attrs.map((a) => { const set = !!a.value; const tone = a.tone ?? C.accent; const isOpen = a.key === open; return ( { haptic.tap(); setOpen(isOpen ? null : a.key); }} style={({ pressed }) => [ s.chip, set && { borderColor: tone, backgroundColor: `${tone}14` }, isOpen && { borderColor: tone, backgroundColor: `${tone}22` }, pressed && { opacity: 0.7 }, ]} > {a.value || a.placeholder} ); })} {!!active && ( {active.placeholder} {!!active.onClear && !!active.value && ( { haptic.select(); active.onClear?.(); }} hitSlop={8} style={({ pressed }) => pressed && { opacity: 0.6 }} > Clear )} setOpen(null)} hitSlop={10} style={({ pressed }) => pressed && { opacity: 0.6 }}> {active.panel} )} ); } const s = StyleSheet.create({ row: { flexDirection: "row", flexWrap: "wrap", gap: space.sm }, chip: { flexDirection: "row", alignItems: "center", gap: 6, paddingVertical: space.sm - 1, paddingLeft: space.md - 2, paddingRight: space.lg - 2, borderRadius: radius.pill, borderWidth: StyleSheet.hairlineWidth, borderColor: C.border, backgroundColor: C.surface, }, chipText: { ...type.chip, color: C.textSecondary }, panel: { marginTop: space.md, backgroundColor: C.surface, borderRadius: radius.lg, borderWidth: StyleSheet.hairlineWidth, borderColor: C.border, paddingHorizontal: space.lg, paddingBottom: space.md, }, panelHead: { flexDirection: "row", alignItems: "center", gap: space.md, paddingTop: space.md, paddingBottom: space.xs, }, panelTitle: { ...type.section, color: C.textSecondary, flex: 1 }, clear: { ...type.meta, color: C.accent, fontWeight: "600" }, });