diff --git a/app/assets/stylesheets/_garden_cards.scss b/app/assets/stylesheets/_garden_cards.scss index 163432552..097bc60e0 100644 --- a/app/assets/stylesheets/_garden_cards.scss +++ b/app/assets/stylesheets/_garden_cards.scss @@ -80,7 +80,7 @@ $garden-card-line: #e6e6e6; // One annual planting: crop | how it's getting on | actions, in aligned columns. .planting-row { display: grid; - grid-template-columns: minmax(11rem, 15rem) minmax(0, 1fr) 2.5rem; + grid-template-columns: minmax(11rem, 15rem) minmax(0, 1fr); align-items: center; padding: 0.6rem 0.5rem; border-top: 1px solid $garden-card-line; @@ -192,21 +192,17 @@ $garden-card-line: #e6e6e6; color: $garden-card-muted; font-size: 0.8rem; } +} - .planting-row-actions { - text-align: right; - } +// The planting menu's items are lowercase. Done in CSS because the labels are +// the shared button translations, which other pages show in sentence case. +.planting-menu .dropdown-item { + text-transform: lowercase; } // The "more" button: three dots in a small round button. The theme gives every // .btn a 5.5rem minimum width, which made the hover background a wide box, and // adds a dropdown caret we don't want. -// The planting menu's items are lowercase. Done in CSS because the labels are -// the shared button translations, which other pages show in sentence case. -.planting-row .dropdown-item { - text-transform: lowercase; -} - .actions-toggle-dots { display: inline-flex; align-items: center; @@ -263,7 +259,7 @@ $garden-card-line: #e6e6e6; } .planting-row { - grid-template-columns: minmax(0, 1fr) 2.5rem; + grid-template-columns: minmax(0, 1fr); .planting-row-status { grid-column: 1 / -1; @@ -315,3 +311,22 @@ $garden-card-line: #e6e6e6; font-size: 0.9rem; } } + +// A crop chip that is also the planting's menu: a caret says it opens something, +// and it takes a slightly deeper tint while pointed at or open. +.chip-menu-toggle { + cursor: pointer; + text-decoration: none; + + &::after { + margin-left: 0.5rem; + vertical-align: 0.15em; + } + + &:hover, + &:focus-visible, + &.show { + background-color: var(--bs-success-border-subtle); + color: var(--bs-success-text-emphasis); + } +} diff --git a/app/javascript/components/ActionsMenu.jsx b/app/javascript/components/ActionsMenu.jsx index e35a99c13..396045a8f 100644 --- a/app/javascript/components/ActionsMenu.jsx +++ b/app/javascript/components/ActionsMenu.jsx @@ -16,7 +16,7 @@ function handled(event, buttonId) { if (dropdown) dropdown.hide(); } -export default function ActionsMenu({id, actions, label = 'Actions', ariaLabel, className = 'btn dropdown-toggle', onSelect}) { +export default function ActionsMenu({id, actions, label = 'Actions', ariaLabel, className = 'btn dropdown-toggle', menuClassName = 'dropdown-menu-end', onSelect}) { if (!actions || actions.length === 0) return null; const buttonId = `actions-${id}`; @@ -35,7 +35,7 @@ export default function ActionsMenu({id, actions, label = 'Actions', ariaLabel, > {label} -
+
{actions.map((action) => ( {action.divider &&
} diff --git a/app/javascript/components/GardenCard.jsx b/app/javascript/components/GardenCard.jsx index 7f0400a4e..346cb800c 100644 --- a/app/javascript/components/GardenCard.jsx +++ b/app/javascript/components/GardenCard.jsx @@ -2,7 +2,7 @@ import React from 'react'; import {isPlainClick} from '../events'; import ActionsMenu from './ActionsMenu'; -import CropChip from './CropChip'; +import PlantingChip from './PlantingChip'; import PlantingRow from './PlantingRow'; // One garden: its name and actions menu (top right), a picture, then what is @@ -11,6 +11,14 @@ import PlantingRow from './PlantingRow'; export default function GardenCard({garden, defaultIconUrl, onPlant, highlightedId, highlightKind, onPlantingUpdated, onEditPlanting, onHarvestPlanting, onSaveSeedsPlanting, onAddPhoto, onFinishPlanting}) { const {id, name, url, image_url: imageUrl, owner, actions, plant_url: plantUrl, perennials, annuals} = garden; const empty = perennials.length === 0 && annuals.length === 0; + // What each planting's menu items open, for annuals and perennials alike. + const handlers = { + edit: onEditPlanting, + harvest: onHarvestPlanting, + seeds: onSaveSeedsPlanting, + finish: onFinishPlanting, + photo: onAddPhoto && ((planting, action) => onAddPhoto({label: planting.crop.name, href: action.href})), + }; function plant(event) { if (onPlant && isPlainClick(event)) { event.preventDefault(); @@ -59,12 +67,12 @@ export default function GardenCard({garden, defaultIconUrl, onPlant, highlighted {perennials.length > 0 ? (
{perennials.map((planting) => ( - ))}
@@ -84,11 +92,7 @@ export default function GardenCard({garden, defaultIconUrl, onPlant, highlighted highlighted={planting.id === highlightedId} highlightKind={highlightKind} onUpdated={onPlantingUpdated} - onEdit={onEditPlanting} - onHarvest={onHarvestPlanting} - onSaveSeeds={onSaveSeedsPlanting} - onFinish={onFinishPlanting} - onAddPhoto={onAddPhoto && ((planting, action) => onAddPhoto({label: planting.crop.name, href: action.href}))} + handlers={handlers} /> ))}
diff --git a/app/javascript/components/PlantingChip.jsx b/app/javascript/components/PlantingChip.jsx new file mode 100644 index 000000000..f9fec087e --- /dev/null +++ b/app/javascript/components/PlantingChip.jsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import {selectPlantingAction} from '../plantingActions'; +import ActionsMenu from './ActionsMenu'; +import CropChip from './CropChip'; + +// A planting's crop chip, which is also its menu: click it for view, edit, +// harvest and the rest, the same for annuals and perennials. Someone who can't +// change the planting has no menu, so for them it stays a plain link to it. +export default function PlantingChip({planting, defaultIconUrl, highlighted, handlers}) { + const {id, url, crop, actions} = planting; + + if (!actions || actions.length === 0) { + return ; + } + + return ( + + + {crop.name} + + )} + ariaLabel={`Actions for ${crop.name}`} + className={`chip crop-chip chip-menu-toggle dropdown-toggle${highlighted ? ' planting-just-added' : ''}`} + menuClassName="dropdown-menu-start planting-menu" + onSelect={selectPlantingAction(planting, handlers)} + /> + ); +} diff --git a/app/javascript/components/PlantingRow.jsx b/app/javascript/components/PlantingRow.jsx index fe900a14d..b0798c3b5 100644 --- a/app/javascript/components/PlantingRow.jsx +++ b/app/javascript/components/PlantingRow.jsx @@ -1,22 +1,21 @@ import React from 'react'; import {formatDate} from '../dates'; -import {isPlainClick} from '../events'; -import ActionsMenu from './ActionsMenu'; -import CropChip from './CropChip'; import PlantedDate from './PlantedDate'; +import PlantingChip from './PlantingChip'; -// One annual planting as a row of three aligned columns: the crop (and when it -// was planted), how it is getting on (badges, then a progress bar or a note -// when there is nothing to predict from), and its own actions menu. -export default function PlantingRow({planting, defaultIconUrl, highlighted, highlightKind = 'added', onUpdated, onEdit, onHarvest, onSaveSeeds, onAddPhoto, onFinish}) { - const {id, url, crop, badges} = planting; +// One annual planting as a row of two aligned columns: the crop, which is also +// the planting's menu (and when it was planted), and how it is getting on +// (badges, then a progress bar or a note when there is nothing to predict from). +// `handlers` is what the menu's items open (see selectPlantingAction). +export default function PlantingRow({planting, defaultIconUrl, highlighted, highlightKind = 'added', onUpdated, handlers}) { + const {badges} = planting; return (
- + {highlighted && highlightKind === 'harvested' && (