@axzydev/axzy_ui_system 1.3.5 → 1.3.6

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.
@@ -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
+ };
@@ -1069,7 +1069,7 @@ export const LayoutShowcase = () => {
1069
1069
  <ITSlideToggle isOn={collapsed} onToggle={setCollapsed} size="sm" />
1070
1070
  </div>
1071
1071
  <div className="text-xs text-slate-500">
1072
- En móvil (&lt;lg) el sidebar se abre como drawer con un fondo oscuro. Usa el botón ☰ del topbar. El botón ⟩ del propio sidebar también colapsa/expande.
1072
+ En móvil (&lt;lg) el sidebar se abre como drawer con un fondo oscuro. Usa el botón ☰ del topbar. En desktop el riel colapsado se expande al pasar el cursor; no hay botón de colapso en el sidebar.
1073
1073
  </div>
1074
1074
  </ITStack>
1075
1075
  }
@@ -1148,7 +1148,7 @@ export const LayoutShowcase = () => {
1148
1148
  { name: "sidebar.header", type: "React.ReactNode", description: "Slot opcional sobre la navegación (ej. wordmark o filtro). Oculto al colapsar." },
1149
1149
  ],
1150
1150
  notes: [
1151
- "`subitems` acepta entradas planas o grupos con `label`+`items`; el título del grupo no es foco ni un tercer nivel.",
1151
+ "`subitems` acepta entradas planas o grupos con `label`+`items`; el título del grupo no es foco ni un tercer nivel y cada grupo se nombra por `aria-labelledby`. En el riel colapsado el flyout aplana los grupos (sin títulos).",
1152
1152
  "El sidebar hereda aria-current/aria-expanded y acepta un slot header opcional.",
1153
1153
  "Con `sidebar.isCollapsed` omitido el contenido reserva 88px (hover expande sobre el contenido). Si lo controlas a `false`, reserva 280px y no tapa el contenido.",
1154
1154
  "El riel colapsado se expande automáticamente al pasar el cursor por encima; no hay botón de colapso.",