@motor-hero/ui-kit 0.34.1 → 0.36.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.34.1",
3
+ "version": "0.36.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,8 +1,8 @@
1
1
  import * as PopoverPrimitive from "@radix-ui/react-popover"
2
2
  import { InputMask } from "@react-input/mask"
3
- import { format, isValid, parse } from "date-fns"
3
+ import { endOfYear, format, isValid, parse, subYears } from "date-fns"
4
4
  import { ptBR } from "date-fns/locale"
5
- import { CalendarIcon, ChevronLeft, ChevronRight, Clock } from "lucide-react"
5
+ import { CalendarIcon, ChevronDown, ChevronLeft, ChevronRight, Clock } from "lucide-react"
6
6
  import * as React from "react"
7
7
  import { DayPicker, type Matcher } from "react-day-picker"
8
8
  import { Drawer as DrawerPrimitive } from "vaul"
@@ -100,7 +100,12 @@ const calendarClassNames = {
100
100
  months: "relative flex flex-col gap-4",
101
101
  month: "flex w-full flex-col gap-4",
102
102
  month_caption: "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
103
- caption_label: "text-sm font-medium select-none",
103
+ caption_label:
104
+ "flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm font-medium select-none [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
105
+ dropdowns: "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
106
+ dropdown_root:
107
+ "relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50",
108
+ dropdown: "absolute inset-0 bg-popover opacity-0",
104
109
  nav: "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
105
110
  button_previous: navButtonClass,
106
111
  button_next: navButtonClass,
@@ -118,14 +123,17 @@ const calendarClassNames = {
118
123
  }
119
124
 
120
125
  const calendarComponents = {
121
- Chevron: ({ orientation }: { orientation?: string }) =>
122
- orientation === "left" ? (
123
- <ChevronLeft className="size-4" />
124
- ) : (
125
- <ChevronRight className="size-4" />
126
- ),
126
+ Chevron: ({ orientation }: { orientation?: string }) => {
127
+ if (orientation === "left") return <ChevronLeft className="size-4" />
128
+ if (orientation === "right") return <ChevronRight className="size-4" />
129
+ return <ChevronDown className="size-4" />
130
+ },
127
131
  }
128
132
 
133
+ /** Nome curto do mês em pt-BR pro rótulo do dropdown (ex. "jul") — explícito porque
134
+ * `toLocaleString("default", ...)` seguiria o locale do navegador, não o do app. */
135
+ const formatMonthDropdown = (date: Date) => date.toLocaleString("pt-BR", { month: "short" })
136
+
129
137
  /**
130
138
  * DatePicker / DateTimePicker — calendário + campo digitável com máscara
131
139
  * (nunca o `<input type="date"/time">` nativo). Desktop: Popover não-modal;
@@ -160,6 +168,12 @@ function DatePickerImpl({
160
168
  const committedDate = withTime ? dateTimeToDate(value) : dateOnlyToDate(value)
161
169
  const displayValue = displayFor(value, withTime)
162
170
  const matchers = toMatchers(minDate, maxDate, disabledDates)
171
+ // Alcance do dropdown de ano: minDate/maxDate quando informados, senão o mesmo
172
+ // padrão da lib (100 anos atrás -> fim do ano atual) — calculado explícito nos dois
173
+ // lados porque passar só um dos dois via prop faz a lib truncar o outro (visto na
174
+ // prática: só uma opção de ano no lado sem prop), em vez de cair no padrão dela.
175
+ const startMonth = dateOnlyToDate(minDate) ?? subYears(new Date(), 100)
176
+ const endMonth = dateOnlyToDate(maxDate) ?? endOfYear(new Date())
163
177
 
164
178
  const setOpen = (next: boolean) => {
165
179
  setOpenRaw(next)
@@ -261,6 +275,10 @@ function DatePickerImpl({
261
275
  <DayPicker
262
276
  mode="single"
263
277
  locale={ptBR}
278
+ captionLayout="dropdown"
279
+ formatters={{ formatMonthDropdown }}
280
+ startMonth={startMonth}
281
+ endMonth={endMonth}
264
282
  selected={withTime ? pendingDate : committedDate}
265
283
  defaultMonth={(withTime ? pendingDate : committedDate) ?? new Date()}
266
284
  onSelect={(date) => date && handleCalendarSelect(date)}
@@ -0,0 +1,239 @@
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 className={cn("mb-6 flex flex-wrap items-center gap-2", className)}>
34
+ {children}
35
+ {isFilterActive && onClear && (
36
+ <button
37
+ type="button"
38
+ onClick={onClear}
39
+ className="inline-flex h-9 shrink-0 items-center gap-1.5 rounded-md px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
40
+ >
41
+ <X className="h-4 w-4" />
42
+ {clearLabel}
43
+ </button>
44
+ )}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ type FilterPopoverContent =
50
+ | React.ReactNode
51
+ | ((api: { close: () => void }) => React.ReactNode);
52
+
53
+ export interface FilterPopoverProps {
54
+ /** Rótulo do gatilho (nome do filtro). */
55
+ label: string;
56
+ /** Ícone opcional à esquerda (ex.: `Filter` no botão de overflow). */
57
+ icon?: LucideIcon;
58
+ /** Ativo → gatilho destacado e badge visível. */
59
+ active?: boolean;
60
+ /** Conteúdo do badge quando ativo: uma contagem ou o rótulo do valor. */
61
+ badge?: React.ReactNode;
62
+ /** Largura/estilo do conteúdo no desktop. Padrão: `w-60`. */
63
+ contentClassName?: string;
64
+ align?: "start" | "center" | "end";
65
+ /** Título do drawer no mobile (acessibilidade). Padrão: `label`. */
66
+ title?: string;
67
+ /** Nó ou render-prop que recebe `{ close }` para fechar após selecionar. */
68
+ children: FilterPopoverContent;
69
+ }
70
+
71
+ /**
72
+ * Gatilho compacto + popover (desktop) / drawer (mobile) para um filtro.
73
+ * Mesmo idiom do `Combobox`: popover não-modal no desktop, bottom sheet no touch.
74
+ */
75
+ export function FilterPopover({
76
+ label,
77
+ icon: Icon,
78
+ active = false,
79
+ badge,
80
+ contentClassName,
81
+ align = "start",
82
+ title,
83
+ children,
84
+ }: FilterPopoverProps) {
85
+ const [open, setOpen] = React.useState(false);
86
+ const isDesktop = useIsDesktop();
87
+ const close = React.useCallback(() => setOpen(false), []);
88
+ const content =
89
+ typeof children === "function" ? children({ close }) : children;
90
+
91
+ const trigger = (
92
+ <button
93
+ type="button"
94
+ aria-expanded={open}
95
+ className={cn(
96
+ "inline-flex h-9 shrink-0 cursor-pointer items-center 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",
97
+ active ? "border-ring/60 text-foreground" : "text-muted-foreground",
98
+ )}
99
+ >
100
+ {Icon && <Icon className="h-4 w-4 shrink-0 opacity-70" />}
101
+ <span className="truncate">{label}</span>
102
+ {active && badge != null && (
103
+ <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">
104
+ {badge}
105
+ </span>
106
+ )}
107
+ <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
108
+ </button>
109
+ );
110
+
111
+ if (isDesktop) {
112
+ return (
113
+ <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
114
+ <PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
115
+ <PopoverPrimitive.Portal>
116
+ <PopoverPrimitive.Content
117
+ align={align}
118
+ sideOffset={4}
119
+ onInteractOutside={(event) => {
120
+ // Mantém aberto quando o clique cai dentro de outro popover
121
+ // aberto a partir daqui (ex.: o dropdown de um Combobox),
122
+ // em vez de um clique genuíno fora.
123
+ const target = event.target as HTMLElement | null;
124
+ if (target?.closest("[data-radix-popper-content-wrapper]")) {
125
+ event.preventDefault();
126
+ }
127
+ }}
128
+ className={cn(
129
+ "pointer-events-auto z-50 w-60 overflow-hidden rounded-md border bg-popover p-0 text-popover-foreground shadow-md",
130
+ contentClassName,
131
+ )}
132
+ >
133
+ {content}
134
+ </PopoverPrimitive.Content>
135
+ </PopoverPrimitive.Portal>
136
+ </PopoverPrimitive.Root>
137
+ );
138
+ }
139
+
140
+ return (
141
+ <DrawerPrimitive.Root open={open} onOpenChange={setOpen}>
142
+ <DrawerPrimitive.Trigger asChild>{trigger}</DrawerPrimitive.Trigger>
143
+ <DrawerPrimitive.Portal>
144
+ <DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
145
+ <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">
146
+ <DrawerPrimitive.Title className="sr-only">
147
+ {title ?? label}
148
+ </DrawerPrimitive.Title>
149
+ <div className="mx-auto my-3 h-1.5 w-12 shrink-0 rounded-full bg-muted" />
150
+ <div className="overflow-y-auto p-2">{content}</div>
151
+ </DrawerPrimitive.Content>
152
+ </DrawerPrimitive.Portal>
153
+ </DrawerPrimitive.Root>
154
+ );
155
+ }
156
+
157
+ export interface FilterOption {
158
+ value: string;
159
+ label: string;
160
+ /** Classe do "dot" à esquerda (ex.: `bg-emerald-500` para um status). */
161
+ dotClassName?: string;
162
+ }
163
+
164
+ export interface FilterOptionListProps {
165
+ options: FilterOption[];
166
+ /** `string` no modo simples; `string[]` no modo múltiplo. */
167
+ value: string | string[];
168
+ onChange: (value: string | string[]) => void;
169
+ multiple?: boolean;
170
+ /** Mostra o link "Limpar" no rodapé quando há seleção. */
171
+ clearable?: boolean;
172
+ clearLabel?: string;
173
+ }
174
+
175
+ /**
176
+ * Lista de opções (seleção simples ou múltipla) para o conteúdo de um
177
+ * `FilterPopover` — evita aninhar um Combobox para enums curtos.
178
+ */
179
+ export function FilterOptionList({
180
+ options,
181
+ value,
182
+ onChange,
183
+ multiple = false,
184
+ clearable = false,
185
+ clearLabel = "Limpar",
186
+ }: FilterOptionListProps) {
187
+ const selected = Array.isArray(value) ? value : value ? [value] : [];
188
+ const hasSelection = selected.length > 0;
189
+
190
+ const toggle = (optionValue: string) => {
191
+ if (!multiple) {
192
+ onChange(optionValue);
193
+ return;
194
+ }
195
+ onChange(
196
+ selected.includes(optionValue)
197
+ ? selected.filter((v) => v !== optionValue)
198
+ : [...selected, optionValue],
199
+ );
200
+ };
201
+
202
+ return (
203
+ <div className="flex flex-col p-1">
204
+ {options.map((option) => {
205
+ const isChecked = selected.includes(option.value);
206
+ return (
207
+ <button
208
+ key={option.value}
209
+ type="button"
210
+ onClick={() => toggle(option.value)}
211
+ 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"
212
+ >
213
+ {option.dotClassName && (
214
+ <span
215
+ className={cn(
216
+ "h-2 w-2 shrink-0 rounded-full",
217
+ option.dotClassName,
218
+ )}
219
+ />
220
+ )}
221
+ <span className="flex-1 truncate">{option.label}</span>
222
+ {isChecked && (
223
+ <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-primary" />
224
+ )}
225
+ </button>
226
+ );
227
+ })}
228
+ {clearable && hasSelection && (
229
+ <button
230
+ type="button"
231
+ onClick={() => onChange(multiple ? [] : "")}
232
+ 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"
233
+ >
234
+ {clearLabel}
235
+ </button>
236
+ )}
237
+ </div>
238
+ );
239
+ }
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";