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/mock-components/front-rich-components/index.ts b/src/common/components/mock-components/front-rich-components/index.ts
index 6664c0ae..c3c7392b 100644
--- a/src/common/components/mock-components/front-rich-components/index.ts
+++ b/src/common/components/mock-components/front-rich-components/index.ts
@@ -18,3 +18,4 @@ export * from './loading-indicator';
export * from './videoconference';
export * from './togglelightdark-shape';
export * from './gauge/gauge';
+export * from './input-with-stepper';
diff --git a/src/common/components/mock-components/front-rich-components/input-with-stepper/index.ts b/src/common/components/mock-components/front-rich-components/input-with-stepper/index.ts
new file mode 100644
index 00000000..05cd467b
--- /dev/null
+++ b/src/common/components/mock-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/mock-components/front-rich-components/input-with-stepper/input-with-stepper.business.ts b/src/common/components/mock-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/mock-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/mock-components/front-rich-components/input-with-stepper/input-with-stepper.tsx b/src/common/components/mock-components/front-rich-components/input-with-stepper/input-with-stepper.tsx
new file mode 100644
index 00000000..e68649a3
--- /dev/null
+++ b/src/common/components/mock-components/front-rich-components/input-with-stepper/input-with-stepper.tsx
@@ -0,0 +1,176 @@
+import { forwardRef } 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 '../../shape.model';
+import {
+ handleButtonWidth,
+ useHandleCounterInputWithStepper,
+} from './input-with-stepper.business';
+import { INPUT_SHAPE } from '../../front-components/shape.const';
+import { KonvaEventObject } from 'konva/lib/Node';
+import { useShapeProps } from '@/common/components/shapes/use-shape-props.hook';
+import { useGroupShapeProps } from '../../mock-components.utils';
+
+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,
+ onSelected,
+ otherProps,
+ ...shapeProps
+ } = props;
+
+ const restrictedSize = fitSizeToShapeSizeRestrictions(
+ inputWithStepperSizeRestrictions,
+ width,
+ height
+ );
+ const { width: restrictedWidth, height: restrictedHeight } = restrictedSize;
+
+ const { handleSelection } = useShapeComponentSelection(props, shapeType);
+
+ const handleDoubleClickInButtons = (e: KonvaEventObject) =>
+ (e.cancelBubble = true);
+
+ const {
+ valueToString: value,
+ handleIncrement,
+ handleDecrement,
+ isTextANumber,
+ } = useHandleCounterInputWithStepper(text);
+
+ const { stroke, strokeStyle, fill, textColor } = useShapeProps(
+ otherProps,
+ INPUT_SHAPE
+ );
+
+ // Reservar espacio para el stepper
+ const buttonWidth = handleButtonWidth(restrictedWidth);
+ const buttonHeight = restrictedHeight / 2;
+
+ const commonGroupProps = useGroupShapeProps(
+ props,
+ restrictedSize,
+ shapeType,
+ ref
+ );
+
+ return (
+
+ {/* Caja del input */}
+
+
+ {/* Texto del input */}
+
+
+ {/* Botón de incremento (flecha arriba) */}
+
+
+
+
+
+ {/* Botón de decremento (flecha abajo) */}
+
+
+
+
+ {!isTextANumber && (
+
+
+
+ )}
+
+ );
+ }
+);
diff --git a/src/core/model/index.ts b/src/core/model/index.ts
index 3ce8dc4a..370dc3a3 100644
--- a/src/core/model/index.ts
+++ b/src/core/model/index.ts
@@ -68,6 +68,7 @@ export type ShapeType =
| 'appBar'
| 'buttonBar'
| 'tooltip'
+ | 'inputWithStepper'
| 'slider'
| 'chip'
| 'link'
@@ -137,6 +138,7 @@ export const ShapeDisplayName: Record = {
buttonBar: 'Button Bar',
tooltip: 'Tooltip',
slider: 'Slider',
+ inputWithStepper: 'Input With Stepper',
chip: 'Chip',
richtext: 'Rich Text',
cilinder: 'Cilinder',
diff --git a/src/pods/canvas/model/inline-editable.model.ts b/src/pods/canvas/model/inline-editable.model.ts
index fd465059..09f6fd43 100644
--- a/src/pods/canvas/model/inline-editable.model.ts
+++ b/src/pods/canvas/model/inline-editable.model.ts
@@ -82,6 +82,7 @@ const shapeTypesWithDefaultText = new Set([
'modalDialog',
'loading-indicator',
'gauge',
+ 'inputWithStepper',
]);
// Map of ShapeTypes to their default text values
@@ -122,6 +123,7 @@ const defaultTextValueMap: Partial> = {
browser: 'https://example.com',
modalDialog: 'Title here...',
'loading-indicator': 'Loading...',
+ inputWithStepper: '0',
};
export const generateDefaultTextValue = (
diff --git a/src/pods/canvas/model/shape-other-props.utils.ts b/src/pods/canvas/model/shape-other-props.utils.ts
index 43e9f5d7..c22aaad3 100644
--- a/src/pods/canvas/model/shape-other-props.utils.ts
+++ b/src/pods/canvas/model/shape-other-props.utils.ts
@@ -260,6 +260,15 @@ export const generateDefaultOtherProps = (
textColor: '#000000',
strokeStyle: [],
};
+ case 'inputWithStepper':
+ return {
+ stroke: INPUT_SHAPE.DEFAULT_STROKE_COLOR,
+ backgroundColor: INPUT_SHAPE.DEFAULT_FILL_BACKGROUND,
+ textColor: INPUT_SHAPE.DEFAULT_FILL_TEXT,
+ borderRadius: `${INPUT_SHAPE.DEFAULT_CORNER_RADIUS}`,
+ disabled: INPUT_SHAPE.DEFAULT_DISABLED,
+ strokeStyle: [],
+ };
default:
return undefined;
}
diff --git a/src/pods/canvas/model/shape-size.mapper.ts b/src/pods/canvas/model/shape-size.mapper.ts
index c4425897..eb3191f1 100644
--- a/src/pods/canvas/model/shape-size.mapper.ts
+++ b/src/pods/canvas/model/shape-size.mapper.ts
@@ -156,6 +156,7 @@ const shapeSizeMap: Record ShapeSizeRestrictions> = {
gauge: getGaugeShapeSizeRestrictions,
imagePlaceholder: getImagePlaceholderShapeSizeRestrictions,
chip: getChipShapeSizeRestrictions,
+ inputWithStepper: getChipShapeSizeRestrictions,
};
export default shapeSizeMap;
diff --git a/src/pods/canvas/model/transformer.model.ts b/src/pods/canvas/model/transformer.model.ts
index f5fdb32a..5bef4ab7 100644
--- a/src/pods/canvas/model/transformer.model.ts
+++ b/src/pods/canvas/model/transformer.model.ts
@@ -78,6 +78,8 @@ export const generateTypeOfTransformer = (shapeType: ShapeType): string[] => {
return [];
case 'image':
return ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
+ case 'inputWithStepper':
+ return ['middle-left', 'middle-right'];
default:
return [
'top-left',
diff --git a/src/pods/canvas/shape-renderer/index.tsx b/src/pods/canvas/shape-renderer/index.tsx
index d2f1f668..5d522ec1 100644
--- a/src/pods/canvas/shape-renderer/index.tsx
+++ b/src/pods/canvas/shape-renderer/index.tsx
@@ -48,6 +48,7 @@ import {
renderCalendar,
renderAppBar,
renderLoadingIndicator,
+ renderInputWithStepper,
} from './simple-rich-components';
import {
renderDiamond,
@@ -206,6 +207,8 @@ export const renderShapeComponent = (
return renderImagePlaceHolder(shape, shapeRenderedProps);
case 'chip':
return renderChip(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 81078779..09ceb073 100644
--- a/src/pods/canvas/shape-renderer/simple-rich-components/index.ts
+++ b/src/pods/canvas/shape-renderer/simple-rich-components/index.ts
@@ -15,6 +15,7 @@ export * from './pie-chart.renderer';
export * from './gauge.renderer';
export * from './table.renderer';
export * from './tabsbar.renderer';
+export * from './input-with-stepper.renderer';
export * from './vertical-menu.renderer';
export * from './video-player.renderer';
export * from './audio-player.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..862613ec
--- /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/mock-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 c8ceb040..f2acef8e 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
@@ -30,6 +30,10 @@ export const mockRichComponentsCollection: ItemInfo[] = [
{ thumbnailSrc: '/rich-components/pie.svg', type: 'pie' },
{ thumbnailSrc: '/rich-components/table.svg', type: 'table' },
{ thumbnailSrc: '/rich-components/tabsbar.svg', type: 'tabsBar' },
+ {
+ thumbnailSrc: '/rich-components/input-with-stepper.svg',
+ type: 'inputWithStepper',
+ },
{ thumbnailSrc: '/widgets/togglelightdark.svg', type: 'toggleLightDark' },
{ thumbnailSrc: '/rich-components/videoPlayer.svg', type: 'videoPlayer' },
{