@axzydev/axzy_ui_system 1.3.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/index.cjs +562 -506
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.cts +107 -3
  5. package/dist/index.d.ts +107 -3
  6. package/dist/index.js +433 -378
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/App.tsx +6 -0
  10. package/src/components/atoms/input/input.props.ts +5 -0
  11. package/src/components/atoms/input/input.tsx +24 -17
  12. package/src/components/atoms/input-number/input-number.doc.mdx +74 -0
  13. package/src/components/atoms/input-number/input-number.props.ts +66 -0
  14. package/src/components/atoms/input-number/input-number.stories.tsx +111 -0
  15. package/src/components/atoms/input-number/input-number.tsx +86 -0
  16. package/src/components/molecules/date-picker/datePicker.tsx +1 -0
  17. package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +1 -0
  18. package/src/components/molecules/multi-select/multi-select.tsx +1 -0
  19. package/src/components/molecules/search-select/search-select.tsx +1 -0
  20. package/src/components/molecules/time-picker/timePicker.tsx +1 -0
  21. package/src/components/organisms/dialog/dialog.stories.tsx +48 -0
  22. package/src/components/organisms/sidebar/sidebar.doc.mdx +12 -12
  23. package/src/hooks/useClickOutside.ts +6 -0
  24. package/src/hooks/useFloatingPanel.ts +4 -1
  25. package/src/index.ts +4 -2
  26. package/src/sandbox/navigation.ts +2 -0
  27. package/src/showcases/FeedbackShowcases.tsx +16 -1
  28. package/src/showcases/FormShowcases.tsx +287 -1
  29. package/src/showcases/HomeShowcase.tsx +552 -331
  30. package/src/showcases/ProductoDetalle.tsx +181 -0
  31. package/src/showcases/ProductoIcono.tsx +25 -0
  32. package/src/showcases/StructureShowcases.tsx +2 -2
  33. package/src/showcases/productos.ts +179 -0
  34. package/src/showcases/techIcons.ts +67 -0
@@ -13,13 +13,13 @@ Vertical navigation sidebar with collapsible submenus, tooltips when collapsed,
13
13
  |------|------|---------|-------------|
14
14
  | `navigationItems` | `ITNavigationItem[]` | — | Navigation structure: top-level items with optional sub-items. |
15
15
  | `isCollapsed` | `boolean` | `false` | Whether the sidebar is collapsed to icon-only mode. |
16
- | `onToggleCollapse` | `() => void` | — | Callback when the user toggles collapse via the toggle button. |
17
- | `visibleOnMobile` | `boolean` | `false` | Force sidebar visible on mobile breakpoints. |
16
+ | `onToggleCollapse` | `() => void` | — | Accepted for API compatibility but never invoked by `ITSidebar`: the component renders no collapse toggle button (the rail expands on pointer hover). `ITLayout` still passes it through. |
17
+ | `visibleOnMobile` | `boolean` | `false` | When `true`, the sidebar is always fully expanded (the collapsed 88px rail is disabled) and visible at every breakpoint. |
18
18
  | `onItemClick` | `(item: ITNavigationItem) => void` | — | Callback when a top-level navigation item is clicked. |
19
19
  | `onSubItemClick` | `(subitem: ITNavigationSubItem) => void` | — | Callback when a sub-navigation item is clicked. |
20
20
  | `subitemConnector` | `"dot" \| "\|" \| "none"` | `"dot"` | Visual connector style for sub-items. |
21
21
  | `className` | `string` | — | Additional CSS classes on the sidebar `<aside>`. |
22
- | `header` | `ReactNode` | — | Optional content rendered above the navigation list (e.g. a wordmark). Hidden while the sidebar is collapsed, and independent of the collapse toggle button, which is always visible in both states. |
22
+ | `header` | `ReactNode` | — | Optional content rendered above the navigation list (e.g. a wordmark). Hidden while the sidebar is collapsed, so it only appears when the hover-expanded rail is shown. |
23
23
 
24
24
  ### ITNavigationItem
25
25
 
@@ -30,7 +30,7 @@ Vertical navigation sidebar with collapsible submenus, tooltips when collapsed,
30
30
  | `icon` | `ReactNode` | Icon rendered left of the label. |
31
31
  | `action` | `() => void` | Click handler for top-level items without submenus. |
32
32
  | `isActive` | `boolean` | Whether this item is active/highlighted. |
33
- | `subitems` | `ITNavigationSubItemEntry[]` | Plain sub-items or titled groups. |
33
+ | `subitems` | `ITNavigationSubItemEntry[]` | Array of entries: plain sub-items or titled groups (`ITNavigationSubItemGroup`). |
34
34
  | `badge` | `string` | Badge text (e.g. notification count). |
35
35
 
36
36
  ### ITNavigationSubItem
@@ -77,7 +77,6 @@ const items = [
77
77
  <ITSidebar
78
78
  navigationItems={items}
79
79
  isCollapsed={false}
80
- onToggleCollapse={toggleCollapse}
81
80
  subitemConnector="dot"
82
81
  />
83
82
  ```
@@ -104,17 +103,18 @@ const items = [
104
103
  ];
105
104
  ```
106
105
 
106
+ Discriminate entries with `"items" in entry`. The heading style (uppercase, `0.68rem`, weight 700, `0.08em` tracking, `var(--it-sidebar-icon-color, var(--color-secondary-400))`) is applied by the component and cannot be overridden per group.
107
+
107
108
  ## Notes
108
109
 
109
- - The button lives above the scrollable `<nav>` (never inside it), so it stays in the tab order and visible in both states; it is centered in the 88px rail when collapsed and right-aligned when expanded.
110
- - The toggle is a real `<button type="button">` with `aria-expanded`, `aria-controls` pointing at the `<nav>` it controls, and a state-dependent `aria-label`/`title` ("Expandir/Colapsar barra lateral"). Its chevron rotates 180° when the sidebar is expanded, so one icon signals both directions.
111
- - There is no collapse toggle button: the collapsed rail expands and collapses automatically on pointer hover.
112
- - Collapsed mode shrinks to icon-only; tooltips show labels on hover with glassmorphism style.
113
- - Submenus expand/collapse with smooth transitions when a parent item is clicked.
110
+ - There is no collapse toggle button: the collapsed 88px rail expands and collapses automatically on pointer hover (300 ms leave delay).
111
+ - Collapsed mode shrinks to icon-only; a glassmorphism panel shows the item's sub-items on hover. For an item whose `subitems` contain groups, that panel **flattens** every group into one plain list and renders **no** group headings.
112
+ - Submenus expand/collapse with smooth transitions when a parent item is clicked; the expanded submenu is animated with a `max-h` transition (cap `1000px`).
114
113
  - Active item highlighting uses a colored left-border accent and subtle background tint.
115
114
  - Badge support renders a notification-style counter on the item.
116
- - `subitemConnector` styles the visual link between parent and sub-items: `dot` (default), `|` (vertical bar), or `none`.
117
- - `visibleOnMobile` forces sidebar visibility on smaller screens for responsive layouts.
115
+ - `subitemConnector` styles the visual link between parent and sub-items: `dot` (default), `|` (vertical bar), or `none`. Grouping does not change it: the `|` guide is a single `border-left` on the outer submenu `<ul>` only.
116
+ - An active sub-item inside a group still auto-expands its parent, because auto-expand flattens groups the same way the collapsed flyout does.
117
+ - `visibleOnMobile` forces the sidebar visible at every breakpoint and disables the collapsed rail, so it stays fully expanded.
118
118
  - Themed via CSS variables for consistent colors, shadows, and glassmorphism effects across light and dark modes.
119
119
  - Accessible by default: the `<nav>` is labelled and carries the `id` referenced by the toggle's `aria-controls`, the toggle exposes `aria-expanded` plus a visible `focus-visible` ring, group toggles expose `aria-expanded`/`aria-controls` and respond to Enter/Space, submenus are labelled `role="group"`, and the active sub-item carries `aria-current="page"`.
120
120
  - `subitems` accepts plain sub-items or titled groups (`label` + `items`); the group heading is rendered as a plain `<span>` inside a non-focusable `<div>`, so it is non-interactive and stays out of the tab order.
@@ -14,6 +14,12 @@ const useClickOutside = (
14
14
  // child overlay or its contents.
15
15
  if (clickedDialog && clickedDialog !== currentDialog) return;
16
16
 
17
+ // Floating panels (select/date/time pickers, menus) are portaled to
18
+ // <body>, so they sit outside the DOM of the dialog/drawer that opened
19
+ // them. Picking an option there must not count as an outside click.
20
+ const clickedFloating = target?.closest("[data-it-floating]");
21
+ if (clickedFloating && !ref.current?.contains(clickedFloating)) return;
22
+
17
23
  if (ref.current && !ref.current.contains(event.target as Node)) {
18
24
  callback();
19
25
  }
@@ -57,7 +57,10 @@ export function useFloatingPanel(
57
57
  const flipAbove = spaceBelow < panelHeight + offset && spaceAbove > spaceBelow;
58
58
 
59
59
  const width = matchWidth ? rect.width : undefined;
60
- const panelWidth = panelRef.current?.offsetWidth ?? width ?? 0;
60
+ // With matchWidth the final width is the anchor's. Measuring the panel
61
+ // here would read its pre-style width (a full-width block on first
62
+ // paint) and clamp `left` to the viewport edge until the next update.
63
+ const panelWidth = width ?? panelRef.current?.offsetWidth ?? 0;
61
64
  let left =
62
65
  align === "end" && panelWidth
63
66
  ? rect.right - panelWidth
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ import ITFormHeader from "./components/molecules/form-header/form-header";
36
36
  import ITGrid from "./components/atoms/grid/grid";
37
37
  import ITImage from "./components/atoms/image/image";
38
38
  import ITInput from "./components/atoms/input/input";
39
+ import ITInputNumber from "./components/atoms/input-number/input-number";
39
40
  import ITNavbar from "./components/organisms/navbar/navbar";
40
41
  import ITPage from "./components/templates/page/page";
41
42
  import ITPageHeader from "./components/organisms/page-header/page-header";
@@ -90,6 +91,7 @@ import type { ITFormHeaderProps } from "./components/molecules/form-header/form-
90
91
  import type { ITGridProps } from "./components/atoms/grid/grid.props";
91
92
  import type { ITImageProps } from "./components/atoms/image/image.props";
92
93
  import type { ITInputProps } from "./components/atoms/input/input.props";
94
+ import type { ITInputNumberProps } from "./components/atoms/input-number/input-number.props";
93
95
  import type { ITNavbarProps } from "./components/organisms/navbar/navbar.props";
94
96
  import type { ITPageProps } from "./components/templates/page/page.props";
95
97
  import type { ITPageHeaderProps } from "./components/organisms/page-header/page-header.props";
@@ -145,7 +147,7 @@ export {
145
147
  ITCard, ITCheckbox, ITChip, ITChipInput, ITConfirmDialog, ITDataTable, ITDatePicker,
146
148
  ITDropdownMenu, ITField, ITMaskedInput, ITMultiSelect,
147
149
  ITDialog, ITDivider, ITDrawer,
148
- ITDropfile, ITEmptyState, ITFlex, ITFormBuilder, ITFormHeader, ITGrid, ITImage, ITInput, ITLayout,
150
+ ITDropfile, ITEmptyState, ITFlex, ITFormBuilder, ITFormHeader, ITGrid, ITImage, ITInput, ITInputNumber, ITLayout,
149
151
  ITLoader, ITNavbar, ITPage, ITPageHeader, ITPagination, ITPopover, ITProgress,
150
152
  ITRadioGroup, ITSelect, ITSearchSelect,
151
153
  ITSearchTable,
@@ -169,7 +171,7 @@ export {
169
171
  ITDialogProps, ITDividerProps, ITDrawerProps,
170
172
  ITEmptyStateProps, ITFlexProps,
171
173
  ITFormBuilderProps, ITFormHeaderProps,
172
- ITGridProps, ITImageProps, ITInputProps,
174
+ ITGridProps, ITImageProps, ITInputProps, ITInputNumberProps,
173
175
  ITLayoutProps, ITLoaderProps, ITNavbarProps,
174
176
  ITPageProps, ITPageHeaderProps,
175
177
  ITNavigationItem, ITNavigationSubItem,
@@ -56,6 +56,7 @@ export const SANDBOX_GROUPS: SandboxGroup[] = [
56
56
  subitems: [
57
57
  { id: "button", label: "ITButton" },
58
58
  { id: "input", label: "ITInput" },
59
+ { id: "inputnumber", label: "ITInputNumber" },
59
60
  { id: "select", label: "ITSelect" },
60
61
  { id: "searchselect", label: "ITSearchSelect" },
61
62
  { id: "multiselect", label: "ITMultiSelect" },
@@ -66,6 +67,7 @@ export const SANDBOX_GROUPS: SandboxGroup[] = [
66
67
  { id: "maskedinput", label: "ITMaskedInput" },
67
68
  { id: "calendar", label: "ITCalendar" },
68
69
  { id: "slidetoggle", label: "ITSlideToggle" },
70
+ { id: "segmentedcontrol", label: "ITSegmentedControl" },
69
71
  { id: "dropfile", label: "ITDropfile" },
70
72
  { id: "wysiwyg", label: "ITWysiwyg" },
71
73
  { id: "formbuilder", label: "ITFormBuilder" },
@@ -2,6 +2,7 @@ import React, { useState } from "react";
2
2
  import {
3
3
  ITDialog,
4
4
  ITButton,
5
+ ITSearchSelect,
5
6
  ITSlideToggle,
6
7
  ITToast,
7
8
  ITInput,
@@ -18,8 +19,9 @@ import { ShowcaseLayout } from "./ShowcaseLayout";
18
19
  export const DialogShowcase = () => {
19
20
  const [isOpen, setIsOpen] = useState(false);
20
21
  const [useFormHeader, setUseFormHeader] = useState(true);
22
+ const [auditor, setAuditor] = useState("");
21
23
 
22
- const code = `<ITDialog\n isOpen={${isOpen}}\n onClose={() => setIsOpen(false)}\n title="Confirmación de Auditoría"\n useFormHeader={${useFormHeader}}\n>\n <p>¿Estás seguro de registrar esta auditoría?</p>\n</ITDialog>`;
24
+ const code = `<ITDialog\n isOpen={${isOpen}}\n onClose={() => setIsOpen(false)}\n title="Confirmación de Auditoría"\n useFormHeader={${useFormHeader}}\n>\n <p>¿Estás seguro de registrar esta auditoría?</p>\n <ITSearchSelect placeholder="Selecciona un auditor" ... />\n</ITDialog>`;
23
25
 
24
26
  return (
25
27
  <ShowcaseLayout
@@ -39,6 +41,19 @@ export const DialogShowcase = () => {
39
41
  <p className="text-sm text-slate-600 dark:text-slate-300">
40
42
  Se guardará un registro inmutable en los logs de la blockchain corporativa. Esta acción es irreversible.
41
43
  </p>
44
+ {/* Los paneles flotantes (selects, pickers, menús) se renderizan en
45
+ un portal fuera del diálogo: elegir una opción no debe cerrarlo. */}
46
+ <ITSearchSelect
47
+ label="Auditor responsable"
48
+ placeholder="Busca un auditor..."
49
+ options={[
50
+ { value: "ana", label: "Ana Torres" },
51
+ { value: "luis", label: "Luis Paredes" },
52
+ { value: "marta", label: "Marta Ruiz" },
53
+ ]}
54
+ value={auditor}
55
+ onChange={(next) => setAuditor(String(next))}
56
+ />
42
57
  <div className="flex justify-end gap-3 pt-4 border-t">
43
58
  <ITButton label="Cancelar" variant="ghost" onClick={() => setIsOpen(false)} />
44
59
  <ITButton label="Autorizar" color="success" onClick={() => { alert("Transacción firmada!"); setIsOpen(false); }} />
@@ -1,8 +1,9 @@
1
1
  import React, { useState, useEffect, useMemo } from "react";
2
- import { FaSync, FaSave, FaTrash, FaEdit, FaCode, FaCheck } from "react-icons/fa";
2
+ import { FaSync, FaSave, FaTrash, FaEdit, FaCode, FaCheck, FaCalendar } from "react-icons/fa";
3
3
  import {
4
4
  ITButton,
5
5
  ITInput,
6
+ ITInputNumber,
6
7
  ITSelect,
7
8
  ITSearchSelect,
8
9
  ITDatePicker,
@@ -17,6 +18,7 @@ import {
17
18
  ITMultiSelect,
18
19
  ITChipInput,
19
20
  ITBadget,
21
+ ITSegmentedControl,
20
22
  UploadStatus
21
23
  } from "../index";
22
24
  import { ShowcaseLayout, CodeViewer } from "./ShowcaseLayout";
@@ -2091,3 +2093,287 @@ export const MaskedInputShowcase = () => {
2091
2093
  />
2092
2094
  );
2093
2095
  };
2096
+
2097
+ // 17. ITSegmentedControl Showcase
2098
+ export const SegmentedControlShowcase = () => {
2099
+ const [view, setView] = useState("day");
2100
+ const [size, setSize] = useState<any>("md");
2101
+ const [disabled, setDisabled] = useState(false);
2102
+ const [withIcons, setWithIcons] = useState(false);
2103
+
2104
+ const options = withIcons
2105
+ ? [
2106
+ { value: "day", label: "Día", icon: <FaCheck size={10} /> },
2107
+ { value: "week", label: "Semana", icon: <FaCalendar size={10} /> },
2108
+ { value: "month", label: "Mes", icon: <FaSave size={10} /> },
2109
+ ]
2110
+ : [
2111
+ { value: "day", label: "Día" },
2112
+ { value: "week", label: "Semana" },
2113
+ { value: "month", label: "Mes" },
2114
+ ];
2115
+
2116
+ const code = `<ITSegmentedControl\n options={[\n { value: "day", label: "Día" },\n { value: "week", label: "Semana" },\n { value: "month", label: "Mes" },\n ]}\n value="${view}"\n onChange={(value) => setView(value)}\n size="${size}"\n disabled={${disabled}}\n/>`;
2117
+
2118
+ return (
2119
+ <ShowcaseLayout
2120
+ title="ITSegmentedControl"
2121
+ description="Grupo de botones mutuamente excluyentes estilo iOS para alternar vistas o filtros."
2122
+ code={code}
2123
+ demo={
2124
+ <div className="flex flex-col items-center gap-3">
2125
+ <ITSegmentedControl
2126
+ options={options}
2127
+ value={view}
2128
+ onChange={setView}
2129
+ size={size}
2130
+ disabled={disabled}
2131
+ />
2132
+ <span className="text-xs font-mono text-slate-500">Seleccionado: "{view}"</span>
2133
+ </div>
2134
+ }
2135
+ controls={
2136
+ <>
2137
+ <ITSelect
2138
+ name="seg_size_ctrl"
2139
+ label="Tamaño"
2140
+ value={size}
2141
+ onChange={(e: any) => setSize(e.target.value)}
2142
+ options={[
2143
+ { label: "Small", value: "sm" },
2144
+ { label: "Medium", value: "md" },
2145
+ ]}
2146
+ />
2147
+ <div className="flex items-center justify-between">
2148
+ <span className="text-sm font-semibold text-gray-700">Con iconos</span>
2149
+ <ITSlideToggle isOn={withIcons} onToggle={setWithIcons} size="sm" />
2150
+ </div>
2151
+ <div className="flex items-center justify-between">
2152
+ <span className="text-sm font-semibold text-gray-700">Deshabilitado</span>
2153
+ <ITSlideToggle isOn={disabled} onToggle={setDisabled} activeColor="danger" size="sm" />
2154
+ </div>
2155
+ </>
2156
+ }
2157
+ gallery={
2158
+ <div className="flex flex-wrap items-center gap-6">
2159
+ <ITSegmentedControl
2160
+ options={[
2161
+ { value: "list", label: "List" },
2162
+ { value: "grid", label: "Grid" },
2163
+ ]}
2164
+ value="list"
2165
+ onChange={() => {}}
2166
+ size="sm"
2167
+ />
2168
+ <ITSegmentedControl
2169
+ options={[
2170
+ { value: "light", label: "☀️" },
2171
+ { value: "dark", label: "🌙" },
2172
+ { value: "system", label: "💻" },
2173
+ ]}
2174
+ value="dark"
2175
+ onChange={() => {}}
2176
+ size="sm"
2177
+ />
2178
+ <ITSegmentedControl
2179
+ options={[
2180
+ { value: "a", label: "Activo" },
2181
+ { value: "b", label: "Inactivo" },
2182
+ ]}
2183
+ value="a"
2184
+ onChange={() => {}}
2185
+ disabled
2186
+ />
2187
+ </div>
2188
+ }
2189
+ doc={{
2190
+ summary:
2191
+ "Control segmentado: una sola opción activa a la vez. Útil para alternar vistas (lista/grid), rangos (día/semana/mes) o filtros.",
2192
+ description:
2193
+ "Componente totalmente controlado: recibe el valor activo vía value y notifica los cambios vía onChange.",
2194
+ examples: [
2195
+ '<ITSegmentedControl options={[{ value: "day", label: "Day" }, { value: "week", label: "Week" }]} value={view} onChange={setView} />',
2196
+ '<ITSegmentedControl size="sm" options={[{ value: "list", label: "List", icon: <FaList /> }, { value: "grid", label: "Grid", icon: <FaTh /> }]} value={view} onChange={setView} />',
2197
+ ],
2198
+ props: [
2199
+ { name: "options", type: "ISegmentedOption[]", required: true, description: "Opciones excluyentes { value, label, icon? }." },
2200
+ { name: "value", type: "string", required: true, description: "Valor de la opción activa." },
2201
+ { name: "onChange", type: "(value: string) => void", required: true, description: "Se dispara al elegir otra opción." },
2202
+ { name: "size", type: '"sm" | "md"', default: '"md"', description: "Tamaño del control." },
2203
+ { name: "className", type: "string", description: "Clases adicionales en el contenedor." },
2204
+ { name: "disabled", type: "boolean", default: "false", description: "Deshabilita todas las opciones." },
2205
+ ],
2206
+ notes: [
2207
+ "Solo una opción puede estar activa; es mutuamente excluyente por diseño.",
2208
+ "Cada opción acepta un icon opcional junto al label.",
2209
+ "No expone roles ARIA de radiogroup: si se usa en un formulario accesible habría que agregarlos.",
2210
+ ],
2211
+ }}
2212
+ />
2213
+ );
2214
+ };
2215
+
2216
+ // 18. ITInputNumber Showcase
2217
+ export const InputNumberShowcase = () => {
2218
+ const [amount, setAmount] = useState<number | null>(1234.5);
2219
+ const [quantity, setQuantity] = useState<number | null>(3);
2220
+ const [decimals, setDecimals] = useState<any>("2");
2221
+ const [disabled, setDisabled] = useState(false);
2222
+ const [required, setRequired] = useState(false);
2223
+ const [showError, setShowError] = useState(false);
2224
+
2225
+ const code = `<ITInputNumber\n name="amount"\n label="Monto"\n value={${amount === null ? "null" : amount}}\n prefix="$"\n decimals={${decimals}}\n disabled={${disabled}}\n required={${required}}\n onChange={(value) => setAmount(value)}\n/>
2226
+
2227
+ // El valor que recibes ya es un number (o undefined si está vacío):
2228
+ onChange={(value) => console.log(value + 1)} // ${amount === null ? "undefined" : amount + 1}`;
2229
+
2230
+ return (
2231
+ <ShowcaseLayout
2232
+ title="ITInputNumber"
2233
+ description="Campo numérico con formato de moneda que emite un number real (entero o decimal) en onChange."
2234
+ code={code}
2235
+ demo={
2236
+ <div className="w-full max-w-sm space-y-3">
2237
+ <ITInputNumber
2238
+ name="amount"
2239
+ label="Monto"
2240
+ placeholder="0.00"
2241
+ prefix="$"
2242
+ value={amount}
2243
+ onChange={setAmount}
2244
+ decimals={Number(decimals)}
2245
+ disabled={disabled}
2246
+ required={required}
2247
+ touched={showError}
2248
+ error={showError ? "El monto es obligatorio" : undefined}
2249
+ />
2250
+ <p className="text-xs font-mono text-slate-500">
2251
+ value: {amount === null ? "null" : `${amount} (${typeof amount})`}
2252
+ </p>
2253
+ <div className="grid grid-cols-2 gap-3">
2254
+ <ITInputNumber
2255
+ name="quantity"
2256
+ label="Cantidad (enteros)"
2257
+ value={quantity}
2258
+ decimals={0}
2259
+ min={1}
2260
+ max={99}
2261
+ onChange={setQuantity}
2262
+ />
2263
+ <ITInputNumber
2264
+ name="bounded"
2265
+ label="Entre 10 y 500"
2266
+ min={10}
2267
+ max={500}
2268
+ value={null}
2269
+ onChange={() => {}}
2270
+ />
2271
+ </div>
2272
+ </div>
2273
+ }
2274
+ controls={
2275
+ <>
2276
+ <ITSelect
2277
+ name="in_decimals"
2278
+ label="Decimales"
2279
+ value={decimals}
2280
+ onChange={(e: any) => setDecimals(e.target.value)}
2281
+ options={[
2282
+ { label: "0 (enteros)", value: "0" },
2283
+ { label: "1 decimal", value: "1" },
2284
+ { label: "2 decimales", value: "2" },
2285
+ ]}
2286
+ />
2287
+ <div className="flex items-center justify-between">
2288
+ <span className="text-sm font-semibold text-gray-700">Requerido</span>
2289
+ <ITSlideToggle isOn={required} onToggle={setRequired} size="sm" />
2290
+ </div>
2291
+ <div className="flex items-center justify-between">
2292
+ <span className="text-sm font-semibold text-gray-700">Simular error</span>
2293
+ <ITSlideToggle isOn={showError} onToggle={setShowError} activeColor="danger" size="sm" />
2294
+ </div>
2295
+ <div className="flex items-center justify-between">
2296
+ <span className="text-sm font-semibold text-gray-700">Deshabilitado</span>
2297
+ <ITSlideToggle isOn={disabled} onToggle={setDisabled} activeColor="danger" size="sm" />
2298
+ </div>
2299
+ </>
2300
+ }
2301
+ gallery={
2302
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
2303
+ <div className="w-full max-w-sm">
2304
+ <ITInputNumber
2305
+ name="g_amount"
2306
+ label="Prefijo $"
2307
+ prefix="$"
2308
+ value={98765.43}
2309
+ readOnly
2310
+ onChange={() => {}}
2311
+ />
2312
+ </div>
2313
+ <div className="w-full max-w-sm">
2314
+ <ITInputNumber
2315
+ name="g_price"
2316
+ label="Precio unitario"
2317
+ prefix="$"
2318
+ value={null}
2319
+ onChange={() => {}}
2320
+ />
2321
+ </div>
2322
+ <div className="w-full max-w-sm">
2323
+ <ITInputNumber
2324
+ name="g_qty"
2325
+ label="Cantidad (decimals=0)"
2326
+ decimals={0}
2327
+ value={12}
2328
+ onChange={() => {}}
2329
+ />
2330
+ </div>
2331
+ <div className="w-full max-w-sm">
2332
+ <ITInputNumber
2333
+ name="g_disabled"
2334
+ label="Deshabilitado"
2335
+ prefix="$"
2336
+ value={1500}
2337
+ disabled
2338
+ onChange={() => {}}
2339
+ />
2340
+ </div>
2341
+ </div>
2342
+ }
2343
+ doc={{
2344
+ summary:
2345
+ "Input numérico con formato de moneda: agrupa miles, fija decimales y entrega un number en onChange/onBlur.",
2346
+ description:
2347
+ "Es un ITInput especializado (misma label, validación, tamaños e iconos). La diferencia es que siempre formatea con currencyFormat y parsea el valor antes de emitirlo, así no hay que hacer parseFloat en el consumidor.",
2348
+ examples: [
2349
+ '<ITInputNumber name="amount" label="Monto" prefix="$" value={amount} onChange={(value) => setAmount(value)} />',
2350
+ '<ITInputNumber name="qty" label="Cantidad" decimals={0} min={1} max={99} value={qty} onChange={setQty} />',
2351
+ '<ITInputNumber name="total" label="Total" value={total} onChange={(value) => setTotal((value ?? 0) + 1)} />',
2352
+ ],
2353
+ props: [
2354
+ { name: "name", type: "string", required: true, description: "Nombre del campo." },
2355
+ { name: "value", type: "number | null", description: "Valor numérico actual." },
2356
+ { name: "onChange", type: "(value, event) => void", required: true, description: "Recibe el number parseado (undefined si está vacío)." },
2357
+ { name: "onBlur", type: "(value, event) => void", description: "Recibe el number parseado y clampeado a min/max." },
2358
+ { name: "decimals", type: "number", default: "2", description: "Decimales permitidos; 0 = solo enteros." },
2359
+ { name: "prefix", type: "string", description: "Prefijo dentro del campo (ej. \"$\"). Se ignora si hay iconLeft." },
2360
+ { name: "min", type: "number", description: "Valor mínimo; se ajusta al salir del campo." },
2361
+ { name: "max", type: "number", description: "Valor máximo; se ajusta al salir del campo." },
2362
+ { name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "Tamaño del campo." },
2363
+ { name: "disabled", type: "boolean", default: "false", description: "Deshabilita el campo." },
2364
+ { name: "readOnly", type: "boolean", default: "false", description: "Modo solo lectura." },
2365
+ { name: "required", type: "boolean", default: "false", description: "Marca el campo como requerido." },
2366
+ { name: "touched", type: "boolean", description: "Indica que el campo fue tocado." },
2367
+ { name: "error", type: "string | boolean", description: "Error de validación (true = mensaje genérico)." },
2368
+ { name: "iconLeft / iconRight", type: "ReactNode", description: "Iconos dentro del campo." },
2369
+ ],
2370
+ notes: [
2371
+ "El valor emitido ya es un number; ya no hace falta parseFloat.",
2372
+ "Formatea en blur (miles + decimales) y muestra crudo mientras escribes.",
2373
+ "Usa decimals={0} para forzar enteros.",
2374
+ "min/max se aplican al salir del campo (clamp), no mientras escribes.",
2375
+ ],
2376
+ }}
2377
+ />
2378
+ );
2379
+ };