@axzydev/axzy_ui_system 1.3.4 → 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.
- package/README.md +1 -1
- package/dist/index.cjs +754 -613
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +148 -5
- package/dist/index.d.ts +148 -5
- package/dist/index.js +628 -488
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
- package/src/App.tsx +6 -0
- package/src/components/atoms/input/input.props.ts +5 -0
- package/src/components/atoms/input/input.tsx +24 -17
- package/src/components/atoms/input-number/input-number.doc.mdx +74 -0
- package/src/components/atoms/input-number/input-number.props.ts +66 -0
- package/src/components/atoms/input-number/input-number.stories.tsx +111 -0
- package/src/components/atoms/input-number/input-number.tsx +86 -0
- package/src/components/molecules/table/table.doc.mdx +12 -1
- package/src/components/molecules/table/table.props.ts +17 -0
- package/src/components/molecules/table/table.stories.tsx +45 -0
- package/src/components/molecules/table/table.tsx +37 -8
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +14 -1
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +25 -0
- package/src/components/organisms/data-table/dataTable.tsx +37 -8
- package/src/components/organisms/sidebar/sidebar.doc.mdx +50 -12
- package/src/components/organisms/sidebar/sidebar.props.ts +28 -2
- package/src/components/organisms/sidebar/sidebar.stories.tsx +61 -1
- package/src/components/organisms/sidebar/sidebar.tsx +111 -57
- package/src/index.css +10 -0
- package/src/index.ts +6 -3
- package/src/sandbox/navigation.ts +2 -0
- package/src/showcases/DataShowcases.tsx +61 -0
- package/src/showcases/FormShowcases.tsx +287 -1
- package/src/showcases/StructureShowcases.tsx +34 -6
- package/src/utils/table.utils.ts +83 -0
|
@@ -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
|
+
};
|
|
@@ -939,10 +939,23 @@ export const LayoutShowcase = () => {
|
|
|
939
939
|
id: "sales",
|
|
940
940
|
label: "Ventas",
|
|
941
941
|
icon: <FaShoppingCart />,
|
|
942
|
-
isActive: activeId === "sales" || activeId === "orders",
|
|
942
|
+
isActive: activeId === "sales" || activeId === "orders" || activeId === "quotes",
|
|
943
943
|
subitems: [
|
|
944
|
-
{
|
|
945
|
-
|
|
944
|
+
{
|
|
945
|
+
id: "sales-gestion",
|
|
946
|
+
label: "Gestión",
|
|
947
|
+
items: [
|
|
948
|
+
{ id: "orders", label: "Órdenes", action: () => setActiveId("orders") },
|
|
949
|
+
{ id: "invoices", label: "Facturas", action: () => setActiveId("invoices") },
|
|
950
|
+
],
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
id: "sales-catalogo",
|
|
954
|
+
label: "Catálogo",
|
|
955
|
+
items: [
|
|
956
|
+
{ id: "quotes", label: "Cotizaciones", action: () => setActiveId("quotes") },
|
|
957
|
+
],
|
|
958
|
+
},
|
|
946
959
|
],
|
|
947
960
|
},
|
|
948
961
|
{ id: "products", label: "Productos", icon: <FaBoxOpen />, isActive: activeId === "products", action: () => setActiveId("products"), badge: "3" },
|
|
@@ -968,7 +981,21 @@ export const LayoutShowcase = () => {
|
|
|
968
981
|
userMenu: { userName, userEmail, menuItems }
|
|
969
982
|
}}
|
|
970
983
|
sidebar={{
|
|
971
|
-
navigationItems: [
|
|
984
|
+
navigationItems: [
|
|
985
|
+
{ id: "dashboard", label: "Dashboard", icon: <FaChartLine />, isActive: true },
|
|
986
|
+
{
|
|
987
|
+
id: "sales", label: "Ventas", icon: <FaShoppingCart />,
|
|
988
|
+
subitems: [
|
|
989
|
+
{ id: "sales-gestion", label: "Gestión", items: [
|
|
990
|
+
{ id: "orders", label: "Órdenes" },
|
|
991
|
+
{ id: "invoices", label: "Facturas" },
|
|
992
|
+
]},
|
|
993
|
+
{ id: "sales-catalogo", label: "Catálogo", items: [
|
|
994
|
+
{ id: "quotes", label: "Cotizaciones" },
|
|
995
|
+
]},
|
|
996
|
+
]
|
|
997
|
+
},
|
|
998
|
+
],
|
|
972
999
|
isCollapsed: ${collapsed},
|
|
973
1000
|
// Slot opcional sobre la navegación (oculto al colapsar)
|
|
974
1001
|
header: <span>AXZY v1.3</span>
|
|
@@ -1042,7 +1069,7 @@ export const LayoutShowcase = () => {
|
|
|
1042
1069
|
<ITSlideToggle isOn={collapsed} onToggle={setCollapsed} size="sm" />
|
|
1043
1070
|
</div>
|
|
1044
1071
|
<div className="text-xs text-slate-500">
|
|
1045
|
-
En móvil (<lg) el sidebar se abre como drawer con un fondo oscuro. Usa el botón ☰ del topbar.
|
|
1072
|
+
En móvil (<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.
|
|
1046
1073
|
</div>
|
|
1047
1074
|
</ITStack>
|
|
1048
1075
|
}
|
|
@@ -1114,13 +1141,14 @@ export const LayoutShowcase = () => {
|
|
|
1114
1141
|
],
|
|
1115
1142
|
props: [
|
|
1116
1143
|
{ name: "topBar", type: "ITTopBarProps", required: true, description: "Configuración de la barra superior." },
|
|
1117
|
-
{ name: "sidebar", type: "ITSidebarProps", required: true, description: "Configuración del sidebar (navigationItems, isCollapsed, header…)." },
|
|
1144
|
+
{ name: "sidebar", type: "ITSidebarProps", required: true, description: "Configuración del sidebar (navigationItems con subítems planos o agrupados, isCollapsed, header…)." },
|
|
1118
1145
|
{ name: "children", type: "React.ReactNode", required: true, description: "Contenido principal." },
|
|
1119
1146
|
{ name: "className", type: "string", description: "Clases del wrapper externo." },
|
|
1120
1147
|
{ name: "contentClassName", type: "string", description: "Clases del contenedor de contenido." },
|
|
1121
1148
|
{ name: "sidebar.header", type: "React.ReactNode", description: "Slot opcional sobre la navegación (ej. wordmark o filtro). Oculto al colapsar." },
|
|
1122
1149
|
],
|
|
1123
1150
|
notes: [
|
|
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).",
|
|
1124
1152
|
"El sidebar hereda aria-current/aria-expanded y acepta un slot header opcional.",
|
|
1125
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.",
|
|
1126
1154
|
"El riel colapsado se expande automáticamente al pasar el cursor por encima; no hay botón de colapso.",
|
package/src/utils/table.utils.ts
CHANGED
|
@@ -39,3 +39,86 @@ export function isInteractiveTarget(
|
|
|
39
39
|
if (boundary instanceof Node && interactive === boundary) return false;
|
|
40
40
|
return true;
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
// ──────────────────────────────────────────────
|
|
44
|
+
// PINNED (FROZEN) TABLE COLUMNS
|
|
45
|
+
// ──────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/** Horizontal edge a table column can be pinned to, used by `Column.pinned`. */
|
|
48
|
+
export type TablePinnedSide = "left" | "right";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Structural shape required to compute pinned offsets. Kept loose (not tied to
|
|
52
|
+
* `Column<T>`) so any column-like definition can be passed without importing
|
|
53
|
+
* the table props module.
|
|
54
|
+
*/
|
|
55
|
+
type PinnableColumn = {
|
|
56
|
+
/** Unique column key; also the map key in the returned metadata. */
|
|
57
|
+
key: string;
|
|
58
|
+
/** Edge the column is frozen to, if any. */
|
|
59
|
+
pinned?: "left" | "right";
|
|
60
|
+
/** Declared width; a pixel `number` / `"<n>px"` string drives the offset. */
|
|
61
|
+
width?: number | string;
|
|
62
|
+
/** Fallback offset width (px) when `width` is not a pixel value. */
|
|
63
|
+
minWidth?: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Sticky metadata for one pinned column. */
|
|
67
|
+
export interface PinnedColumnMeta {
|
|
68
|
+
/** Edge the column is frozen to. */
|
|
69
|
+
side: TablePinnedSide;
|
|
70
|
+
/** Distance (px) from that edge to the column's sticky position. */
|
|
71
|
+
offset: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Pixel width a pinned column contributes to the accumulated sticky offset.
|
|
76
|
+
* Numbers are pixels; `"<n>px"` strings are parsed; every other length
|
|
77
|
+
* (`%`, `rem`, `em`, …) falls back to `minWidth ?? 0`.
|
|
78
|
+
*/
|
|
79
|
+
function pinnedColumnWidth(col: PinnableColumn): number {
|
|
80
|
+
if (typeof col.width === "number") return col.width;
|
|
81
|
+
if (typeof col.width === "string" && /^\d+(\.\d+)?px$/.test(col.width.trim())) {
|
|
82
|
+
return parseFloat(col.width);
|
|
83
|
+
}
|
|
84
|
+
return col.minWidth ?? 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Computes the sticky `offset` for every pinned column in a table.
|
|
89
|
+
*
|
|
90
|
+
* Left-pinned columns accumulate forward in document order; right-pinned
|
|
91
|
+
* columns accumulate backward. Only pinned columns appear in the returned map,
|
|
92
|
+
* keyed by `col.key`. A column whose width is not resolvable to pixels
|
|
93
|
+
* contributes `minWidth ?? 0`, so declare a pixel `width` on every pinned
|
|
94
|
+
* column of the same side to keep offsets aligned.
|
|
95
|
+
*
|
|
96
|
+
* @param columns - Column definitions in render order.
|
|
97
|
+
* @returns Map of pinned column `key` → `{ side, offset }`.
|
|
98
|
+
*/
|
|
99
|
+
export function getPinnedColumnMeta<T extends PinnableColumn>(
|
|
100
|
+
columns: T[]
|
|
101
|
+
): Record<string, PinnedColumnMeta> {
|
|
102
|
+
const meta: Record<string, PinnedColumnMeta> = {};
|
|
103
|
+
|
|
104
|
+
// Left edge: walk forward, accumulating widths of preceding left-pinned columns.
|
|
105
|
+
let leftOffset = 0;
|
|
106
|
+
for (const col of columns) {
|
|
107
|
+
if (col.pinned === "left") {
|
|
108
|
+
meta[col.key] = { side: "left", offset: leftOffset };
|
|
109
|
+
leftOffset += pinnedColumnWidth(col);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Right edge: walk backward, accumulating widths of preceding right-pinned columns.
|
|
114
|
+
let rightOffset = 0;
|
|
115
|
+
for (let i = columns.length - 1; i >= 0; i--) {
|
|
116
|
+
const col = columns[i];
|
|
117
|
+
if (col.pinned === "right") {
|
|
118
|
+
meta[col.key] = { side: "right", offset: rightOffset };
|
|
119
|
+
rightOffset += pinnedColumnWidth(col);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return meta;
|
|
124
|
+
}
|