@motor-hero/ui-kit 0.46.1 → 0.48.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.46.1",
3
+ "version": "0.48.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",
@@ -49,6 +49,7 @@
49
49
  "lint": "tsc --noEmit",
50
50
  "docs:dev": "vite --config docs/vite.config.ts",
51
51
  "docs:build": "vite build --config docs/vite.config.ts",
52
+ "release": "scripts/release.sh",
52
53
  "prepublishOnly": "npm run build",
53
54
  "prepare": "husky"
54
55
  },
@@ -124,7 +124,7 @@ export function AppShell({
124
124
  <button
125
125
  type="button"
126
126
  onClick={() => setMobileOpen(true)}
127
- className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-input bg-background shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground md:hidden"
127
+ className="inline-flex h-9 w-9 max-sm:h-10 max-sm:w-10 items-center justify-center rounded-md border border-input bg-background shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground md:hidden"
128
128
  aria-label="Abrir menu"
129
129
  >
130
130
  <Menu className="h-4 w-4" />
@@ -58,15 +58,26 @@ export interface BaseDialogProps {
58
58
 
59
59
  /**
60
60
  * Standardized, responsive dialog shell:
61
- * - mobile: bottom sheet (full width, up to 92dvh tall)
61
+ * - mobile: bottom sheet that can grow to nearly the whole screen
62
62
  * - desktop: centered dialog (up to 92dvh tall)
63
63
  * Header and footer stay fixed; only the body scrolls.
64
64
  *
65
+ * The mobile ceiling is the screen minus a slim top gap (safe area or
66
+ * 0.75rem, whichever is larger): a long form gets virtually the whole
67
+ * screen — the old 92dvh cap wasted ~70px on the one device where height is
68
+ * scarce — while the rounded top and the sliver of scrim above it keep the
69
+ * sheet language every other mobile surface in this kit speaks (combobox,
70
+ * pickers, menus are all `rounded-t-2xl` bottom sheets). An edge-to-edge
71
+ * square dialog here read as a foreign screen, not a layer of this app.
72
+ * Short dialogs still hug their content instead of stretching.
73
+ *
65
74
  * Heights use `dvh`, not `vh`: on iOS `vh` is locked to the large viewport
66
- * (toolbars collapsed), so `92vh` can exceed the visible area by 10-15%. With
67
- * the sheet anchored to `bottom-0` the overflow leaves through the top, and
68
- * since the body is the only scrollable child, what goes off-screen is the
69
- * header - title and close button. Does not reproduce outside real iOS.
75
+ * (toolbars collapsed), so a `vh` cap can exceed the visible area by
76
+ * 10-15%. With the sheet anchored to `bottom-0` the overflow leaves
77
+ * through the top, and since the body is the only scrollable child, what
78
+ * goes off-screen is the header - title and close button. Does not
79
+ * reproduce outside real iOS. The footer pads for the home indicator
80
+ * (`env(safe-area-inset-bottom)`); the top gap already clears the notch.
70
81
  *
71
82
  * Also fixes the known Radix bug where `body { pointer-events: none }`
72
83
  * can persist after closing a dialog that contained a Select/Popover,
@@ -119,7 +130,7 @@ export function BaseDialog({
119
130
  </div>
120
131
  <div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">{children}</div>
121
132
  {footer && (
122
- <div className="flex shrink-0 flex-col-reverse gap-2 border-t px-6 py-3 sm:flex-row sm:justify-end">
133
+ <div className="flex shrink-0 flex-col-reverse gap-2 border-t px-6 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:flex-row sm:justify-end">
123
134
  {footer}
124
135
  </div>
125
136
  )}
@@ -159,9 +170,13 @@ export function BaseDialog({
159
170
  document.body.style.pointerEvents = ""
160
171
  }}
161
172
  className={cn(
162
- "fixed z-50 flex max-h-[92dvh] flex-col bg-background shadow-lg outline-none",
163
- "inset-x-0 bottom-0 rounded-t-2xl",
164
- "sm:inset-x-auto sm:bottom-auto sm:left-1/2 sm:top-1/2 sm:max-h-[92dvh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg",
173
+ "fixed z-50 flex w-full flex-col bg-background shadow-lg outline-none",
174
+ "inset-x-0 bottom-0 rounded-t-2xl max-h-[calc(100dvh_-_max(0.75rem,env(safe-area-inset-top)))]",
175
+ // Entra deslizando de baixo, como os drawers (vaul) do restante do
176
+ // kit; no desktop os slides sairiam brigando com o translate de
177
+ // centralização, então ficam restritos ao mobile.
178
+ "max-sm:slide-in-from-bottom-8 max-sm:slide-out-to-bottom-8",
179
+ "sm:inset-x-auto sm:bottom-auto sm:left-1/2 sm:top-1/2 sm:max-h-[92dvh] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg",
165
180
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
166
181
  sizeClasses[size],
167
182
  className,
@@ -269,7 +269,7 @@ function DatePickerImpl({
269
269
  aria-haspopup="dialog"
270
270
  aria-expanded={open}
271
271
  onClick={() => setOpen(!open)}
272
- className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
272
+ className="absolute right-1 top-1/2 inline-flex size-8 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
273
273
  >
274
274
  {withTime ? <Clock className="h-4 w-4" /> : <CalendarIcon className="h-4 w-4" />}
275
275
  </button>
@@ -10,7 +10,7 @@ export function ModeToggle() {
10
10
  <DropdownMenu.Trigger asChild>
11
11
  <button
12
12
  type="button"
13
- className="relative inline-flex h-9 w-9 items-center justify-center rounded-md border border-input bg-background text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
13
+ className="relative inline-flex h-9 w-9 max-sm:h-10 max-sm:w-10 items-center justify-center rounded-md border border-input bg-background text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
14
14
  aria-label="Alternar tema"
15
15
  >
16
16
  <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
@@ -13,8 +13,9 @@ export interface PaginationProps {
13
13
  className?: string
14
14
  }
15
15
 
16
+ // `max-sm:h-10`: alvo de toque de 40px no celular; o desktop segue em 32px.
16
17
  const navButtonClass =
17
- "inline-flex h-8 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"
18
+ "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"
18
19
 
19
20
  export function Pagination({
20
21
  page,
@@ -30,7 +31,10 @@ export function Pagination({
30
31
  }: PaginationProps) {
31
32
  return (
32
33
  <div
33
- className={`flex items-center ${showPageSizeSelector ? "justify-between" : "justify-end"} gap-4 ${className ?? ""}`}
34
+ // Quebra em vez de empurrar: num celular de 360px os dois grupos não
35
+ // cabem na mesma linha e o "Próximo" saía da área visível, com a lista
36
+ // inteira rolando de lado atrás dele.
37
+ className={`flex flex-wrap items-center ${showPageSizeSelector ? "justify-between" : "justify-end"} gap-x-4 gap-y-2 ${className ?? ""}`}
34
38
  >
35
39
  {showPageSizeSelector && (
36
40
  <div className="flex items-center gap-2">
@@ -41,7 +45,7 @@ export function Pagination({
41
45
  id="pagination-page-size"
42
46
  value={pageSize}
43
47
  onChange={(e) => onPageSizeChange?.(Number(e.target.value))}
44
- className="h-8 rounded-md border border-input bg-background px-2 text-xs font-medium shadow-sm transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
48
+ className="h-8 max-sm:h-10 rounded-md border border-input bg-background px-2 text-xs font-medium shadow-sm transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
45
49
  >
46
50
  {(pageSizeOptions ?? []).map((size) => (
47
51
  <option key={size} value={size}>
@@ -25,7 +25,7 @@ export const PasswordInput = React.forwardRef<
25
25
  onClick={() => setVisible((prev) => !prev)}
26
26
  aria-label={visible ? "Ocultar senha" : "Mostrar senha"}
27
27
  tabIndex={-1}
28
- className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
28
+ className="absolute right-1 top-1/2 inline-flex size-8 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground"
29
29
  >
30
30
  {visible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
31
31
  </button>
@@ -20,6 +20,24 @@ interface ResponsiveDataViewProps {
20
20
  /** CTA do estado vazio — normalmente o mesmo botão "Adicionar X" da página. */
21
21
  emptyAction?: ReactNode
22
22
  pagination?: ReactNode
23
+ /**
24
+ * A partir de qual largura a tabela substitui os cards. Padrão `md`. Use
25
+ * `lg`/`xl` quando a tabela tem colunas demais para caber ao lado da barra
26
+ * lateral num tablet — os cards leem melhor do que uma tabela rolando de lado.
27
+ */
28
+ breakpoint?: ResponsiveDataViewBreakpoint
29
+ }
30
+
31
+ export type ResponsiveDataViewBreakpoint = "md" | "lg" | "xl"
32
+
33
+ // Classes literais: o Tailwind do consumidor só gera o que encontra escrito.
34
+ const BREAKPOINT_CLASSES: Record<
35
+ ResponsiveDataViewBreakpoint,
36
+ { table: string; cards: string }
37
+ > = {
38
+ md: { table: "md:block", cards: "md:hidden" },
39
+ lg: { table: "lg:block", cards: "lg:hidden" },
40
+ xl: { table: "xl:block", cards: "xl:hidden" },
23
41
  }
24
42
 
25
43
  export function ResponsiveDataView({
@@ -33,6 +51,7 @@ export function ResponsiveDataView({
33
51
  emptyDescription,
34
52
  emptyAction,
35
53
  pagination,
54
+ breakpoint = "md",
36
55
  }: ResponsiveDataViewProps) {
37
56
  // Lista vazia mostra só o EmptyState. Antes renderizava a caixa com o
38
57
  // cabeçalho da tabela vazio, o bloco de texto por fora dela e a paginação
@@ -51,18 +70,20 @@ export function ResponsiveDataView({
51
70
  }
52
71
 
53
72
  const busyClass = isBusy && "pointer-events-none opacity-50"
73
+ const visibility = BREAKPOINT_CLASSES[breakpoint]
54
74
  return (
55
75
  <div className="space-y-4">
56
76
  <div
57
77
  aria-busy={isBusy}
58
78
  className={cn(
59
- "hidden overflow-x-auto rounded-md border transition-opacity md:block",
79
+ "hidden overflow-x-auto rounded-md border transition-opacity",
80
+ visibility.table,
60
81
  busyClass,
61
82
  )}
62
83
  >
63
84
  {table}
64
85
  </div>
65
- <div aria-busy={isBusy} className={cn("transition-opacity md:hidden", busyClass)}>
86
+ <div aria-busy={isBusy} className={cn("transition-opacity", visibility.cards, busyClass)}>
66
87
  {cards}
67
88
  </div>
68
89
  {pagination}
@@ -27,8 +27,9 @@ export interface RowActionsMenuProps {
27
27
  renderLink?: RenderLink
28
28
  }
29
29
 
30
+ // 32px no desktop, 40px de alvo de toque no celular.
30
31
  const triggerClass =
31
- "inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
32
+ "inline-flex h-8 w-8 max-sm:h-10 max-sm:w-10 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
32
33
 
33
34
  // Desktop: itens compactos do dropdown. Mobile: alvos de toque maiores.
34
35
  const itemClass =
@@ -57,7 +57,7 @@ export function UserMenu({
57
57
  const trigger = (
58
58
  <button
59
59
  type="button"
60
- className="inline-flex h-9 w-9 items-center justify-center overflow-hidden rounded-full border border-input bg-background text-xs font-semibold shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
60
+ className="inline-flex h-9 w-9 max-sm:h-10 max-sm:w-10 items-center justify-center overflow-hidden rounded-full border border-input bg-background text-xs font-semibold shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
61
61
  aria-label="Menu do usuário"
62
62
  data-testid="user-menu"
63
63
  >
package/src/index.ts CHANGED
@@ -43,7 +43,7 @@ export type { PaginationProps } from "./components/pagination";
43
43
  export { Pagination } from "./components/pagination";
44
44
  export { PasswordInput } from "./components/password-input";
45
45
  export { ProgressBar } from "./components/progress-bar";
46
- export { ResponsiveDataView } from "./components/responsive-data-view";
46
+ export { ResponsiveDataView, type ResponsiveDataViewBreakpoint } from "./components/responsive-data-view";
47
47
  export type {
48
48
  RowAction,
49
49
  RowActionsMenuProps,