@motor-hero/ui-kit 0.25.1 → 0.27.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.25.1",
3
+ "version": "0.27.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",
@@ -0,0 +1,126 @@
1
+ import * as Dialog from "@radix-ui/react-dialog"
2
+ import { X } from "lucide-react"
3
+ import * as React from "react"
4
+ import type { ReactNode } from "react"
5
+
6
+ import { useIsDesktop } from "../hooks/use-is-desktop"
7
+ import { cn } from "../lib/utils"
8
+
9
+ const sizeClasses = {
10
+ sm: "sm:max-w-sm",
11
+ md: "sm:max-w-md",
12
+ lg: "sm:max-w-lg",
13
+ xl: "sm:max-w-xl",
14
+ "2xl": "sm:max-w-2xl",
15
+ } as const
16
+
17
+ export type BaseDialogSize = keyof typeof sizeClasses
18
+
19
+ export interface BaseDialogProps {
20
+ open: boolean
21
+ onOpenChange: (open: boolean) => void
22
+ title: string
23
+ description?: ReactNode
24
+ children: ReactNode
25
+ footer?: ReactNode
26
+ size?: BaseDialogSize
27
+ className?: string
28
+ /**
29
+ * Radix `modal` for the underlying Dialog. Defaults to `!isDesktop`: on
30
+ * desktop the dialog is non-modal so a Combobox/Select popover (portaled
31
+ * outside the dialog) can receive focus and keystrokes; on mobile it stays
32
+ * modal so the bottom-sheet behaves. Pass explicitly to override.
33
+ */
34
+ modal?: boolean
35
+ /**
36
+ * Advanced: wraps the dialog's inner content (header + body + footer) inside
37
+ * Content. Used by FormDialog to inject a <form>. Defaults to identity.
38
+ */
39
+ contentWrapper?: (content: ReactNode) => ReactNode
40
+ }
41
+
42
+ /**
43
+ * Standardized, responsive dialog shell:
44
+ * - mobile: bottom sheet (full width, up to 92vh tall)
45
+ * - desktop: centered dialog (up to 90vh tall)
46
+ * Header and footer stay fixed; only the body scrolls.
47
+ *
48
+ * Also fixes the known Radix bug where `body { pointer-events: none }`
49
+ * can persist after closing a dialog that contained a Select/Popover,
50
+ * which would otherwise make the rest of the page unclickable.
51
+ */
52
+ export function BaseDialog({
53
+ open,
54
+ onOpenChange,
55
+ title,
56
+ description,
57
+ children,
58
+ footer,
59
+ size = "lg",
60
+ className,
61
+ modal,
62
+ contentWrapper = (content) => content,
63
+ }: BaseDialogProps) {
64
+ const isDesktop = useIsDesktop()
65
+ React.useEffect(() => {
66
+ if (open) return
67
+ const id = window.setTimeout(() => {
68
+ if (document.body.style.pointerEvents === "none") {
69
+ document.body.style.pointerEvents = ""
70
+ }
71
+ }, 0)
72
+ return () => window.clearTimeout(id)
73
+ }, [open])
74
+
75
+ const body = (
76
+ <>
77
+ <div className="flex shrink-0 items-start justify-between gap-4 border-b px-6 py-3">
78
+ <div className="min-w-0 space-y-1">
79
+ <Dialog.Title className="text-lg font-semibold leading-none tracking-tight">
80
+ {title}
81
+ </Dialog.Title>
82
+ {description && (
83
+ <Dialog.Description className="text-sm text-muted-foreground">
84
+ {description}
85
+ </Dialog.Description>
86
+ )}
87
+ </div>
88
+ <Dialog.Close
89
+ className="-mr-1 shrink-0 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
90
+ aria-label="Fechar"
91
+ >
92
+ <X className="h-5 w-5" />
93
+ </Dialog.Close>
94
+ </div>
95
+ <div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">{children}</div>
96
+ {footer && (
97
+ <div className="flex shrink-0 flex-col-reverse gap-2 border-t px-6 py-3 sm:flex-row sm:justify-end">
98
+ {footer}
99
+ </div>
100
+ )}
101
+ </>
102
+ )
103
+
104
+ return (
105
+ <Dialog.Root open={open} onOpenChange={onOpenChange} modal={modal ?? !isDesktop}>
106
+ <Dialog.Portal>
107
+ <Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
108
+ <Dialog.Content
109
+ onCloseAutoFocus={() => {
110
+ document.body.style.pointerEvents = ""
111
+ }}
112
+ className={cn(
113
+ "fixed z-50 flex max-h-[92vh] flex-col bg-background shadow-lg outline-none",
114
+ "inset-x-0 bottom-0 rounded-t-2xl",
115
+ "sm:inset-x-auto sm:bottom-auto sm:left-1/2 sm:top-1/2 sm:max-h-[92vh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg",
116
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
117
+ sizeClasses[size],
118
+ className,
119
+ )}
120
+ >
121
+ {contentWrapper(body)}
122
+ </Dialog.Content>
123
+ </Dialog.Portal>
124
+ </Dialog.Root>
125
+ )
126
+ }
@@ -1,9 +1,7 @@
1
- import * as Dialog from "@radix-ui/react-dialog"
2
- import { X } from "lucide-react"
3
1
  import * as React from "react"
4
2
  import type { ReactNode } from "react"
5
3
 
6
- import { cn } from "../lib/utils"
4
+ import { BaseDialog, type BaseDialogProps, type BaseDialogSize } from "./base-dialog"
7
5
 
8
6
  interface FormDialogLayoutProps {
9
7
  title: string
@@ -52,118 +50,32 @@ export function FormDialogLayout({
52
50
  )
53
51
  }
54
52
 
55
- const sizeClasses = {
56
- sm: "sm:max-w-sm",
57
- md: "sm:max-w-md",
58
- lg: "sm:max-w-lg",
59
- xl: "sm:max-w-xl",
60
- "2xl": "sm:max-w-2xl",
61
- } as const
53
+ export type FormDialogSize = BaseDialogSize
62
54
 
63
- export type FormDialogSize = keyof typeof sizeClasses
64
-
65
- interface FormDialogProps {
66
- open: boolean
67
- onOpenChange: (open: boolean) => void
68
- title: string
69
- description?: ReactNode
70
- children: ReactNode
71
- footer?: ReactNode
55
+ export interface FormDialogProps extends Omit<BaseDialogProps, "contentWrapper"> {
72
56
  /** When provided, the dialog renders a <form> wrapping body + footer. */
73
57
  onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void
74
- size?: FormDialogSize
75
- className?: string
76
58
  }
77
59
 
78
60
  /**
79
- * Standardized, responsive modal:
80
- * - mobile: bottom sheet (full width, up to 92vh tall)
81
- * - desktop: centered dialog (up to 90vh tall)
82
- * Header and footer stay fixed; only the body scrolls.
83
- *
84
- * Also fixes the known Radix bug where `body { pointer-events: none }`
85
- * can persist after closing a dialog that contained a Select/Popover,
86
- * which would otherwise make the rest of the page unclickable.
61
+ * Form-oriented specialization of {@link BaseDialog}: when `onSubmit` is
62
+ * provided, the body and footer are wrapped in a <form> (so footer submit
63
+ * buttons and Enter trigger submission). Without `onSubmit` it behaves like a
64
+ * plain BaseDialog.
87
65
  */
88
- export function FormDialog({
89
- open,
90
- onOpenChange,
91
- title,
92
- description,
93
- children,
94
- footer,
95
- onSubmit,
96
- size = "lg",
97
- className,
98
- }: FormDialogProps) {
99
- React.useEffect(() => {
100
- if (open) return
101
- const id = window.setTimeout(() => {
102
- if (document.body.style.pointerEvents === "none") {
103
- document.body.style.pointerEvents = ""
104
- }
105
- }, 0)
106
- return () => window.clearTimeout(id)
107
- }, [open])
108
-
109
- const body = (
110
- <>
111
- <div className="flex shrink-0 items-start justify-between gap-4 border-b px-6 py-3">
112
- <div className="min-w-0 space-y-1">
113
- <Dialog.Title className="text-lg font-semibold leading-none tracking-tight">
114
- {title}
115
- </Dialog.Title>
116
- {description && (
117
- <Dialog.Description className="text-sm text-muted-foreground">
118
- {description}
119
- </Dialog.Description>
120
- )}
121
- </div>
122
- <Dialog.Close
123
- className="-mr-1 shrink-0 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
124
- aria-label="Fechar"
125
- >
126
- <X className="h-5 w-5" />
127
- </Dialog.Close>
128
- </div>
129
- <div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">{children}</div>
130
- {footer && (
131
- <div className="flex shrink-0 flex-col-reverse gap-2 border-t px-6 py-3 sm:flex-row sm:justify-end">
132
- {footer}
133
- </div>
134
- )}
135
- </>
136
- )
137
-
66
+ export function FormDialog({ onSubmit, ...rest }: FormDialogProps) {
138
67
  return (
139
- <Dialog.Root open={open} onOpenChange={onOpenChange}>
140
- <Dialog.Portal>
141
- <Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
142
- <Dialog.Content
143
- onCloseAutoFocus={() => {
144
- document.body.style.pointerEvents = ""
145
- }}
146
- className={cn(
147
- "fixed z-50 flex max-h-[92vh] flex-col bg-background shadow-lg outline-none",
148
- "inset-x-0 bottom-0 rounded-t-2xl",
149
- "sm:inset-x-auto sm:bottom-auto sm:left-1/2 sm:top-1/2 sm:max-h-[92vh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg",
150
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
151
- sizeClasses[size],
152
- className,
153
- )}
154
- >
155
- {onSubmit ? (
156
- <form
157
- onSubmit={onSubmit}
158
- className="flex min-h-0 flex-1 flex-col"
159
- >
160
- {body}
161
- </form>
162
- ) : (
163
- body
164
- )}
165
- </Dialog.Content>
166
- </Dialog.Portal>
167
- </Dialog.Root>
68
+ <BaseDialog
69
+ {...rest}
70
+ contentWrapper={
71
+ onSubmit
72
+ ? (content) => (
73
+ <form onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
74
+ {content}
75
+ </form>
76
+ )
77
+ : undefined
78
+ }
79
+ />
168
80
  )
169
81
  }
package/src/index.ts CHANGED
@@ -19,8 +19,10 @@ export { StatusBadge } from "./components/status-badge"
19
19
  export type { StatusBadgeVariant } from "./components/status-badge"
20
20
  export { ProgressBar } from "./components/progress-bar"
21
21
  export { FormField } from "./components/form-field"
22
+ export { BaseDialog } from "./components/base-dialog"
23
+ export type { BaseDialogProps, BaseDialogSize } from "./components/base-dialog"
22
24
  export { FormDialogLayout, FormDialog } from "./components/form-dialog"
23
- export type { FormDialogSize } from "./components/form-dialog"
25
+ export type { FormDialogProps, FormDialogSize } from "./components/form-dialog"
24
26
  export { AuthCard } from "./components/auth-card"
25
27
  export { Pagination } from "./components/pagination"
26
28
  export { TableSkeleton } from "./components/table-skeleton"