@motor-hero/ui-kit 0.38.0 → 0.40.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motor-hero/ui-kit",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Componentes React reutilizáveis para o ecossistema MotorHero — shadcn/ui + Tailwind CSS v4",
5
5
  "author": "MotorHero",
6
6
  "license": "UNLICENSED",
@@ -1,11 +1,19 @@
1
1
  import type { ReactNode } from "react"
2
2
  import { EmptyState } from "./empty-state"
3
3
 
4
+ import { cn } from "../lib/utils"
5
+
4
6
  interface ResponsiveDataViewProps {
5
7
  table: ReactNode
6
8
  cards: ReactNode
7
9
  isEmpty: boolean
8
10
  isLoading: boolean
11
+ /**
12
+ * Dados desatualizados na tela enquanto um refetch está em voo (ex.:
13
+ * `isPlaceholderData` do TanStack Query). Esmaece e bloqueia a interação
14
+ * com table/cards; a paginação continua clicável.
15
+ */
16
+ isBusy?: boolean
9
17
  emptyIcon?: ReactNode
10
18
  emptyTitle?: string
11
19
  emptyDescription?: string
@@ -19,6 +27,7 @@ export function ResponsiveDataView({
19
27
  cards,
20
28
  isEmpty,
21
29
  isLoading,
30
+ isBusy = false,
22
31
  emptyIcon,
23
32
  emptyTitle = "Nenhum registro encontrado",
24
33
  emptyDescription,
@@ -28,7 +37,7 @@ export function ResponsiveDataView({
28
37
  // Lista vazia mostra só o EmptyState. Antes renderizava a caixa com o
29
38
  // cabeçalho da tabela vazio, o bloco de texto por fora dela e a paginação
30
39
  // desabilitada — três blocos empilhados para zero registro.
31
- if (!isLoading && isEmpty) {
40
+ if (!isLoading && !isBusy && isEmpty) {
32
41
  return (
33
42
  <div className="rounded-md border">
34
43
  <EmptyState
@@ -41,12 +50,21 @@ export function ResponsiveDataView({
41
50
  )
42
51
  }
43
52
 
53
+ const busyClass = isBusy && "pointer-events-none opacity-50"
44
54
  return (
45
55
  <div className="space-y-4">
46
- <div className="hidden overflow-x-auto rounded-md border md:block">
56
+ <div
57
+ aria-busy={isBusy}
58
+ className={cn(
59
+ "hidden overflow-x-auto rounded-md border transition-opacity md:block",
60
+ busyClass,
61
+ )}
62
+ >
47
63
  {table}
48
64
  </div>
49
- <div className="md:hidden">{cards}</div>
65
+ <div aria-busy={isBusy} className={cn("transition-opacity md:hidden", busyClass)}>
66
+ {cards}
67
+ </div>
50
68
  {pagination}
51
69
  </div>
52
70
  )
@@ -1,5 +1,5 @@
1
1
  import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
2
- import { MoreVertical } from "lucide-react"
2
+ import { ChevronRight, MoreVertical } from "lucide-react"
3
3
  import { type ReactNode, useState } from "react"
4
4
  import { Drawer as DrawerPrimitive } from "vaul"
5
5
  import { useIsDesktop } from "../hooks/use-is-desktop"
@@ -14,6 +14,9 @@ export interface RowAction {
14
14
  href?: string
15
15
  destructive?: boolean
16
16
  disabled?: boolean
17
+ // Submenu de um nível: com `children`, `onClick`/`href` são ignorados.
18
+ // Desktop: Radix Sub; mobile: grupo inline no action sheet.
19
+ children?: RowAction[]
17
20
  }
18
21
 
19
22
  export interface RowActionsMenuProps {
@@ -72,6 +75,42 @@ export function RowActionsMenu({
72
75
  action.destructive &&
73
76
  "text-destructive focus:bg-destructive/10",
74
77
  )
78
+ if (action.children?.length) {
79
+ return (
80
+ <DropdownMenu.Sub key={action.label}>
81
+ <DropdownMenu.SubTrigger
82
+ className={cls}
83
+ disabled={action.disabled}
84
+ >
85
+ {action.icon}
86
+ {action.label}
87
+ <ChevronRight className="ml-auto h-4 w-4 text-muted-foreground" />
88
+ </DropdownMenu.SubTrigger>
89
+ <DropdownMenu.Portal>
90
+ <DropdownMenu.SubContent
91
+ sideOffset={4}
92
+ className="z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
93
+ >
94
+ {action.children.map((child) => (
95
+ <DropdownMenu.Item
96
+ key={child.label}
97
+ className={cn(
98
+ itemClass,
99
+ child.destructive &&
100
+ "text-destructive focus:bg-destructive/10",
101
+ )}
102
+ disabled={child.disabled}
103
+ onClick={child.onClick}
104
+ >
105
+ {child.icon}
106
+ {child.label}
107
+ </DropdownMenu.Item>
108
+ ))}
109
+ </DropdownMenu.SubContent>
110
+ </DropdownMenu.Portal>
111
+ </DropdownMenu.Sub>
112
+ )
113
+ }
75
114
  return action.href ? (
76
115
  <DropdownMenu.Item
77
116
  key={action.label}
@@ -129,6 +168,36 @@ export function RowActionsMenu({
129
168
  {action.label}
130
169
  </>
131
170
  )
171
+ if (action.children?.length) {
172
+ return (
173
+ <div key={action.label}>
174
+ <div className="mx-3 my-1 h-px bg-border" />
175
+ <div className="flex items-center gap-3 px-3 pb-1 pt-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
176
+ {action.icon}
177
+ {action.label}
178
+ </div>
179
+ {action.children.map((child) => (
180
+ <button
181
+ key={child.label}
182
+ type="button"
183
+ disabled={child.disabled}
184
+ className={cn(
185
+ sheetItemClass,
186
+ child.destructive && "text-destructive",
187
+ )}
188
+ onClick={() => {
189
+ child.onClick?.()
190
+ setOpen(false)
191
+ }}
192
+ >
193
+ {child.icon}
194
+ {child.label}
195
+ </button>
196
+ ))}
197
+ <div className="mx-3 my-1 h-px bg-border" />
198
+ </div>
199
+ )
200
+ }
132
201
  return action.href ? (
133
202
  <span
134
203
  key={action.label}