diff --git a/app/checkout/components/CheckoutContent.tsx b/app/checkout/components/CheckoutContent.tsx index af82bb4..25b9eb1 100644 --- a/app/checkout/components/CheckoutContent.tsx +++ b/app/checkout/components/CheckoutContent.tsx @@ -11,6 +11,7 @@ import { computeSubtotal, computeCartTotals, effectivePrice, effectiveTaxRate, c import { computeTaxBreakdown } from "@einfach-produktiv/invoicing"; import { formatPrice } from "../../lib/format"; import { Reveal } from "../../components/Reveal"; +import { CustomSelect } from "../../components/CustomSelect"; import { VersandModal } from "../../components/VersandModal"; import { VatBreakdown } from "../../components/VatBreakdown"; import { CheckoutSteps } from "../../components/CheckoutSteps"; @@ -1021,17 +1022,19 @@ export function CheckoutContent({
@@ -1188,16 +1191,14 @@ export function CheckoutContent({
{/* Both optional and independent — handed to the shipping carrier, not used for any customer communication (that diff --git a/app/konto/bestellungen/components/CustomSelect.tsx b/app/components/CustomSelect.tsx similarity index 72% rename from app/konto/bestellungen/components/CustomSelect.tsx rename to app/components/CustomSelect.tsx index db026bc..22ec45f 100644 --- a/app/konto/bestellungen/components/CustomSelect.tsx +++ b/app/components/CustomSelect.tsx @@ -8,25 +8,43 @@ type Option = { value: string; label: string }; // styled, but its open options popup is rendered by the browser/OS itself // and can't be reached with CSS at all (wrong font size, wrong colors, no // brand styling whatsoever). This renders both the trigger and the -// options panel as plain HTML we control end to end. +// options panel as plain HTML we control end to end. Originally built for +// /konto/bestellungen's filters, promoted to a shared component so +// checkout's country selects can use the same look (moved here 2026-07-30). export function CustomSelect({ label, options, value, onChange, + includeAllOption = true, + fullWidth = false, }: { - /** Screen-reader label and the "not selected" trigger text. */ + /** Screen-reader label — also the trigger's placeholder text when + * `includeAllOption` is true and nothing is selected. */ label: string; options: Option[]; value: string; onChange: (value: string) => void; + /** true (default): prepends a `{value: "", label}` "clear/show all" + * pseudo-option — the filter-dropdown use case (Order/blog/etc. + * filters), where "nothing selected" is a real, meaningful state. + * false: no pseudo-option, every real option is selectable and one is + * always genuinely selected — the plain-select-replacement use case + * (e.g. checkout's country picker), where there's no "clear" concept. */ + includeAllOption?: boolean; + /** false (default): trigger shrinks to its content width from sm: up — + * right for a row of compact filter dropdowns. true: trigger always + * stays full width of its container — right for a form-field + * replacement (e.g. checkout's country picker, alongside other w-full + * inputs). */ + fullWidth?: boolean; }) { const [open, setOpen] = useState(false); const [highlighted, setHighlighted] = useState(0); const rootRef = useRef(null); const listRef = useRef(null); - const allOptions: Option[] = [{ value: "", label }, ...options]; + const allOptions: Option[] = includeAllOption ? [{ value: "", label }, ...options] : options; const selectedIndex = Math.max( 0, allOptions.findIndex((o) => o.value === value), @@ -78,7 +96,7 @@ export function CustomSelect({ } return ( -
+