@motor-hero/ui-kit 0.48.1 → 0.50.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.
- package/dist/components/app-shell.d.ts +9 -1
- package/dist/components/app-shell.d.ts.map +1 -1
- package/dist/components/pagination.d.ts +13 -4
- package/dist/components/pagination.d.ts.map +1 -1
- package/dist/index.cjs +101 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +94 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/app-shell.tsx +31 -7
- package/src/components/pagination.tsx +79 -7
package/package.json
CHANGED
|
@@ -28,6 +28,14 @@ export interface AppShellProps {
|
|
|
28
28
|
sidebarFooter?: ReactNode | ((collapsed: boolean) => ReactNode)
|
|
29
29
|
/** Above the header, full width of the content column — `UpdateBanner` goes here. */
|
|
30
30
|
banner?: ReactNode
|
|
31
|
+
/**
|
|
32
|
+
* Merged into the sidebar (desktop and mobile drawer), e.g.
|
|
33
|
+
* `bg-sidebar text-sidebar-foreground border-sidebar-border`. Inner dividers
|
|
34
|
+
* follow the sidebar's border color.
|
|
35
|
+
*/
|
|
36
|
+
sidebarClassName?: string
|
|
37
|
+
/** Merged into the top header bar. */
|
|
38
|
+
headerClassName?: string
|
|
31
39
|
children: ReactNode
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -47,6 +55,8 @@ export function AppShell({
|
|
|
47
55
|
defaultCollapsed = false,
|
|
48
56
|
sidebarFooter,
|
|
49
57
|
banner,
|
|
58
|
+
sidebarClassName,
|
|
59
|
+
headerClassName,
|
|
50
60
|
children,
|
|
51
61
|
}: AppShellProps) {
|
|
52
62
|
const [collapsed, setCollapsed] = useState(defaultCollapsed)
|
|
@@ -68,24 +78,29 @@ export function AppShell({
|
|
|
68
78
|
<div className="flex h-screen overflow-hidden bg-background">
|
|
69
79
|
{/* Desktop sidebar */}
|
|
70
80
|
<aside
|
|
81
|
+
data-slot="sidebar"
|
|
71
82
|
className={cn(
|
|
72
83
|
"hidden shrink-0 flex-col border-r bg-card transition-all duration-300 md:flex",
|
|
73
84
|
collapsed ? "w-16" : "w-60",
|
|
85
|
+
sidebarClassName,
|
|
74
86
|
)}
|
|
75
87
|
>
|
|
76
|
-
<div className="flex h-14 shrink-0 items-center border-b px-4">
|
|
88
|
+
<div className="flex h-14 shrink-0 items-center border-b border-inherit px-4" data-slot="sidebar-brand">
|
|
77
89
|
{collapsed ? (brandCollapsed ?? brand) : brand}
|
|
78
90
|
</div>
|
|
79
|
-
<div className="flex-1 overflow-y-auto px-2 py-4">
|
|
91
|
+
<div className="flex-1 overflow-y-auto px-2 py-4" data-slot="sidebar-content">
|
|
92
|
+
{nav(collapsed)}
|
|
93
|
+
</div>
|
|
80
94
|
{sidebarFooter && (
|
|
81
|
-
<div className="border-t px-3 py-2" data-slot="sidebar-footer">
|
|
95
|
+
<div className="border-t border-inherit px-3 py-2" data-slot="sidebar-footer">
|
|
82
96
|
{typeof sidebarFooter === "function" ? sidebarFooter(collapsed) : sidebarFooter}
|
|
83
97
|
</div>
|
|
84
98
|
)}
|
|
85
99
|
{collapsible && (
|
|
86
|
-
<div className="border-t p-2">
|
|
100
|
+
<div className="border-t border-inherit p-2">
|
|
87
101
|
<button
|
|
88
102
|
type="button"
|
|
103
|
+
data-slot="sidebar-toggle"
|
|
89
104
|
onClick={() => setCollapsed((v) => !v)}
|
|
90
105
|
className="flex w-full items-center justify-center rounded-md px-2 py-2 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
91
106
|
aria-label={collapsed ? "Expandir menu" : "Recolher menu"}
|
|
@@ -106,21 +121,30 @@ export function AppShell({
|
|
|
106
121
|
>
|
|
107
122
|
<div className="absolute inset-0 bg-black/50" />
|
|
108
123
|
<div
|
|
124
|
+
data-slot="sidebar-drawer"
|
|
109
125
|
className={cn(
|
|
110
126
|
"absolute left-0 top-0 flex h-full w-64 flex-col border-r bg-card transition-transform duration-300",
|
|
111
127
|
mobileOpen ? "translate-x-0" : "-translate-x-full",
|
|
128
|
+
sidebarClassName,
|
|
112
129
|
)}
|
|
113
130
|
onClick={(e) => e.stopPropagation()}
|
|
114
131
|
>
|
|
115
|
-
<div className="flex h-14 shrink-0 items-center border-b px-4"
|
|
116
|
-
|
|
132
|
+
<div className="flex h-14 shrink-0 items-center border-b border-inherit px-4" data-slot="sidebar-brand">
|
|
133
|
+
{brand}
|
|
134
|
+
</div>
|
|
135
|
+
<div className="flex-1 overflow-y-auto px-2 py-4" data-slot="sidebar-content">
|
|
136
|
+
{nav(false, () => setMobileOpen(false))}
|
|
137
|
+
</div>
|
|
117
138
|
</div>
|
|
118
139
|
</div>
|
|
119
140
|
|
|
120
141
|
{/* Main column */}
|
|
121
142
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
122
143
|
{banner}
|
|
123
|
-
<header
|
|
144
|
+
<header
|
|
145
|
+
data-slot="header"
|
|
146
|
+
className={cn("flex h-14 shrink-0 items-center gap-2 border-b bg-background px-4", headerClassName)}
|
|
147
|
+
>
|
|
124
148
|
<button
|
|
125
149
|
type="button"
|
|
126
150
|
onClick={() => setMobileOpen(true)}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { useId, useState } from "react"
|
|
2
|
+
|
|
3
|
+
interface PaginationBaseProps {
|
|
2
4
|
page: number
|
|
3
5
|
onPageChange: (page: number) => void
|
|
4
6
|
hasNextPage: boolean
|
|
5
7
|
hasPreviousPage: boolean
|
|
6
|
-
/** Total de páginas. Quando informado, o rótulo vira "Página {page} de {totalPages}". */
|
|
7
|
-
totalPages?: number
|
|
8
8
|
/** Exibe o seletor de itens por página. Use com `pageSize`, `onPageSizeChange` e `pageSizeOptions`. */
|
|
9
9
|
showPageSizeSelector?: boolean
|
|
10
10
|
pageSize?: number
|
|
@@ -13,6 +13,20 @@ export interface PaginationProps {
|
|
|
13
13
|
className?: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
type PageJumpProps =
|
|
17
|
+
| {
|
|
18
|
+
/** Total de páginas. Quando informado, o rótulo vira "Página {page} de {totalPages}". */
|
|
19
|
+
totalPages?: number
|
|
20
|
+
showPageJump?: false
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
totalPages: number
|
|
24
|
+
/** Torna o número da página editável para pular direto a uma página. Exige `totalPages`. */
|
|
25
|
+
showPageJump: true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type PaginationProps = PaginationBaseProps & PageJumpProps
|
|
29
|
+
|
|
16
30
|
// `max-sm:h-10`: alvo de toque de 40px no celular; o desktop segue em 32px.
|
|
17
31
|
const navButtonClass =
|
|
18
32
|
"inline-flex h-8 max-sm:h-10 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer disabled:pointer-events-none disabled:opacity-50"
|
|
@@ -23,6 +37,7 @@ export function Pagination({
|
|
|
23
37
|
hasNextPage,
|
|
24
38
|
hasPreviousPage,
|
|
25
39
|
totalPages,
|
|
40
|
+
showPageJump = false,
|
|
26
41
|
showPageSizeSelector = false,
|
|
27
42
|
pageSize,
|
|
28
43
|
onPageSizeChange,
|
|
@@ -64,10 +79,14 @@ export function Pagination({
|
|
|
64
79
|
>
|
|
65
80
|
Anterior
|
|
66
81
|
</button>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
{showPageJump && totalPages !== undefined ? (
|
|
83
|
+
<PageJumpInput page={page} totalPages={totalPages} onPageChange={onPageChange} />
|
|
84
|
+
) : (
|
|
85
|
+
<span className="text-sm text-muted-foreground">
|
|
86
|
+
Página {page}
|
|
87
|
+
{totalPages ? ` de ${totalPages}` : ""}
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
71
90
|
<button
|
|
72
91
|
type="button"
|
|
73
92
|
onClick={() => onPageChange(page + 1)}
|
|
@@ -80,3 +99,56 @@ export function Pagination({
|
|
|
80
99
|
</div>
|
|
81
100
|
)
|
|
82
101
|
}
|
|
102
|
+
|
|
103
|
+
interface PageJumpInputProps {
|
|
104
|
+
page: number
|
|
105
|
+
totalPages: number
|
|
106
|
+
onPageChange: (page: number) => void
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function PageJumpInput({ page, totalPages, onPageChange }: PageJumpInputProps) {
|
|
110
|
+
const inputId = useId()
|
|
111
|
+
// `null` = sem edição em andamento; o campo mostra a página atual.
|
|
112
|
+
const [draft, setDraft] = useState<string | null>(null)
|
|
113
|
+
const [prevPage, setPrevPage] = useState(page)
|
|
114
|
+
if (page !== prevPage) {
|
|
115
|
+
setPrevPage(page)
|
|
116
|
+
setDraft(null)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Enter e blur confirmam; valores fora do intervalo são ajustados em silêncio.
|
|
120
|
+
const commit = () => {
|
|
121
|
+
if (draft === null) return
|
|
122
|
+
setDraft(null)
|
|
123
|
+
if (draft === "") return
|
|
124
|
+
const target = Math.max(1, Math.min(Number(draft), totalPages))
|
|
125
|
+
if (target !== page) onPageChange(target)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<span className="flex items-center gap-1.5 whitespace-nowrap text-sm text-muted-foreground">
|
|
130
|
+
<label htmlFor={inputId} className="max-sm:sr-only">
|
|
131
|
+
Página
|
|
132
|
+
</label>
|
|
133
|
+
<input
|
|
134
|
+
id={inputId}
|
|
135
|
+
type="text"
|
|
136
|
+
inputMode="numeric"
|
|
137
|
+
enterKeyHint="go"
|
|
138
|
+
autoComplete="off"
|
|
139
|
+
aria-label="Ir para a página"
|
|
140
|
+
value={draft ?? String(page)}
|
|
141
|
+
onChange={(e) => setDraft(e.target.value.replace(/\D/g, ""))}
|
|
142
|
+
onFocus={(e) => e.target.select()}
|
|
143
|
+
onBlur={commit}
|
|
144
|
+
onKeyDown={(e) => {
|
|
145
|
+
if (e.key === "Enter") commit()
|
|
146
|
+
else if (e.key === "Escape") setDraft(null)
|
|
147
|
+
}}
|
|
148
|
+
style={{ width: `${String(totalPages).length + 3}ch` }}
|
|
149
|
+
className="h-8 max-sm:h-10 rounded-md border border-input bg-background px-1.5 text-center text-xs font-medium tabular-nums text-foreground shadow-sm transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
150
|
+
/>
|
|
151
|
+
<span>de {totalPages}</span>
|
|
152
|
+
</span>
|
|
153
|
+
)
|
|
154
|
+
}
|