diff --git a/apps/web/package.json b/apps/web/package.json index cf78bf5..eb06d08 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.0.0", + "version": "0.2.0", "private": true, "type": "module", "scripts": { @@ -16,6 +16,7 @@ "@batch-cooking/date-tools": "workspace:*", "@batch-cooking/shared": "workspace:*", "i18next": "^26.3.6", + "lucide-react": "^1.32.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^17.0.11", diff --git a/apps/web/src/components/ui/Checkbox.tsx b/apps/web/src/components/ui/Checkbox.tsx new file mode 100644 index 0000000..fbd6d9f --- /dev/null +++ b/apps/web/src/components/ui/Checkbox.tsx @@ -0,0 +1,41 @@ +import type { ReactNode } from "react"; + +/** + * The app-wide "selectable card" checkbox — see `global.scss`'s + * `label:has(> input[type="checkbox"])` rule for the actual look (hidden + * native input, a `.check-mark` that scales in, `is-selected` driving the + * tinted/bordered state). Factors out the JSX triplet (`label` → hidden + * `input` → `span.check-mark` → label text) that used to be duplicated + * across `AllergySelect`, `DietTagSelect`, `IngredientPicker`'s display + * menu, and `UserPreferencesPage`'s theme picker (see {@link RadioOption} + * for its `type="radio"` sibling) — one place to get the markup/a11y right + * instead of four. + * + * `is-selected` is applied in JS from the same `checked` boolean the caller + * already has, not derived via a CSS `:has(:checked)` chain — that turned + * out unreliable across browsers (see the callers this replaces for the + * original note). + * + * `className` is for the *container* layout only (grid item, flex-wrap + * chip, stacked list…) — the control's own look never varies, so there's + * no `variant` prop here. + */ +export function CheckboxOption({ + checked, + onChange, + children, + className, +}: { + checked: boolean; + onChange: (checked: boolean) => void; + children: ReactNode; + className?: string; +}) { + return ( + + ); +} diff --git a/apps/web/src/components/ui/Radio.tsx b/apps/web/src/components/ui/Radio.tsx new file mode 100644 index 0000000..1a3e15f --- /dev/null +++ b/apps/web/src/components/ui/Radio.tsx @@ -0,0 +1,38 @@ +import type { ReactNode } from "react"; + +/** + * The `type="radio"` sibling of {@link CheckboxOption} — same "selectable + * card" markup/look (see `global.scss`'s `label:has(> input[...])` rule, + * shared by both), just a native radio input under the hood so a group of + * `RadioOption`s sharing `name` behaves as mutually exclusive (see + * `UserPreferencesPage`'s theme picker, the one caller so far). + */ +export function RadioOption({ + name, + value, + checked, + onChange, + children, + className, +}: { + name: string; + value: T; + checked: boolean; + onChange: (value: T) => void; + children: ReactNode; + className?: string; +}) { + return ( + + ); +} diff --git a/apps/web/src/features/profile/AllergySelect.tsx b/apps/web/src/features/profile/AllergySelect.tsx index 824b321..1782279 100644 --- a/apps/web/src/features/profile/AllergySelect.tsx +++ b/apps/web/src/features/profile/AllergySelect.tsx @@ -1,4 +1,5 @@ import type { AllergyView } from "@batch-cooking/shared"; +import { CheckboxOption } from "../../components/ui/Checkbox"; import "./profile-forms.scss"; interface AllergySelectProps { @@ -38,19 +39,14 @@ export function AllergySelect({ legend, allergies, value, onChange }: AllergySel {allergies.map((allergy) => { const checked = value.includes(allergy.id); return ( - + ); })} diff --git a/apps/web/src/features/recipes/DietTagSelect.tsx b/apps/web/src/features/recipes/DietTagSelect.tsx index febbfff..6413e79 100644 --- a/apps/web/src/features/recipes/DietTagSelect.tsx +++ b/apps/web/src/features/recipes/DietTagSelect.tsx @@ -1,5 +1,6 @@ import type { DietView } from "@batch-cooking/shared"; import { useTranslation } from "react-i18next"; +import { CheckboxOption } from "../../components/ui/Checkbox"; import "./recipes.scss"; /** @@ -29,11 +30,9 @@ export function DietTagSelect({ {diets.map((diet) => { const checked = value.includes(diet.id); return ( - + ); })} diff --git a/apps/web/src/features/recipes/FavoriteStarButton.tsx b/apps/web/src/features/recipes/FavoriteStarButton.tsx index 9b9912f..638fc1d 100644 --- a/apps/web/src/features/recipes/FavoriteStarButton.tsx +++ b/apps/web/src/features/recipes/FavoriteStarButton.tsx @@ -52,7 +52,7 @@ export function FavoriteStarButton({ disabled={isSaving} title={t(isFavorite ? "recipes.detail.unfavorite" : "recipes.detail.favorite")} > - +