@seedgrid/fe-components 2026.8.2 → 2026.8.4
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/dist/ai/seedgrid-components.manifest.json +26 -6
- package/dist/generated/compiledStyles.d.ts +1 -1
- package/dist/generated/compiledStyles.d.ts.map +1 -1
- package/dist/generated/compiledStyles.js +1 -1
- package/dist/inputs/SgDatatable.d.ts.map +1 -1
- package/dist/inputs/SgDatatable.js +44 -8
- package/dist/inputs/SgInputPassword.d.ts +8 -0
- package/dist/inputs/SgInputPassword.d.ts.map +1 -1
- package/dist/inputs/SgInputPassword.js +7 -2
- package/dist/layout/SgPageControl.d.ts +13 -0
- package/dist/layout/SgPageControl.d.ts.map +1 -1
- package/dist/layout/SgPageControl.js +56 -6
- package/dist/layout/SgPageControl.meta.d.ts.map +1 -1
- package/dist/layout/SgPageControl.meta.js +3 -1
- package/dist/sandbox.cjs +48 -48
- package/dist/style.css +1 -1
- package/package.json +3 -3
|
@@ -15,6 +15,19 @@ function resolveRecords(children, hiddenPageIds) {
|
|
|
15
15
|
return { id, props: element.props, element, hidden };
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
+
// <option> so aceita texto. `title` e ReactNode (pode vir com icone/markup), entao
|
|
19
|
+
// caimos para `hint` e, por ultimo, para o id — nunca para string vazia, que deixaria
|
|
20
|
+
// a opcao invisivel no select do celular.
|
|
21
|
+
function optionLabel(record) {
|
|
22
|
+
const { title, hint } = record.props;
|
|
23
|
+
if (typeof title === "string" && title.trim())
|
|
24
|
+
return title;
|
|
25
|
+
if (typeof title === "number")
|
|
26
|
+
return String(title);
|
|
27
|
+
if (hint?.trim())
|
|
28
|
+
return hint;
|
|
29
|
+
return record.id;
|
|
30
|
+
}
|
|
18
31
|
function clampIndex(index, length) {
|
|
19
32
|
if (length <= 0)
|
|
20
33
|
return 0;
|
|
@@ -23,7 +36,7 @@ function clampIndex(index, length) {
|
|
|
23
36
|
return Math.max(0, Math.min(length - 1, Math.floor(index)));
|
|
24
37
|
}
|
|
25
38
|
export function SgPageControl(props) {
|
|
26
|
-
const { children, activePageId, activeIndex, defaultActivePageId, defaultActiveIndex = 0, onActivePageIdChange, onActiveIndexChange, hiddenPageIds, keepMounted = false, pageControlStyle = "underline", size = "md", fullWidthTabs = false, keyboardNavigation = true, ariaLabel, emptyMessage, className, tabListClassName, tabClassName, panelClassName, style } = props;
|
|
39
|
+
const { children, activePageId, activeIndex, defaultActivePageId, defaultActiveIndex = 0, onActivePageIdChange, onActiveIndexChange, hiddenPageIds, keepMounted = false, pageControlStyle = "underline", size = "md", fullWidthTabs = false, mobilePageControl = "scroll", keyboardNavigation = true, ariaLabel, emptyMessage, className, tabListClassName, tabClassName, panelClassName, style } = props;
|
|
27
40
|
const i18n = useComponentsI18n();
|
|
28
41
|
const resolvedAriaLabel = ariaLabel ?? t(i18n, "components.pageControl.ariaLabel");
|
|
29
42
|
const resolvedEmptyMessage = emptyMessage ?? t(i18n, "components.pageControl.empty");
|
|
@@ -71,23 +84,50 @@ export function SgPageControl(props) {
|
|
|
71
84
|
fireOnChange(nextId);
|
|
72
85
|
}, [fireOnChange, isControlled, visiblePages]);
|
|
73
86
|
const tabsRef = React.useRef([]);
|
|
87
|
+
const tabListRef = React.useRef(null);
|
|
74
88
|
const rootId = React.useId();
|
|
89
|
+
// Com rolagem horizontal a aba ativa pode estar fora da area visivel — ao trocar de
|
|
90
|
+
// pagina por teclado, por codigo (modo controlado) ou pelo select do celular, ela
|
|
91
|
+
// precisa aparecer. Mexemos em scrollLeft em vez de scrollIntoView de proposito:
|
|
92
|
+
// scrollIntoView sobe pelos ancestrais e rola a PAGINA junto.
|
|
93
|
+
React.useEffect(() => {
|
|
94
|
+
const list = tabListRef.current;
|
|
95
|
+
const tab = tabsRef.current[activeIndexResolved];
|
|
96
|
+
if (!list || !tab)
|
|
97
|
+
return;
|
|
98
|
+
if (list.scrollWidth <= list.clientWidth)
|
|
99
|
+
return;
|
|
100
|
+
const margin = 8;
|
|
101
|
+
const tabStart = tab.offsetLeft;
|
|
102
|
+
const tabEnd = tabStart + tab.offsetWidth;
|
|
103
|
+
const viewStart = list.scrollLeft;
|
|
104
|
+
const viewEnd = viewStart + list.clientWidth;
|
|
105
|
+
if (tabStart < viewStart) {
|
|
106
|
+
list.scrollLeft = Math.max(0, tabStart - margin);
|
|
107
|
+
}
|
|
108
|
+
else if (tabEnd > viewEnd) {
|
|
109
|
+
list.scrollLeft = tabEnd - list.clientWidth + margin;
|
|
110
|
+
}
|
|
111
|
+
}, [activeIndexResolved]);
|
|
75
112
|
const sizeClasses = size === "sm"
|
|
76
113
|
? {
|
|
77
114
|
tab: "h-8 px-3 text-xs gap-1.5",
|
|
78
115
|
icon: "size-4",
|
|
79
|
-
panel: "p-3"
|
|
116
|
+
panel: "p-3",
|
|
117
|
+
select: "h-8 text-xs"
|
|
80
118
|
}
|
|
81
119
|
: size === "lg"
|
|
82
120
|
? {
|
|
83
121
|
tab: "h-11 px-4 text-sm gap-2.5",
|
|
84
122
|
icon: "size-5",
|
|
85
|
-
panel: "p-5"
|
|
123
|
+
panel: "p-5",
|
|
124
|
+
select: "h-11 text-sm"
|
|
86
125
|
}
|
|
87
126
|
: {
|
|
88
127
|
tab: "h-9 px-3.5 text-sm gap-2",
|
|
89
128
|
icon: "size-4.5",
|
|
90
|
-
panel: "p-4"
|
|
129
|
+
panel: "p-4",
|
|
130
|
+
select: "h-9 text-sm"
|
|
91
131
|
};
|
|
92
132
|
const tabStyle = pageControlStyle === "pills"
|
|
93
133
|
? {
|
|
@@ -100,7 +140,14 @@ export function SgPageControl(props) {
|
|
|
100
140
|
active: "border-primary text-primary bg-muted/30",
|
|
101
141
|
inactive: ""
|
|
102
142
|
};
|
|
103
|
-
return (_jsxs("div", { className: cn("w-full", className), style: style, children: [
|
|
143
|
+
return (_jsxs("div", { className: cn("w-full", className), style: style, children: [mobilePageControl === "select" && visiblePages.length > 0 ? (
|
|
144
|
+
// Alternativa do celular: as abas viram um select nativo. Fica escondido a partir
|
|
145
|
+
// do tablet, e a troca e feita por CSS (nao por media query em JS) para nao
|
|
146
|
+
// depender do tamanho da janela na primeira renderizacao.
|
|
147
|
+
_jsx("div", { className: "mb-2 md:hidden", children: _jsx("select", { "aria-label": resolvedAriaLabel, value: resolvedActiveId ?? "", onChange: (event) => selectPage(event.target.value), className: cn("w-full rounded-md border border-border bg-background px-3 font-medium text-foreground", sizeClasses.select), children: visiblePages.map((record) => (_jsx("option", { value: record.id, disabled: record.props.disabled, children: optionLabel(record) }, record.id))) }) })) : null, _jsx("div", { role: "tablist", ref: tabListRef, "aria-label": resolvedAriaLabel, className: cn("min-h-0 items-end gap-1 overflow-x-auto border-b border-border pb-0.5",
|
|
148
|
+
// Um unico utilitario de display por breakpoint. Antes `flex` e `grid` saiam
|
|
149
|
+
// juntos no mesmo elemento e quem vencia dependia da ordem no CSS gerado.
|
|
150
|
+
mobilePageControl === "select" ? "hidden" : "flex", fullWidthTabs ? "md:grid md:auto-cols-fr md:grid-flow-col" : "md:flex", tabListClassName), onKeyDown: (event) => {
|
|
104
151
|
if (!keyboardNavigation || visiblePages.length === 0)
|
|
105
152
|
return;
|
|
106
153
|
const focused = document.activeElement;
|
|
@@ -141,7 +188,10 @@ export function SgPageControl(props) {
|
|
|
141
188
|
const panelId = `${rootId}-panel-${record.id}`;
|
|
142
189
|
return (_jsxs("button", { ref: (el) => {
|
|
143
190
|
tabsRef.current[index] = el;
|
|
144
|
-
}, type: "button", id: tabId, role: "tab", "aria-controls": panelId, "aria-selected": isActive, tabIndex: isActive ? 0 : -1, title: record.props.hint, disabled: record.props.disabled, onClick: () => selectPage(record.id), className: cn("inline-flex shrink-0 items-center justify-center whitespace-nowrap font-medium transition-colors", sizeClasses.tab, tabStyle.base, isActive ? tabStyle.active : tabStyle.inactive,
|
|
191
|
+
}, type: "button", id: tabId, role: "tab", "aria-controls": panelId, "aria-selected": isActive, tabIndex: isActive ? 0 : -1, title: record.props.hint, disabled: record.props.disabled, onClick: () => selectPage(record.id), className: cn("inline-flex shrink-0 items-center justify-center whitespace-nowrap font-medium transition-colors", sizeClasses.tab, tabStyle.base, isActive ? tabStyle.active : tabStyle.inactive,
|
|
192
|
+
// No celular a lista e flex com rolagem: `w-full` faria cada aba ocupar a
|
|
193
|
+
// largura toda do container.
|
|
194
|
+
fullWidthTabs ? "md:w-full" : "", record.props.disabled ? "cursor-not-allowed opacity-45" : "", tabClassName, record.props.tabClassName), children: [record.props.icon ? (_jsx("span", { className: cn("inline-flex items-center justify-center", sizeClasses.icon), children: record.props.icon })) : null, _jsx("span", { className: "truncate", children: record.props.title })] }, record.id));
|
|
145
195
|
}) }), _jsx("div", { className: cn("rounded-b-md border border-t-0 border-border bg-background", panelClassName), children: visiblePages.length === 0 ? (_jsx("div", { className: cn(sizeClasses.panel, "text-sm text-muted-foreground"), children: resolvedEmptyMessage })) : keepMounted ? (visiblePages.map((record) => {
|
|
146
196
|
const isActive = record.id === resolvedActiveId;
|
|
147
197
|
const tabId = `${rootId}-tab-${record.id}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SgPageControl.meta.d.ts","sourceRoot":"","sources":["../../src/layout/SgPageControl.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"SgPageControl.meta.d.ts","sourceRoot":"","sources":["../../src/layout/SgPageControl.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,MAAM,EAAE,QAgCpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WAmBrB,CAAC"}
|
|
@@ -9,7 +9,7 @@ export const sgMeta = {
|
|
|
9
9
|
subcategory: "page-control",
|
|
10
10
|
description: "Controle de paginas em abas para alternar entre paineis de conteudo relacionados em uma mesma area da tela.",
|
|
11
11
|
tags: ["navigation", "tabs", "page-control", "panels"],
|
|
12
|
-
capabilities: ["tab-navigation", "controlled-active-page", "hidden-pages", "keyboard-navigation"],
|
|
12
|
+
capabilities: ["tab-navigation", "controlled-active-page", "hidden-pages", "keyboard-navigation", "responsive-tabs"],
|
|
13
13
|
fieldSemantics: ["tabs", "pageTabs", "panelNavigation", "sectionSwitcher"],
|
|
14
14
|
props: [
|
|
15
15
|
{ name: "children", type: "ReactNode", required: true, description: "Paginas filhas definidas por SgPageControlPage.", semanticRole: "data", bindable: false },
|
|
@@ -18,6 +18,8 @@ export const sgMeta = {
|
|
|
18
18
|
{ name: "keepMounted", type: "boolean", default: false, description: "Mantem paineis inativos montados no DOM.", semanticRole: "behavior", bindable: true },
|
|
19
19
|
{ name: "pageControlStyle", type: '"underline" | "pills"', default: "underline", description: "Estilo visual da lista de abas.", semanticRole: "appearance", bindable: true },
|
|
20
20
|
{ name: "size", type: '"sm" | "md" | "lg"', default: "md", description: "Escala visual das abas.", semanticRole: "appearance", bindable: true },
|
|
21
|
+
{ name: "fullWidthTabs", type: "boolean", default: false, description: "Distribui as abas em larguras iguais a partir do tablet; no celular elas mantem a largura natural e a lista rola.", semanticRole: "appearance", bindable: true },
|
|
22
|
+
{ name: "mobilePageControl", type: '"scroll" | "select"', default: "scroll", description: "Comportamento no celular: rolagem horizontal das abas ou troca por um select nativo.", semanticRole: "behavior", bindable: true },
|
|
21
23
|
{ name: "onActivePageIdChange", type: "(pageId: string, context: object) => void", description: "Callback disparado quando a pagina ativa muda.", semanticRole: "event", bindable: false }
|
|
22
24
|
],
|
|
23
25
|
states: ["default", "controlled", "keyboard-navigation"],
|