127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
// 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<string | null>(null);
|
|
const active = attrs.find((a) => a.key === open) ?? null;
|
|
|
|
return (
|
|
<View>
|
|
<View style={s.row}>
|
|
{attrs.map((a) => {
|
|
const set = !!a.value;
|
|
const tone = a.tone ?? C.accent;
|
|
const isOpen = a.key === open;
|
|
return (
|
|
<Pressable
|
|
key={a.key}
|
|
onPress={() => {
|
|
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 },
|
|
]}
|
|
>
|
|
<Icon name={a.icon} size={15} color={set || isOpen ? tone : C.muted} bg={C.bg} />
|
|
<Text style={[s.chipText, (set || isOpen) && { color: tone, fontWeight: "600" }]}>
|
|
{a.value || a.placeholder}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</View>
|
|
|
|
{!!active && (
|
|
<View style={s.panel}>
|
|
<View style={s.panelHead}>
|
|
<Text style={s.panelTitle}>{active.placeholder}</Text>
|
|
{!!active.onClear && !!active.value && (
|
|
<Pressable
|
|
onPress={() => {
|
|
haptic.select();
|
|
active.onClear?.();
|
|
}}
|
|
hitSlop={8}
|
|
style={({ pressed }) => pressed && { opacity: 0.6 }}
|
|
>
|
|
<Text style={s.clear}>Clear</Text>
|
|
</Pressable>
|
|
)}
|
|
<Pressable onPress={() => setOpen(null)} hitSlop={10} style={({ pressed }) => pressed && { opacity: 0.6 }}>
|
|
<Icon name="close" size={17} color={C.muted} />
|
|
</Pressable>
|
|
</View>
|
|
{active.panel}
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
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" },
|
|
});
|