@motor-hero/ui-kit 0.35.0 → 0.36.1

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.35.0",
3
+ "version": "0.36.1",
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,244 @@
1
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
2
+ import { ChevronDown, type LucideIcon, X } from "lucide-react";
3
+ import * as React from "react";
4
+ import { Drawer as DrawerPrimitive } from "vaul";
5
+ import { useIsDesktop } from "../hooks/use-is-desktop";
6
+ import { cn } from "../lib/utils";
7
+
8
+ /**
9
+ * Barra de filtros compacta para telas com muitos filtros.
10
+ *
11
+ * Mantém poucos controles visíveis (uma busca + 1–2 filtros primários) e recolhe
12
+ * o restante atrás de um botão "Filtros" com contador, no lugar de uma fileira
13
+ * poluída de dropdowns. Composicional e router-agnóstico: o app fornece os
14
+ * controles reais (Combobox, listas) dentro de cada `FilterPopover`.
15
+ */
16
+ export interface FilterToolbarProps {
17
+ children: React.ReactNode;
18
+ /** Mostra o botão "Limpar filtros" quando `true`. */
19
+ isFilterActive?: boolean;
20
+ onClear?: () => void;
21
+ clearLabel?: string;
22
+ className?: string;
23
+ }
24
+
25
+ export function FilterToolbar({
26
+ children,
27
+ isFilterActive = false,
28
+ onClear,
29
+ clearLabel = "Limpar filtros",
30
+ className,
31
+ }: FilterToolbarProps) {
32
+ return (
33
+ <div
34
+ className={cn(
35
+ "mb-6 flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center",
36
+ className,
37
+ )}
38
+ >
39
+ {children}
40
+ {isFilterActive && onClear && (
41
+ <button
42
+ type="button"
43
+ onClick={onClear}
44
+ className="inline-flex h-9 shrink-0 items-center justify-center gap-1.5 self-start rounded-md px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground sm:self-auto"
45
+ >
46
+ <X className="h-4 w-4" />
47
+ {clearLabel}
48
+ </button>
49
+ )}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ type FilterPopoverContent =
55
+ | React.ReactNode
56
+ | ((api: { close: () => void }) => React.ReactNode);
57
+
58
+ export interface FilterPopoverProps {
59
+ /** Rótulo do gatilho (nome do filtro). */
60
+ label: string;
61
+ /** Ícone opcional à esquerda (ex.: `Filter` no botão de overflow). */
62
+ icon?: LucideIcon;
63
+ /** Ativo → gatilho destacado e badge visível. */
64
+ active?: boolean;
65
+ /** Conteúdo do badge quando ativo: uma contagem ou o rótulo do valor. */
66
+ badge?: React.ReactNode;
67
+ /** Largura/estilo do conteúdo no desktop. Padrão: `w-60`. */
68
+ contentClassName?: string;
69
+ align?: "start" | "center" | "end";
70
+ /** Título do drawer no mobile (acessibilidade). Padrão: `label`. */
71
+ title?: string;
72
+ /** Nó ou render-prop que recebe `{ close }` para fechar após selecionar. */
73
+ children: FilterPopoverContent;
74
+ }
75
+
76
+ /**
77
+ * Gatilho compacto + popover (desktop) / drawer (mobile) para um filtro.
78
+ * Mesmo idiom do `Combobox`: popover não-modal no desktop, bottom sheet no touch.
79
+ */
80
+ export function FilterPopover({
81
+ label,
82
+ icon: Icon,
83
+ active = false,
84
+ badge,
85
+ contentClassName,
86
+ align = "start",
87
+ title,
88
+ children,
89
+ }: FilterPopoverProps) {
90
+ const [open, setOpen] = React.useState(false);
91
+ const isDesktop = useIsDesktop();
92
+ const close = React.useCallback(() => setOpen(false), []);
93
+ const content =
94
+ typeof children === "function" ? children({ close }) : children;
95
+
96
+ const trigger = (
97
+ <button
98
+ type="button"
99
+ aria-expanded={open}
100
+ className={cn(
101
+ "flex h-9 w-full shrink-0 cursor-pointer items-center justify-between gap-1.5 rounded-md border border-input bg-transparent px-3 text-sm shadow-sm transition-colors hover:border-ring/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:w-auto sm:justify-start",
102
+ active ? "border-ring/60 text-foreground" : "text-muted-foreground",
103
+ )}
104
+ >
105
+ {Icon && <Icon className="h-4 w-4 shrink-0 opacity-70" />}
106
+ <span className="truncate">{label}</span>
107
+ {active && badge != null && (
108
+ <span className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium leading-none text-primary-foreground">
109
+ {badge}
110
+ </span>
111
+ )}
112
+ <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
113
+ </button>
114
+ );
115
+
116
+ if (isDesktop) {
117
+ return (
118
+ <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
119
+ <PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
120
+ <PopoverPrimitive.Portal>
121
+ <PopoverPrimitive.Content
122
+ align={align}
123
+ sideOffset={4}
124
+ onInteractOutside={(event) => {
125
+ // Mantém aberto quando o clique cai dentro de outro popover
126
+ // aberto a partir daqui (ex.: o dropdown de um Combobox),
127
+ // em vez de um clique genuíno fora.
128
+ const target = event.target as HTMLElement | null;
129
+ if (target?.closest("[data-radix-popper-content-wrapper]")) {
130
+ event.preventDefault();
131
+ }
132
+ }}
133
+ className={cn(
134
+ "pointer-events-auto z-50 w-60 overflow-hidden rounded-md border bg-popover p-0 text-popover-foreground shadow-md",
135
+ contentClassName,
136
+ )}
137
+ >
138
+ {content}
139
+ </PopoverPrimitive.Content>
140
+ </PopoverPrimitive.Portal>
141
+ </PopoverPrimitive.Root>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <DrawerPrimitive.Root open={open} onOpenChange={setOpen}>
147
+ <DrawerPrimitive.Trigger asChild>{trigger}</DrawerPrimitive.Trigger>
148
+ <DrawerPrimitive.Portal>
149
+ <DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
150
+ <DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85vh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none">
151
+ <DrawerPrimitive.Title className="sr-only">
152
+ {title ?? label}
153
+ </DrawerPrimitive.Title>
154
+ <div className="mx-auto my-3 h-1.5 w-12 shrink-0 rounded-full bg-muted" />
155
+ <div className="overflow-y-auto p-2">{content}</div>
156
+ </DrawerPrimitive.Content>
157
+ </DrawerPrimitive.Portal>
158
+ </DrawerPrimitive.Root>
159
+ );
160
+ }
161
+
162
+ export interface FilterOption {
163
+ value: string;
164
+ label: string;
165
+ /** Classe do "dot" à esquerda (ex.: `bg-emerald-500` para um status). */
166
+ dotClassName?: string;
167
+ }
168
+
169
+ export interface FilterOptionListProps {
170
+ options: FilterOption[];
171
+ /** `string` no modo simples; `string[]` no modo múltiplo. */
172
+ value: string | string[];
173
+ onChange: (value: string | string[]) => void;
174
+ multiple?: boolean;
175
+ /** Mostra o link "Limpar" no rodapé quando há seleção. */
176
+ clearable?: boolean;
177
+ clearLabel?: string;
178
+ }
179
+
180
+ /**
181
+ * Lista de opções (seleção simples ou múltipla) para o conteúdo de um
182
+ * `FilterPopover` — evita aninhar um Combobox para enums curtos.
183
+ */
184
+ export function FilterOptionList({
185
+ options,
186
+ value,
187
+ onChange,
188
+ multiple = false,
189
+ clearable = false,
190
+ clearLabel = "Limpar",
191
+ }: FilterOptionListProps) {
192
+ const selected = Array.isArray(value) ? value : value ? [value] : [];
193
+ const hasSelection = selected.length > 0;
194
+
195
+ const toggle = (optionValue: string) => {
196
+ if (!multiple) {
197
+ onChange(optionValue);
198
+ return;
199
+ }
200
+ onChange(
201
+ selected.includes(optionValue)
202
+ ? selected.filter((v) => v !== optionValue)
203
+ : [...selected, optionValue],
204
+ );
205
+ };
206
+
207
+ return (
208
+ <div className="flex flex-col p-1">
209
+ {options.map((option) => {
210
+ const isChecked = selected.includes(option.value);
211
+ return (
212
+ <button
213
+ key={option.value}
214
+ type="button"
215
+ onClick={() => toggle(option.value)}
216
+ className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground"
217
+ >
218
+ {option.dotClassName && (
219
+ <span
220
+ className={cn(
221
+ "h-2 w-2 shrink-0 rounded-full",
222
+ option.dotClassName,
223
+ )}
224
+ />
225
+ )}
226
+ <span className="flex-1 truncate">{option.label}</span>
227
+ {isChecked && (
228
+ <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-primary" />
229
+ )}
230
+ </button>
231
+ );
232
+ })}
233
+ {clearable && hasSelection && (
234
+ <button
235
+ type="button"
236
+ onClick={() => onChange(multiple ? [] : "")}
237
+ className="mt-1 border-t px-2 pt-1.5 pb-0.5 text-center text-xs text-muted-foreground transition-colors hover:text-foreground"
238
+ >
239
+ {clearLabel}
240
+ </button>
241
+ )}
242
+ </div>
243
+ );
244
+ }
package/src/index.ts CHANGED
@@ -1,65 +1,87 @@
1
1
  // Theme
2
- export { ThemeProvider, useTheme } from "./components/theme-provider"
3
- export type { Theme, ThemeProviderProps, ThemeProviderState } from "./components/theme-provider"
4
- export { applyCompanyTheme } from "./lib/company-theme"
5
- export type { CompanyTheme, CompanyThemeTokens } from "./lib/company-theme"
6
- export { useCompanySubdomain, getSubdomain } from "./hooks/use-company-subdomain"
7
- export type {
8
- UseCompanySubdomainOptions,
9
- UseCompanySubdomainResult,
10
- } from "./hooks/use-company-subdomain"
11
-
12
- // Components
13
- export { ModeToggle } from "./components/mode-toggle"
14
- export { EmptyState } from "./components/empty-state"
15
- export { ConfirmDialog } from "./components/confirm-dialog"
16
- export { PageHeader } from "./components/page-header"
17
- export { StatusDot } from "./components/status-dot"
18
- export { StatusBadge } from "./components/status-badge"
19
- export type { StatusBadgeVariant } from "./components/status-badge"
20
- export { ProgressBar } from "./components/progress-bar"
21
- export { FormField } from "./components/form-field"
22
- export { BaseDialog } from "./components/base-dialog"
23
- export type { BaseDialogProps, BaseDialogSize } from "./components/base-dialog"
24
- export { FormDialogLayout, FormDialog } from "./components/form-dialog"
25
- export type { FormDialogProps, FormDialogSize } from "./components/form-dialog"
26
- export { AuthCard } from "./components/auth-card"
27
- export { Pagination } from "./components/pagination"
28
- export type { PaginationProps } from "./components/pagination"
29
- export { TableSkeleton } from "./components/table-skeleton"
30
- export { SearchInput } from "./components/search-input"
31
- export { PasswordInput } from "./components/password-input"
32
- export { Combobox } from "./components/combobox"
33
- export type { ComboboxOption, ComboboxProps } from "./components/combobox"
34
- export { MultiCombobox } from "./components/multi-combobox"
35
- export type { MultiComboboxProps } from "./components/multi-combobox"
36
- export { ColorPicker } from "./components/color-picker"
37
- export type { ColorPickerProps } from "./components/color-picker"
38
- export { DatePicker, DateTimePicker } from "./components/date-picker"
39
- export type { DatePickerProps } from "./components/date-picker"
40
- export { StatCard } from "./components/stat-card"
41
- export { DataTableWrapper } from "./components/data-table-wrapper"
42
- export { MobileCardList } from "./components/mobile-card-list"
43
- export { ResponsiveDataView } from "./components/responsive-data-view"
44
- export { Toaster } from "./components/toaster"
45
2
 
3
+ export type { AppShellProps } from "./components/app-shell";
46
4
  // App shell
47
- export { AppShell } from "./components/app-shell"
48
- export type { AppShellProps } from "./components/app-shell"
49
- export { SidebarNav } from "./components/sidebar-nav"
50
- export type { NavItem, SidebarNavProps } from "./components/sidebar-nav"
51
- export { UserMenu } from "./components/user-menu"
52
- export type { AppUser, UserMenuItem, UserMenuProps } from "./components/user-menu"
53
- export { RowActionsMenu } from "./components/row-actions-menu"
54
- export type { RowAction, RowActionsMenuProps } from "./components/row-actions-menu"
55
- export { defaultRenderLink } from "./components/types"
56
- export type { RenderLink, RenderLinkProps } from "./components/types"
57
-
58
- // Utilities
59
- export { cn } from "./lib/utils"
60
- export { extractApiError } from "./lib/api-error"
61
-
5
+ export { AppShell } from "./components/app-shell";
6
+ export { AuthCard } from "./components/auth-card";
7
+ export type { BaseDialogProps, BaseDialogSize } from "./components/base-dialog";
8
+ export { BaseDialog } from "./components/base-dialog";
9
+ export type { ColorPickerProps } from "./components/color-picker";
10
+ export { ColorPicker } from "./components/color-picker";
11
+ export type { ComboboxOption, ComboboxProps } from "./components/combobox";
12
+ export { Combobox } from "./components/combobox";
13
+ export { ConfirmDialog } from "./components/confirm-dialog";
14
+ export { DataTableWrapper } from "./components/data-table-wrapper";
15
+ export type { DatePickerProps } from "./components/date-picker";
16
+ export { DatePicker, DateTimePicker } from "./components/date-picker";
17
+ export { EmptyState } from "./components/empty-state";
18
+ export type {
19
+ FilterOption,
20
+ FilterOptionListProps,
21
+ FilterPopoverProps,
22
+ FilterToolbarProps,
23
+ } from "./components/filter-toolbar";
24
+ export {
25
+ FilterOptionList,
26
+ FilterPopover,
27
+ FilterToolbar,
28
+ } from "./components/filter-toolbar";
29
+ export type { FormDialogProps, FormDialogSize } from "./components/form-dialog";
30
+ export { FormDialog, FormDialogLayout } from "./components/form-dialog";
31
+ export { FormField } from "./components/form-field";
32
+ export { MobileCardList } from "./components/mobile-card-list";
33
+ // Components
34
+ export { ModeToggle } from "./components/mode-toggle";
35
+ export type { MultiComboboxProps } from "./components/multi-combobox";
36
+ export { MultiCombobox } from "./components/multi-combobox";
37
+ export { PageHeader } from "./components/page-header";
38
+ export type { PaginationProps } from "./components/pagination";
39
+ export { Pagination } from "./components/pagination";
40
+ export { PasswordInput } from "./components/password-input";
41
+ export { ProgressBar } from "./components/progress-bar";
42
+ export { ResponsiveDataView } from "./components/responsive-data-view";
43
+ export type {
44
+ RowAction,
45
+ RowActionsMenuProps,
46
+ } from "./components/row-actions-menu";
47
+ export { RowActionsMenu } from "./components/row-actions-menu";
48
+ export { SearchInput } from "./components/search-input";
49
+ export type { NavItem, SidebarNavProps } from "./components/sidebar-nav";
50
+ export { SidebarNav } from "./components/sidebar-nav";
51
+ export { StatCard } from "./components/stat-card";
52
+ export type { StatusBadgeVariant } from "./components/status-badge";
53
+ export { StatusBadge } from "./components/status-badge";
54
+ export { StatusDot } from "./components/status-dot";
55
+ export { TableSkeleton } from "./components/table-skeleton";
56
+ export type {
57
+ Theme,
58
+ ThemeProviderProps,
59
+ ThemeProviderState,
60
+ } from "./components/theme-provider";
61
+ export { ThemeProvider, useTheme } from "./components/theme-provider";
62
+ export { Toaster } from "./components/toaster";
63
+ export type { RenderLink, RenderLinkProps } from "./components/types";
64
+ export { defaultRenderLink } from "./components/types";
65
+ export type {
66
+ AppUser,
67
+ UserMenuItem,
68
+ UserMenuProps,
69
+ } from "./components/user-menu";
70
+ export { UserMenu } from "./components/user-menu";
71
+ export type {
72
+ UseCompanySubdomainOptions,
73
+ UseCompanySubdomainResult,
74
+ } from "./hooks/use-company-subdomain";
75
+ export {
76
+ getSubdomain,
77
+ useCompanySubdomain,
78
+ } from "./hooks/use-company-subdomain";
62
79
  // Hooks
63
- export { useDisclosure } from "./hooks/use-disclosure"
64
- export { useIsDesktop } from "./hooks/use-is-desktop"
65
- export { useCustomToast, toast } from "./hooks/use-toast"
80
+ export { useDisclosure } from "./hooks/use-disclosure";
81
+ export { useIsDesktop } from "./hooks/use-is-desktop";
82
+ export { toast, useCustomToast } from "./hooks/use-toast";
83
+ export { extractApiError } from "./lib/api-error";
84
+ export type { CompanyTheme, CompanyThemeTokens } from "./lib/company-theme";
85
+ export { applyCompanyTheme } from "./lib/company-theme";
86
+ // Utilities
87
+ export { cn } from "./lib/utils";