From 394c5f74dff2cef397dfe0fdc4adc44dba879a6c Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Wed, 2 Sep 2026 22:58:20 +0200 Subject: [PATCH] feat(ui): add a shared native select and a segmented control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two patterns the app had been rebuilding per call site. NativeSelect: eight ``, styled to Input's exact metrics. + * + * Native on purpose. This app is a fast-entry tool, and a real `` itself and there is no wrapper element, + * so every layout class a call site already used — `w-full`, `flex-1`, `h-8`, + * `w-40` — keeps behaving exactly as it did. That is also why the chevron is a + * background image from `--budget-select-chevron` rather than an absolutely + * positioned icon: no wrapper to position it against. + */ +function NativeSelect({ className, ...props }: React.ComponentProps<"select">) { + return ( + draws for itself is the OS widget's, not + ours, so `appearance: none` removes it and this paints it back. It has to + be a data URI in a token rather than an inline SVG in the component: the + stroke colour cannot be `currentColor` inside a background-image, so the + glyph is re-declared per theme instead. Same path and stroke weight as + lucide's chevron-down, which is the icon family the rest of the app uses. */ + --budget-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23546575' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + /* Elevation is light and cool, never a grey drop shadow. */ --budget-shadow-card: 0 1px 2px rgb(22 33 43 / 0.04), 0 8px 24px -12px rgb(22 33 43 / 0.12); --budget-shadow-raised: 0 1px 1px rgb(22 33 43 / 0.05), 0 2px 6px -2px rgb(22 33 43 / 0.08); @@ -101,6 +109,9 @@ --surplus: #57bb83; --overspend: #f0866a; + /* Same glyph, lifted to the dark theme's muted ink. */ + --budget-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a7b8' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + /* Dark elevation reads as a lighter surface plus a hairline, not a shadow: shadows are invisible on a dark ground. */ --budget-shadow-card: 0 1px 0 rgb(255 255 255 / 0.04) inset, 0 12px 32px -16px rgb(0 0 0 / 0.7); diff --git a/src/ui/components/Segmented.tsx b/src/ui/components/Segmented.tsx new file mode 100644 index 0000000..04cf120 --- /dev/null +++ b/src/ui/components/Segmented.tsx @@ -0,0 +1,65 @@ +interface Option { + value: T; + label: string; +} + +/** + * A two-or-three-way view switch: spend/balance, by post/by month, one/many, + * percentages/amounts. + * + * Four screens had each built this from two loose ` + ); + })} + + ); +}