Extract NumberStepControl

This commit is contained in:
araemer 2025-10-21 07:54:13 +02:00
parent f980d4d86d
commit 13fe0ee852
4 changed files with 134 additions and 83 deletions

View file

@ -108,7 +108,7 @@
@apply py-4 border-b border-gray-400;
}
.enumeration-indicator {
.circular-container {
@apply flex-shrink-0 w-7 h-7 rounded-full bg-blue-300 text-white flex items-center justify-center shadow-sm;
}

View file

@ -0,0 +1,74 @@
import {Minus, Plus} from "lucide-react";
import {defaultIconSize} from "./SvgIcon.tsx";
type NumberStepControlProps = {
/** Current numeric value */
value: number;
/** Callback when value changes */
onChange: (newValue: number) => void;
/** Optional: minimum allowed value */
min?: number;
/** Optional: maximum allowed value */
max?: number;
/** Optional: step increment (default = 1) */
step?: number;
/** Optional: additional Tailwind classes */
className?: string;
};
/**
* A compact number input with +/ buttons.
* Removes native browser spinners and supports min/max limits.
*/
export function NumberStepControl({
value,
onChange,
min = Number.NEGATIVE_INFINITY,
max = Number.POSITIVE_INFINITY,
step = 1,
className = "",
}: NumberStepControlProps) {
const handleDecrease = () => {
const newValue = Math.max(value - step, min);
onChange(newValue);
};
const handleIncrease = () => {
const newValue = Math.min(value + step, max);
onChange(newValue);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const num = Number(e.target.value);
if (!isNaN(num)) onChange(Math.min(Math.max(num, min), max));
};
return (
<div className={`flex items-center gap-2 ${className}`}>
<button
type="button"
onClick={handleDecrease}
className="circular-container primary-button-bg"
>
<Minus size={defaultIconSize}/>
</button>
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={value}
onChange={handleInputChange}
className="w-16 text-center input-field"
/>
<button
type="button"
onClick={handleIncrease}
className="circular-container primary-button-bg"
>
<Plus size={defaultIconSize}/>
</button>
</div>
);
}

View file

@ -2,12 +2,12 @@
* Single step component for Desktop editor with drag-and-drop support.
*/
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type { InstructionStepModel } from "../../models/InstructionStepModel";
import {useSortable} from "@dnd-kit/sortable";
import {CSS} from "@dnd-kit/utilities";
import type {InstructionStepModel} from "../../models/InstructionStepModel";
import Button from "../basics/Button";
import { X } from "lucide-react"
import { ButtonType } from "../basics/BasicButtonDefinitions";
import {X} from "lucide-react"
import {ButtonType} from "../basics/BasicButtonDefinitions";
type InstructionStepDesktopListItemProps = {
id: string | number;
@ -23,10 +23,10 @@ export function InstructionStepDesktopListItem({
step,
onUpdate,
onRemove,
}: InstructionStepDesktopListItemProps) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
}: InstructionStepDesktopListItemProps) {
const {attributes, listeners, setNodeRef, transform, transition} = useSortable({id});
const style = { transform: CSS.Transform.toString(transform), transition };
const style = {transform: CSS.Transform.toString(transform), transition};
return (
<div
@ -37,7 +37,7 @@ export function InstructionStepDesktopListItem({
<div
{...attributes}
{...listeners}
className="enumeration-indicator cursor-grab"
className="circular-container cursor-grab"
title="Ziehen, um neu zu ordnen"
>
{index + 1}

View file

@ -5,6 +5,7 @@ import {fetchRecipe} from "../../api/points/RecipePoint"
import {getRecipeEditUrl, getRecipeListUrl} from "../../routes"
import ButtonLink from "../basics/ButtonLink"
import {mapRecipeDtoToModel} from "../../mappers/RecipeMapper"
import {NumberStepControl} from "../basics/NumberStepControl.tsx";
/**
@ -99,36 +100,12 @@ export default function RecipeDetailPage() {
Für {recipeWorkingCopy.servings.amount} {recipeWorkingCopy.servings.unit}
</p>
<div className="flex items-center justify-end sm:justify-center gap-2">
{/* Minus button */}
<button
type="button"
onClick={() => recalculateIngredients(Math.max(1, recipeWorkingCopy.servings.amount - 1))}
className="enumeration-indicator primary-button-bg"
>
</button>
{/* Number input (no spin buttons) */}
<input
type="number"
inputMode="numeric"
pattern="[0-9]*"
className="w-16 text-center input-field"
<NumberStepControl
value={recipeWorkingCopy.servings.amount}
onChange={e => recalculateIngredients(Number(e.target.value))}
onChange={recalculateIngredients}
min={1}
className="justify-end sm:justify-center"
/>
{/* Plus button */}
<button
type="button"
onClick={() => recalculateIngredients(recipeWorkingCopy.servings.amount + 1)}
className="enumeration-indicator primary-button-bg"
>
+
</button>
</div>
</div>
{/* Ingredients */}
@ -156,7 +133,7 @@ export default function RecipeDetailPage() {
{recipe.instructionStepList.map((step, j) => (
<li key={j} className="flex items-start gap-4">
{/* Step number circle */}
<div className="enumeration-indicator">
<div className="circular-container">
{j + 1}
</div>