refactor(ui): one chip primitive for all four pickers; DateField reuses lib/dates
This commit is contained in:
126
app/src/components/AttrBar.tsx
Normal file
126
app/src/components/AttrBar.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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" },
|
||||
});
|
||||
Reference in New Issue
Block a user