diff --git a/public/rich-components/input-with-stepper.svg b/public/rich-components/input-with-stepper.svg
new file mode 100644
index 00000000..be747f48
--- /dev/null
+++ b/public/rich-components/input-with-stepper.svg
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/src/common/components/front-components/input-shape.tsx b/src/common/components/front-components/input-shape.tsx
index 32899dc3..df3ef440 100644
--- a/src/common/components/front-components/input-shape.tsx
+++ b/src/common/components/front-components/input-shape.tsx
@@ -10,10 +10,10 @@ import { useShapeProps } from '../shapes/use-shape-props.hook';
const inputShapeRestrictions: ShapeSizeRestrictions = {
minWidth: 60,
minHeight: 38,
- maxWidth: -1,
+ maxWidth: 2000,
maxHeight: 38,
- defaultWidth: INPUT_SHAPE.DEFAULT_TEXT_WIDTH,
- defaultHeight: INPUT_SHAPE.DEFAULT_TEXT_HEIGHT,
+ defaultWidth: 155,
+ defaultHeight: 38,
};
export const getInputShapeSizeRestrictions = (): ShapeSizeRestrictions =>
diff --git a/src/common/components/front-rich-components/index.ts b/src/common/components/front-rich-components/index.ts
index db76ce59..e9a5dd0e 100644
--- a/src/common/components/front-rich-components/index.ts
+++ b/src/common/components/front-rich-components/index.ts
@@ -13,3 +13,4 @@ export * from './modal/modal';
export * from './appBar';
export * from './buttonBar/buttonBar';
export * from './tabsbar';
+export * from './input-with-stepper/';
diff --git a/src/common/components/front-rich-components/input-with-stepper/index.ts b/src/common/components/front-rich-components/input-with-stepper/index.ts
new file mode 100644
index 00000000..05cd467b
--- /dev/null
+++ b/src/common/components/front-rich-components/input-with-stepper/index.ts
@@ -0,0 +1,2 @@
+export * from './input-with-stepper';
+export * from './input-with-stepper.business';
diff --git a/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.business.ts b/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.business.ts
new file mode 100644
index 00000000..50c75d65
--- /dev/null
+++ b/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.business.ts
@@ -0,0 +1,63 @@
+import React, { useEffect } from 'react';
+
+type MustBeANumberError = 'You must enter a number';
+
+interface handleCounterInputWithStepperHook {
+ valueToString: string | MustBeANumberError;
+ handleIncrement: () => void;
+ handleDecrement: () => void;
+ isTextANumber: boolean;
+}
+
+const MUST_BE_A_NUMBER: MustBeANumberError = 'You must enter a number';
+
+export const useHandleCounterInputWithStepper = (
+ text: string
+): handleCounterInputWithStepperHook => {
+ const [value, setValue] = React.useState(0);
+
+ const textToNumber = parseInt(text);
+
+ const isTextANumber: boolean = !isNaN(textToNumber);
+
+ useEffect(() => {
+ if (isTextANumber) {
+ setValue(textToNumber);
+ } else {
+ setValue(MUST_BE_A_NUMBER);
+ }
+ }, [text]);
+
+ const handleIncrement = () => {
+ if (typeof value === 'number') {
+ setValue(value + 1);
+ }
+ };
+
+ const handleDecrement = () => {
+ if (typeof value === 'number') {
+ if (value === 0) return;
+ setValue(value - 1);
+ }
+ };
+
+ const valueToString: string =
+ typeof value === 'string' ? value : value.toString();
+
+ return {
+ valueToString,
+ handleIncrement,
+ handleDecrement,
+ isTextANumber,
+ };
+};
+
+export const handleButtonWidth = (restrictedWidth: number): number => {
+ const buttonWidth = restrictedWidth * 0.3;
+ const minButtonWidth = 30;
+ const maxButtonWidth = 70;
+
+ if (buttonWidth < minButtonWidth) return minButtonWidth;
+ if (buttonWidth > maxButtonWidth) return maxButtonWidth;
+ return buttonWidth;
+};
diff --git a/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.tsx b/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.tsx
new file mode 100644
index 00000000..2f08a18a
--- /dev/null
+++ b/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.tsx
@@ -0,0 +1,182 @@
+import { forwardRef, useMemo } from 'react';
+import { Group, Rect, Text } from 'react-konva';
+import { ShapeSizeRestrictions } from '@/core/model';
+import { ShapeType } from '../../../../core/model/index';
+import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes';
+import { useShapeComponentSelection } from '../../shapes/use-shape-selection.hook';
+import { ShapeProps } from '../../front-components/shape.model';
+import {
+ handleButtonWidth,
+ useHandleCounterInputWithStepper,
+} from './input-with-stepper.business';
+import { INPUT_SHAPE } from '../../front-components/shape.const';
+import { KonvaEventObject } from 'konva/lib/Node';
+
+const inputWithStepperSizeRestrictions: ShapeSizeRestrictions = {
+ minWidth: 60,
+ minHeight: 35,
+ maxWidth: 500,
+ maxHeight: 35,
+ defaultWidth: 100,
+ defaultHeight: 35,
+};
+
+export const getInputWithStepperSizeRestrictions = (): ShapeSizeRestrictions =>
+ inputWithStepperSizeRestrictions;
+
+const shapeType: ShapeType = 'inputWithStepper';
+
+export const InputWithStepperShape = forwardRef(
+ (props, ref) => {
+ const { x, y, width, height, id, text, otherProps, ...shapeProps } = props;
+
+ const { width: restrictedWidth, height: restrictedHeight } =
+ fitSizeToShapeSizeRestrictions(
+ inputWithStepperSizeRestrictions,
+ width,
+ height
+ );
+
+ const { handleSelection } = useShapeComponentSelection(props, shapeType);
+
+ const handleDoubleClickInButtons = (e: KonvaEventObject) =>
+ (e.cancelBubble = true);
+
+ const {
+ valueToString: value,
+ handleIncrement,
+ handleDecrement,
+ isTextANumber,
+ } = useHandleCounterInputWithStepper(text);
+
+ const stroke = useMemo(
+ () => otherProps?.stroke ?? INPUT_SHAPE.DEFAULT_STROKE_COLOR,
+ [otherProps?.stroke]
+ );
+
+ const fill = useMemo(
+ () => otherProps?.backgroundColor ?? INPUT_SHAPE.DEFAULT_FILL_BACKGROUND,
+ [otherProps?.backgroundColor]
+ );
+
+ const textColor = useMemo(
+ () => otherProps?.textColor ?? INPUT_SHAPE.DEFAULT_FILL_TEXT,
+ [otherProps?.textColor]
+ );
+
+ const strokeStyle = useMemo(
+ () => otherProps?.strokeStyle ?? [],
+ [otherProps?.strokeStyle]
+ );
+
+ // Reservar espacio para el stepper
+ const buttonWidth = handleButtonWidth(restrictedWidth);
+ const buttonHeight = restrictedHeight / 2;
+
+ return (
+
+ {/* Caja del input */}
+
+
+ {/* Texto del input */}
+
+
+ {/* Botón de incremento (flecha arriba) */}
+
+
+
+
+
+ {/* Botón de decremento (flecha abajo) */}
+
+
+
+
+ {!isTextANumber && (
+
+
+
+ )}
+
+ );
+ }
+);
+
+export default InputWithStepperShape;
diff --git a/src/core/model/index.ts b/src/core/model/index.ts
index 442f9da7..87e99020 100644
--- a/src/core/model/index.ts
+++ b/src/core/model/index.ts
@@ -64,6 +64,7 @@ export type ShapeType =
| 'appBar'
| 'buttonBar'
| 'tooltip'
+ | 'inputWithStepper'
| 'slider';
export const ShapeDisplayName: Record = {
@@ -119,6 +120,7 @@ export const ShapeDisplayName: Record = {
buttonBar: 'Button Bar',
tooltip: 'Tooltip',
slider: 'Slider',
+ inputWithStepper: 'Input With Stepper',
};
export type EditType = 'input' | 'textarea' | 'imageupload';
diff --git a/src/pods/canvas/canvas.model.ts b/src/pods/canvas/canvas.model.ts
index 3811f8b4..3d322f43 100644
--- a/src/pods/canvas/canvas.model.ts
+++ b/src/pods/canvas/canvas.model.ts
@@ -61,6 +61,7 @@ import {
getAppBarShapeSizeRestrictions,
getButtonBarShapeSizeRestrictions,
getTabsBarShapeSizeRestrictions,
+ getInputWithStepperSizeRestrictions,
} from '@/common/components/front-rich-components';
import {
getHeading1SizeRestrictions,
@@ -181,6 +182,8 @@ export const getSizeRestrictionFromShape = (
return getTooltipShapeSizeRestrictions();
case 'slider':
return getSliderShapeSizeRestrictions();
+ case 'inputWithStepper':
+ return getInputWithStepperSizeRestrictions();
default:
console.warn(
`** Shape ${shapeType} has not defined default size, check getDefaultSizeFromShape helper function`
@@ -238,6 +241,7 @@ const doesShapeAllowInlineEdition = (shapeType: ShapeType): boolean => {
case 'buttonBar':
case 'tabsBar':
case 'tooltip':
+ case 'inputWithStepper':
return true;
default:
return false;
@@ -269,6 +273,7 @@ const generateTypeOfTransformer = (shapeType: ShapeType): string[] => {
case 'appBar':
case 'buttonBar':
case 'slider':
+ case 'inputWithStepper':
return ['middle-left', 'middle-right'];
case 'verticalScrollBar':
return ['top-center', 'bottom-center'];
@@ -343,6 +348,8 @@ const generateDefaultTextValue = (shapeType: ShapeType): string | undefined => {
return 'Button 1, Button 2, Button 3';
case 'tabsBar':
return 'Tab 1, Tab 2, Tab 3';
+ case 'inputWithStepper':
+ return '0';
default:
return undefined;
}
@@ -376,6 +383,13 @@ export const generateDefaultOtherProps = (
shapeType: ShapeType
): OtherProps | undefined => {
switch (shapeType) {
+ case 'inputWithStepper':
+ return {
+ stroke: INPUT_SHAPE.DEFAULT_STROKE_COLOR,
+ backgroundColor: INPUT_SHAPE.DEFAULT_FILL_BACKGROUND,
+ textColor: INPUT_SHAPE.DEFAULT_FILL_TEXT,
+ strokeStyle: [],
+ };
case 'input':
return {
stroke: INPUT_SHAPE.DEFAULT_STROKE_COLOR,
diff --git a/src/pods/canvas/shape-renderer/index.tsx b/src/pods/canvas/shape-renderer/index.tsx
index b4a9f84d..1729eb5f 100644
--- a/src/pods/canvas/shape-renderer/index.tsx
+++ b/src/pods/canvas/shape-renderer/index.tsx
@@ -40,6 +40,7 @@ import {
renderModal,
renderButtonBar,
renderTabsBar,
+ renderInputWithStepper,
} from './simple-rich-components';
import {
renderDiamond,
@@ -170,6 +171,8 @@ export const renderShapeComponent = (
return renderTooltip(shape, shapeRenderedProps);
case 'slider':
return renderSlider(shape, shapeRenderedProps);
+ case 'inputWithStepper':
+ return renderInputWithStepper(shape, shapeRenderedProps);
default:
return renderNotFound(shape, shapeRenderedProps);
}
diff --git a/src/pods/canvas/shape-renderer/simple-rich-components/index.ts b/src/pods/canvas/shape-renderer/simple-rich-components/index.ts
index cbee3ae9..6cd811e3 100644
--- a/src/pods/canvas/shape-renderer/simple-rich-components/index.ts
+++ b/src/pods/canvas/shape-renderer/simple-rich-components/index.ts
@@ -13,3 +13,4 @@ export * from './modal.renderer';
export * from './appBar.renderer';
export * from './button-bar.renderer';
export * from './tabsbar.renderer';
+export * from './input-with-stepper.renderer';
diff --git a/src/pods/canvas/shape-renderer/simple-rich-components/input-with-stepper.renderer.tsx b/src/pods/canvas/shape-renderer/simple-rich-components/input-with-stepper.renderer.tsx
new file mode 100644
index 00000000..7767e288
--- /dev/null
+++ b/src/pods/canvas/shape-renderer/simple-rich-components/input-with-stepper.renderer.tsx
@@ -0,0 +1,32 @@
+import { InputWithStepperShape } from '@/common/components/front-rich-components';
+import { ShapeRendererProps } from '../model';
+import { ShapeModel } from '@/core/model';
+
+export const renderInputWithStepper = (
+ shape: ShapeModel,
+ shapeRenderedProps: ShapeRendererProps
+) => {
+ const { handleSelected, shapeRefs, handleDragEnd, handleTransform } =
+ shapeRenderedProps;
+
+ return (
+
+ );
+};
diff --git a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts
index ca8bfa0b..537ad1bc 100644
--- a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts
+++ b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts
@@ -16,6 +16,10 @@ export const mockRichComponentsCollection: ItemInfo[] = [
{ thumbnailSrc: '/rich-components/breadcrumb.svg', type: 'breadcrumb' },
{ thumbnailSrc: '/rich-components/modal.svg', type: 'modal' },
{ thumbnailSrc: '/rich-components/tabsbar.svg', type: 'tabsBar' },
+ {
+ thumbnailSrc: '/rich-components/input-with-stepper.svg',
+ type: 'inputWithStepper',
+ },
{ thumbnailSrc: '/rich-components/calendar.svg', type: 'calendar' },
{ thumbnailSrc: '/rich-components/videoPlayer.svg', type: 'videoPlayer' },
{ thumbnailSrc: '/rich-components/pie.svg', type: 'pie' },