@erplora/outfitkit 0.1.18 → 0.1.20
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 +3 -1
- package/dist/components/ok-hero/ok-hero.d.ts +11 -0
- package/dist/components/ok-hero/ok-hero.test.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -1
- package/dist/ok-hero.js +204 -12
- package/dist/outfitkit.bundle.js +1474 -1300
- package/dist/tabbar/tabbar.d.ts +69 -0
- package/dist/tabbar/tabbar.test.d.ts +1 -0
- package/dist/tabbar.css +72 -0
- package/dist/tabbar.js +76 -0
- package/package.json +6 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabbar de footer — comportamiento compartido Cloud↔Hub para un `ion-segment` usado como barra de
|
|
3
|
+
* NAVEGACIÓN (no como el pill de filtro de iOS).
|
|
4
|
+
*
|
|
5
|
+
* Por qué está aquí y no en cada producto: el Hub y el SaaS habían resuelto POR SEPARADO el mismo
|
|
6
|
+
* problema, con reglas que ya divergían. Y es un hueco real de Ionic, no un wrapper: `ion-segment`
|
|
7
|
+
* no trae `scrollIntoView` a la pestaña activa, y su prop `scrollable` sólo cambia layout y gestos.
|
|
8
|
+
* Si desborda y por dónde va el scroll es justo lo que CSS no puede saber solo, así que se publica
|
|
9
|
+
* en `data-overflow` y el degradado lo pinta `tabbar.css`.
|
|
10
|
+
*
|
|
11
|
+
* Uso: una sola llamada, y guardar el cleanup.
|
|
12
|
+
*
|
|
13
|
+
* const cleanup = bindTabbar(document.querySelector('ion-footer ion-segment'));
|
|
14
|
+
* // al desmontar la vista → cleanup();
|
|
15
|
+
*/
|
|
16
|
+
/** Qué bordes del tabbar esconden pestañas. `none` = caben todas. */
|
|
17
|
+
export type TabbarOverflow = 'none' | 'start' | 'end' | 'both';
|
|
18
|
+
/**
|
|
19
|
+
* Calcula qué bordes esconden pestañas, para pintar el degradado SOLO donde hay más.
|
|
20
|
+
*
|
|
21
|
+
* Un degradado fijo a la derecha seguiría oscureciendo la última pestaña cuando ya has llegado al
|
|
22
|
+
* final: se lee como un fallo de pintado, no como "hay más". Por eso son cuatro estados.
|
|
23
|
+
*/
|
|
24
|
+
export declare function tabbarOverflow(segment: HTMLElement | null): TabbarOverflow;
|
|
25
|
+
/** Publica el estado en `data-overflow` para que el degradado lo pinte desde CSS. */
|
|
26
|
+
export declare function syncTabbarOverflow(segment: HTMLElement | null): void;
|
|
27
|
+
/**
|
|
28
|
+
* Trae a la vista la pestaña activa.
|
|
29
|
+
*
|
|
30
|
+
* Hace falta cuando la pestaña activa la fija la RUTA y no un toque (deep-link, back/forward): al
|
|
31
|
+
* montar, la barra arranca en `scrollLeft: 0` y la activa puede quedar fuera de pantalla, así que el
|
|
32
|
+
* usuario no ve en cuál está. Se mueve `scrollLeft` y no `scrollIntoView` para no arrastrar a los
|
|
33
|
+
* ancestros scrolleables ni pelearse con el scroll de la página.
|
|
34
|
+
*/
|
|
35
|
+
export declare function scrollActiveTabIntoView(segment: HTMLElement | null): void;
|
|
36
|
+
/**
|
|
37
|
+
* ¿Merece la pena la pista de scroll al entrar?
|
|
38
|
+
*
|
|
39
|
+
* El degradado dice que hay más; el movimiento enseña el GESTO. Pero es movimiento que el usuario
|
|
40
|
+
* no ha pedido, así que sólo se da cuando aporta: si caben todas no hay nada que descubrir; si ya
|
|
41
|
+
* estás al final a la derecha no queda nada; con `prefers-reduced-motion` no se anima; y si la barra
|
|
42
|
+
* ya se movió sola para revelar la activa, repetirlo sería un tirón raro.
|
|
43
|
+
*/
|
|
44
|
+
export declare function shouldHintScroll(opts: {
|
|
45
|
+
overflow: TabbarOverflow;
|
|
46
|
+
reducedMotion: boolean;
|
|
47
|
+
yaScrolleado: boolean;
|
|
48
|
+
}): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Asoma la barra unos píxeles y la devuelve: enseña que se puede deslizar.
|
|
51
|
+
*
|
|
52
|
+
* Mueve el scroll REAL (no un `transform`) para que el degradado se recalcule solo con el evento
|
|
53
|
+
* `scroll` — al asomarse aparece el degradado izquierdo, lo que refuerza la pista.
|
|
54
|
+
*/
|
|
55
|
+
export declare function hintScroll(segment: HTMLElement | null): void;
|
|
56
|
+
/**
|
|
57
|
+
* Cablea un tabbar y devuelve su cleanup. Es la única llamada que necesita el consumidor.
|
|
58
|
+
*
|
|
59
|
+
* Observa tres cosas distintas porque son tres causas distintas de cambio:
|
|
60
|
+
* - `scroll` → el usuario desliza;
|
|
61
|
+
* - `ResizeObserver` → cambia el ancho disponible (rotar el móvil, plegar el menú);
|
|
62
|
+
* - `MutationObserver` → cambia el NÚMERO de pestañas sin cambiar el ancho (un shell que las carga
|
|
63
|
+
* async desde un manifest), caso en el que el ResizeObserver no se entera.
|
|
64
|
+
*
|
|
65
|
+
* `hint: false` desactiva la pista de movimiento (el degradado se mantiene).
|
|
66
|
+
*/
|
|
67
|
+
export declare function bindTabbar(segment: HTMLElement | null, opts?: {
|
|
68
|
+
hint?: boolean;
|
|
69
|
+
}): () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/tabbar.css
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* OutfitKit — tabbar.css
|
|
2
|
+
*
|
|
3
|
+
* Un `ion-segment` usado como barra de NAVEGACIÓN de footer (no como el pill de filtro de iOS).
|
|
4
|
+
* Complementa a Ionic en un hueco real, no lo envuelve: Ionic no tiene ninguna estrategia para un
|
|
5
|
+
* tabbar con más pestañas de las que caben, y su geometría cambia según el modo.
|
|
6
|
+
*
|
|
7
|
+
* La clase `.ok-tabbar` la pone `bindTabbar()` (`@erplora/outfitkit/tabbar`), no el consumidor:
|
|
8
|
+
* así el CSS y el comportamiento no pueden desincronizarse. Ese mismo JS mantiene `data-overflow`,
|
|
9
|
+
* que es lo único que CSS no puede calcular solo (si desborda y por dónde va el scroll).
|
|
10
|
+
*
|
|
11
|
+
* Tokens:
|
|
12
|
+
* --ok-tabbar-min ancho mínimo de pestaña antes de scrollear (default 88px)
|
|
13
|
+
* --ok-tabbar-fade anchura del degradado de borde (default 36px)
|
|
14
|
+
*
|
|
15
|
+
* Export del paquete: `@erplora/outfitkit/tabbar.css`. Cargar después del tema.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
.ok-tabbar {
|
|
19
|
+
/* En modo `ios` Ionic trata el segment como el segmented control compacto y le pone
|
|
20
|
+
`:host(.in-toolbar){ width:auto; margin-inline:auto }`, así que el host se encoge a su
|
|
21
|
+
contenido (medido: 289px dentro de una toolbar de 1560px) sin importar cómo dimensiones las
|
|
22
|
+
columnas. Esto es lo que lo arregla; el `grid-auto-columns` por sí solo NO basta. */
|
|
23
|
+
width: 100%;
|
|
24
|
+
margin-inline: 0;
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
|
|
27
|
+
/* Con pocas pestañas manda el `1fr` y llenan el ancho; con muchas manda el mínimo y NO se
|
|
28
|
+
aplastan. El default de 88px sale de MEDIR el umbral en que se trunca el label más largo del
|
|
29
|
+
Hub (86px) más 2px de margen por si el render de la fuente es algo más ancho. Cada producto
|
|
30
|
+
puede ajustarlo con `--ok-tabbar-min`: preferimos scroll antes que labels cortados. */
|
|
31
|
+
grid-auto-columns: minmax(var(--ok-tabbar-min, 88px), 1fr);
|
|
32
|
+
justify-content: start;
|
|
33
|
+
|
|
34
|
+
overflow-x: auto;
|
|
35
|
+
scrollbar-width: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ok-tabbar::-webkit-scrollbar {
|
|
39
|
+
display: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Degradado de borde. Al esconder la barra de scroll hay que señalar de otro modo que quedan
|
|
43
|
+
pestañas fuera: sin esto el único indicio es una pestaña cortada a medias, que se lee como un
|
|
44
|
+
fallo de maquetación y no como una invitación a deslizar.
|
|
45
|
+
Va SOLO en el lado que esconde algo — uno fijo a la derecha seguiría oscureciendo la última
|
|
46
|
+
pestaña al llegar al final, que es justo el efecto de "roto" que se quiere evitar. */
|
|
47
|
+
.ok-tabbar[data-overflow='end'] {
|
|
48
|
+
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--ok-tabbar-fade, 36px)), transparent 100%);
|
|
49
|
+
mask-image: linear-gradient(to right, #000 calc(100% - var(--ok-tabbar-fade, 36px)), transparent 100%);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.ok-tabbar[data-overflow='start'] {
|
|
53
|
+
-webkit-mask-image: linear-gradient(to left, #000 calc(100% - var(--ok-tabbar-fade, 36px)), transparent 100%);
|
|
54
|
+
mask-image: linear-gradient(to left, #000 calc(100% - var(--ok-tabbar-fade, 36px)), transparent 100%);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ok-tabbar[data-overflow='both'] {
|
|
58
|
+
-webkit-mask-image: linear-gradient(
|
|
59
|
+
to right,
|
|
60
|
+
transparent 0,
|
|
61
|
+
#000 var(--ok-tabbar-fade, 36px),
|
|
62
|
+
#000 calc(100% - var(--ok-tabbar-fade, 36px)),
|
|
63
|
+
transparent 100%
|
|
64
|
+
);
|
|
65
|
+
mask-image: linear-gradient(
|
|
66
|
+
to right,
|
|
67
|
+
transparent 0,
|
|
68
|
+
#000 var(--ok-tabbar-fade, 36px),
|
|
69
|
+
#000 calc(100% - var(--ok-tabbar-fade, 36px)),
|
|
70
|
+
transparent 100%
|
|
71
|
+
);
|
|
72
|
+
}
|
package/dist/tabbar.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const EPSILON = 1;
|
|
2
|
+
const HINT_PX = 28;
|
|
3
|
+
const HINT_VUELTA_MS = 420;
|
|
4
|
+
const CLASE = "ok-tabbar";
|
|
5
|
+
function tabbarOverflow(segment) {
|
|
6
|
+
if (!segment) return "none";
|
|
7
|
+
const maximo = segment.scrollWidth - segment.clientWidth;
|
|
8
|
+
if (maximo <= EPSILON) return "none";
|
|
9
|
+
const hayAntes = segment.scrollLeft > EPSILON;
|
|
10
|
+
const hayDespues = segment.scrollLeft < maximo - EPSILON;
|
|
11
|
+
if (hayAntes && hayDespues) return "both";
|
|
12
|
+
if (hayAntes) return "start";
|
|
13
|
+
return "end";
|
|
14
|
+
}
|
|
15
|
+
function syncTabbarOverflow(segment) {
|
|
16
|
+
if (!segment) return;
|
|
17
|
+
segment.dataset.overflow = tabbarOverflow(segment);
|
|
18
|
+
}
|
|
19
|
+
function scrollActiveTabIntoView(segment) {
|
|
20
|
+
if (!segment) return;
|
|
21
|
+
const activa = segment.querySelector(".segment-button-checked");
|
|
22
|
+
if (!activa) return;
|
|
23
|
+
if (segment.scrollWidth <= segment.clientWidth) return;
|
|
24
|
+
const inicio = activa.offsetLeft;
|
|
25
|
+
const fin = inicio + activa.offsetWidth;
|
|
26
|
+
const visibleInicio = segment.scrollLeft;
|
|
27
|
+
const visibleFin = visibleInicio + segment.clientWidth;
|
|
28
|
+
if (inicio < visibleInicio) segment.scrollLeft = inicio;
|
|
29
|
+
else if (fin > visibleFin) segment.scrollLeft = fin - segment.clientWidth;
|
|
30
|
+
}
|
|
31
|
+
function shouldHintScroll(opts) {
|
|
32
|
+
if (opts.reducedMotion) return false;
|
|
33
|
+
if (opts.yaScrolleado) return false;
|
|
34
|
+
return opts.overflow === "end" || opts.overflow === "both";
|
|
35
|
+
}
|
|
36
|
+
function hintScroll(segment) {
|
|
37
|
+
if (!segment) return;
|
|
38
|
+
segment.scrollTo({ left: HINT_PX, behavior: "smooth" });
|
|
39
|
+
setTimeout(() => segment.scrollTo({ left: 0, behavior: "smooth" }), HINT_VUELTA_MS);
|
|
40
|
+
}
|
|
41
|
+
function bindTabbar(segment, opts = {}) {
|
|
42
|
+
if (!segment) return () => {
|
|
43
|
+
};
|
|
44
|
+
segment.classList.add(CLASE);
|
|
45
|
+
const sync = () => syncTabbarOverflow(segment);
|
|
46
|
+
sync();
|
|
47
|
+
segment.addEventListener("scroll", sync, { passive: true });
|
|
48
|
+
const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(sync) : null;
|
|
49
|
+
ro?.observe(segment);
|
|
50
|
+
const mo = typeof MutationObserver !== "undefined" ? new MutationObserver(sync) : null;
|
|
51
|
+
mo?.observe(segment, { childList: true });
|
|
52
|
+
let pista = null;
|
|
53
|
+
if (opts.hint !== false) {
|
|
54
|
+
pista = setTimeout(() => {
|
|
55
|
+
pista = null;
|
|
56
|
+
const reducedMotion = typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
57
|
+
if (shouldHintScroll({ overflow: tabbarOverflow(segment), reducedMotion, yaScrolleado: segment.scrollLeft > 1 })) {
|
|
58
|
+
hintScroll(segment);
|
|
59
|
+
}
|
|
60
|
+
}, 450);
|
|
61
|
+
}
|
|
62
|
+
return () => {
|
|
63
|
+
segment.removeEventListener("scroll", sync);
|
|
64
|
+
ro?.disconnect();
|
|
65
|
+
mo?.disconnect();
|
|
66
|
+
if (pista) clearTimeout(pista);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
bindTabbar,
|
|
71
|
+
hintScroll,
|
|
72
|
+
scrollActiveTabIntoView,
|
|
73
|
+
shouldHintScroll,
|
|
74
|
+
syncTabbarOverflow,
|
|
75
|
+
tabbarOverflow
|
|
76
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erplora/outfitkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "OutfitKit — librería de Web Components (Lit) que CONSTRUYE lo que Ionic no tiene (tree, data-table rica, inline-feedback, kpi/stat, stepper/wizard, calendar, kanban…) sobre primitivos de Ionic. Ionic es la base; OutfitKit cubre los huecos. npm + CDN, imports individuales, CSP-safe. Tema vía tokens --ok-* (fallback a --ion-*).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"types": "./dist/store/controller.d.ts",
|
|
28
28
|
"import": "./dist/store-controller.js"
|
|
29
29
|
},
|
|
30
|
+
"./tabbar": {
|
|
31
|
+
"types": "./dist/tabbar/tabbar.d.ts",
|
|
32
|
+
"import": "./dist/tabbar.js"
|
|
33
|
+
},
|
|
30
34
|
"./ok-store": "./dist/ok-store.js",
|
|
31
35
|
"./ok-data-table": "./dist/ok-data-table.js",
|
|
32
36
|
"./ok-mail": "./dist/ok-mail.js",
|
|
@@ -128,6 +132,7 @@
|
|
|
128
132
|
"./erplora.css": "./dist/erplora.css",
|
|
129
133
|
"./palettes.css": "./dist/palettes.css",
|
|
130
134
|
"./layout.css": "./dist/layout.css",
|
|
135
|
+
"./tabbar.css": "./dist/tabbar.css",
|
|
131
136
|
"./cdn": "./dist/outfitkit.js",
|
|
132
137
|
"./bundle": "./dist/outfitkit.bundle.js"
|
|
133
138
|
},
|