@strategicos/ui 0.7.0 → 0.9.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/README.md +51 -2
- package/THIRD_PARTY_NOTICES.md +187 -0
- package/dist/components/calendar.d.ts +24 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +81 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/combobox.d.ts +73 -0
- package/dist/components/combobox.d.ts.map +1 -0
- package/dist/components/combobox.js +193 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/date-input.d.ts +46 -0
- package/dist/components/date-input.d.ts.map +1 -0
- package/dist/components/date-input.js +202 -0
- package/dist/components/date-input.js.map +1 -0
- package/dist/components/date-picker-input.d.ts +49 -0
- package/dist/components/date-picker-input.d.ts.map +1 -0
- package/dist/components/date-picker-input.js +126 -0
- package/dist/components/date-picker-input.js.map +1 -0
- package/dist/components/date-time-picker.d.ts +27 -0
- package/dist/components/date-time-picker.d.ts.map +1 -0
- package/dist/components/date-time-picker.js +77 -0
- package/dist/components/date-time-picker.js.map +1 -0
- package/dist/components/dates.d.ts +94 -0
- package/dist/components/dates.d.ts.map +1 -0
- package/dist/components/dates.js +94 -0
- package/dist/components/dates.js.map +1 -0
- package/dist/components/multi-select.d.ts.map +1 -1
- package/dist/components/multi-select.js +2 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/select.d.ts +16 -1
- package/dist/components/select.d.ts.map +1 -1
- package/dist/components/select.js +24 -2
- package/dist/components/select.js.map +1 -1
- package/dist/components/time-input.d.ts +16 -0
- package/dist/components/time-input.d.ts.map +1 -0
- package/dist/components/time-input.js +18 -0
- package/dist/components/time-input.js.map +1 -0
- package/package.json +2 -1
- package/src/components/calendar.tsx +139 -0
- package/src/components/combobox.tsx +390 -0
- package/src/components/date-input.tsx +287 -0
- package/src/components/date-picker-input.tsx +287 -0
- package/src/components/date-time-picker.tsx +152 -0
- package/src/components/dates.tsx +274 -0
- package/src/components/multi-select.tsx +3 -1
- package/src/components/select.tsx +29 -1
- package/src/components/time-input.tsx +34 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Select as SelectPrimitive } from "radix-ui";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
4
5
|
import { cn } from "../lib/utils.js";
|
|
5
6
|
import { useFieldControl } from "./field.js";
|
|
6
7
|
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
|
|
@@ -13,8 +14,29 @@ function SelectGroup({ className, ...props }) {
|
|
|
13
14
|
function SelectValue({ ...props }) {
|
|
14
15
|
return _jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Visual do gatilho de seleção: borda, foco, aberto, desabilitado, erro e
|
|
19
|
+
* tamanhos. É o mesmo do SelectTrigger, exportado para os gatilhos que não são
|
|
20
|
+
* o Select do Radix (MultiSelect, Combobox, DatePicker ou um Popover próprio)
|
|
21
|
+
* não precisarem copiar as classes — cópia que fica para trás quando o kit
|
|
22
|
+
* muda.
|
|
23
|
+
*
|
|
24
|
+
* Placeholder: marque o gatilho com `data-placeholder` enquanto estiver vazio,
|
|
25
|
+
* como o Radix faz.
|
|
26
|
+
*
|
|
27
|
+
* <button className={cn(selectTriggerVariants({ size }), "w-full")} …>
|
|
28
|
+
*/
|
|
29
|
+
const selectTriggerVariants = cva("flex items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent pr-2.5 pl-3 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring data-[state=open]:border-ring disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60 aria-invalid:border-destructive data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
|
|
30
|
+
variants: {
|
|
31
|
+
size: {
|
|
32
|
+
default: "h-9",
|
|
33
|
+
sm: "h-7.5 rounded-[min(var(--radius-md),10px)]",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: { size: "default" },
|
|
37
|
+
});
|
|
16
38
|
function SelectTrigger({ className, size = "default", children, ...props }) {
|
|
17
|
-
return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn("
|
|
39
|
+
return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn(selectTriggerVariants({ size }), "w-fit py-2 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5", className), ...useFieldControl(props, { withRequired: false }), children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDownIcon, { className: "pointer-events-none size-4 opacity-70" }) })] }));
|
|
18
40
|
}
|
|
19
41
|
function SelectContent({ className, children, position = "popper", align = "start", ...props }) {
|
|
20
42
|
return (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { "data-slot": "select-content", "data-align-trigger": position === "item-aligned", className: cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, align: align, ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx(SelectPrimitive.Viewport, { "data-position": position, className: cn("p-1 data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)", position === "popper" && ""), children: children }), _jsx(SelectScrollDownButton, {})] }) }));
|
|
@@ -34,5 +56,5 @@ function SelectScrollUpButton({ className, ...props }) {
|
|
|
34
56
|
function SelectScrollDownButton({ className, ...props }) {
|
|
35
57
|
return (_jsx(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: cn("z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: _jsx(ChevronDownIcon, {}) }));
|
|
36
58
|
}
|
|
37
|
-
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
|
|
59
|
+
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, selectTriggerVariants, };
|
|
38
60
|
//# sourceMappingURL=select.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.js","sourceRoot":"","sources":["../../src/components/select.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAGZ,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"select.js","sourceRoot":"","sources":["../../src/components/select.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type InputProps } from "./input.js";
|
|
3
|
+
/**
|
|
4
|
+
* Hora, como o TimeInput do Mantine: o <input type="time"> do navegador, no
|
|
5
|
+
* visual do Input. O valor é "HH:mm" (ou "HH:mm:ss" com `withSeconds`).
|
|
6
|
+
*
|
|
7
|
+
* <Field label="Horário"><TimeInput value={hora} onChange={e => setHora(e.target.value)} /></Field>
|
|
8
|
+
*
|
|
9
|
+
* O formato de 12 ou 24 horas segue o sistema do usuário — no Brasil, 24 horas.
|
|
10
|
+
* O relógio à direita é o do navegador (abre a lista de horas no Chrome).
|
|
11
|
+
*/
|
|
12
|
+
declare function TimeInput({ withSeconds, className, ...props }: Omit<InputProps, "type"> & {
|
|
13
|
+
withSeconds?: boolean;
|
|
14
|
+
}): React.JSX.Element;
|
|
15
|
+
export { TimeInput };
|
|
16
|
+
//# sourceMappingURL=time-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-input.d.ts","sourceRoot":"","sources":["../../src/components/time-input.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAEnD;;;;;;;;GAQG;AACH,iBAAS,SAAS,CAAC,EACjB,WAAmB,EACnB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,GAAG;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,qBASA;AAED,OAAO,EAAE,SAAS,EAAE,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
import { Input } from "./input.js";
|
|
5
|
+
/**
|
|
6
|
+
* Hora, como o TimeInput do Mantine: o <input type="time"> do navegador, no
|
|
7
|
+
* visual do Input. O valor é "HH:mm" (ou "HH:mm:ss" com `withSeconds`).
|
|
8
|
+
*
|
|
9
|
+
* <Field label="Horário"><TimeInput value={hora} onChange={e => setHora(e.target.value)} /></Field>
|
|
10
|
+
*
|
|
11
|
+
* O formato de 12 ou 24 horas segue o sistema do usuário — no Brasil, 24 horas.
|
|
12
|
+
* O relógio à direita é o do navegador (abre a lista de horas no Chrome).
|
|
13
|
+
*/
|
|
14
|
+
function TimeInput({ withSeconds = false, className, ...props }) {
|
|
15
|
+
return (_jsx(Input, { type: "time", step: withSeconds ? 1 : 60, className: cn("tabular-nums", className), ...props }));
|
|
16
|
+
}
|
|
17
|
+
export { TimeInput };
|
|
18
|
+
//# sourceMappingURL=time-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-input.js","sourceRoot":"","sources":["../../src/components/time-input.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAIZ,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AACpC,OAAO,EAAE,KAAK,EAAmB,MAAM,YAAY,CAAA;AAEnD;;;;;;;;GAQG;AACH,SAAS,SAAS,CAAC,EACjB,WAAW,GAAG,KAAK,EACnB,SAAS,EACT,GAAG,KAAK,EAGT;IACC,OAAO,CACL,KAAC,KAAK,IACJ,IAAI,EAAC,MAAM,EACX,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAC1B,SAAS,EAAE,EAAE,CAAC,cAAc,EAAE,SAAS,CAAC,KACpC,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,OAAO,EAAE,SAAS,EAAE,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strategicos/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Componentes React da Strategicos (shadcn + Radix + Tailwind v4).",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"clsx": "^2.1.1",
|
|
44
44
|
"lucide-react": "^1.12.0",
|
|
45
45
|
"radix-ui": "^1.4.3",
|
|
46
|
+
"react-day-picker": "^10.0.2",
|
|
46
47
|
"tailwind-merge": "^3.5.0",
|
|
47
48
|
"tw-animate-css": "^1.4.0"
|
|
48
49
|
},
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
// Derivado do Calendar do Tremor (Apache-2.0) e modificado pela Strategicos.
|
|
4
|
+
// Ver THIRD_PARTY_NOTICES.md.
|
|
5
|
+
|
|
6
|
+
import * as React from "react"
|
|
7
|
+
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
|
|
8
|
+
import { DayPicker, type DayButtonProps } from "react-day-picker"
|
|
9
|
+
import { ptBR } from "react-day-picker/locale"
|
|
10
|
+
|
|
11
|
+
import { cn } from "../lib/utils.js"
|
|
12
|
+
import { buttonVariants } from "./button.js"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calendário (react-day-picker), com o desenho do Calendar do Tremor nos tokens
|
|
16
|
+
* do kit. É a peça de dentro do DatePicker; use solto quando o calendário fica
|
|
17
|
+
* aberto na tela.
|
|
18
|
+
*
|
|
19
|
+
* <Calendar mode="single" selected={dia} onSelect={setDia} />
|
|
20
|
+
*
|
|
21
|
+
* Padrões: português do Brasil (semana começando no domingo) e dias de fora do
|
|
22
|
+
* mês visíveis. `captionLayout="dropdown"` troca o título por seletores de mês
|
|
23
|
+
* e ano — útil para datas distantes, como nascimento. Aceita todas as props do
|
|
24
|
+
* DayPicker.
|
|
25
|
+
*/
|
|
26
|
+
function Calendar({
|
|
27
|
+
className,
|
|
28
|
+
classNames,
|
|
29
|
+
captionLayout = "label",
|
|
30
|
+
showOutsideDays = true,
|
|
31
|
+
locale = ptBR,
|
|
32
|
+
components,
|
|
33
|
+
...props
|
|
34
|
+
}: React.ComponentProps<typeof DayPicker>) {
|
|
35
|
+
return (
|
|
36
|
+
<DayPicker
|
|
37
|
+
data-slot="calendar"
|
|
38
|
+
locale={locale}
|
|
39
|
+
showOutsideDays={showOutsideDays}
|
|
40
|
+
captionLayout={captionLayout}
|
|
41
|
+
className={cn("w-fit p-3", className)}
|
|
42
|
+
classNames={{
|
|
43
|
+
months: "relative flex flex-col gap-4 sm:flex-row",
|
|
44
|
+
month: "flex w-full flex-col gap-3",
|
|
45
|
+
nav: "absolute inset-x-0 top-0 flex items-center justify-between",
|
|
46
|
+
button_previous: cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), "aria-disabled:pointer-events-none aria-disabled:opacity-40"),
|
|
47
|
+
button_next: cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), "aria-disabled:pointer-events-none aria-disabled:opacity-40"),
|
|
48
|
+
month_caption: "flex h-7.5 items-center justify-center px-8",
|
|
49
|
+
// Os meses vêm em minúscula no pt-BR ("outubro 2026"): só a primeira
|
|
50
|
+
// letra sobe — `capitalize` faria "Outubro De 2026" em outros formatos.
|
|
51
|
+
caption_label: cn(
|
|
52
|
+
"text-sm font-semibold select-none first-letter:uppercase",
|
|
53
|
+
captionLayout !== "label" && "flex h-7.5 items-center gap-1 rounded-md pr-1 pl-2 font-medium [&>svg]:size-3.5 [&>svg]:text-muted-foreground"
|
|
54
|
+
),
|
|
55
|
+
dropdowns: "flex items-center gap-1.5 text-sm",
|
|
56
|
+
dropdown_root: "relative rounded-md border border-input transition-colors has-focus-visible:border-ring",
|
|
57
|
+
dropdown: "absolute inset-0 cursor-pointer opacity-0",
|
|
58
|
+
month_grid: "w-full border-collapse",
|
|
59
|
+
weekdays: "flex",
|
|
60
|
+
weekday: "w-9 pb-1 text-xs font-medium text-muted-foreground select-none",
|
|
61
|
+
week: "mt-0.5 flex w-full",
|
|
62
|
+
day: "relative size-9 p-0 text-center select-none",
|
|
63
|
+
// Faixa do intervalo: o fundo fica na célula, para os dias do meio
|
|
64
|
+
// emendarem; as pontas arredondam do lado de fora.
|
|
65
|
+
range_start: "rounded-l-md bg-accent",
|
|
66
|
+
range_middle: "bg-accent",
|
|
67
|
+
range_end: "rounded-r-md bg-accent",
|
|
68
|
+
today: "",
|
|
69
|
+
outside: "text-muted-foreground aria-selected:text-muted-foreground",
|
|
70
|
+
disabled: "text-muted-foreground opacity-50",
|
|
71
|
+
hidden: "invisible",
|
|
72
|
+
...classNames,
|
|
73
|
+
}}
|
|
74
|
+
components={{
|
|
75
|
+
Chevron: ({ className, orientation, ...rest }) => {
|
|
76
|
+
const Icon = orientation === "left" ? ChevronLeftIcon : orientation === "right" ? ChevronRightIcon : ChevronDownIcon
|
|
77
|
+
return <Icon className={cn("size-4", className)} {...rest} />
|
|
78
|
+
},
|
|
79
|
+
DayButton: CalendarDayButton,
|
|
80
|
+
...components,
|
|
81
|
+
}}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Dia do calendário. Os estados viram data-* para o estilo, e "hoje" ganha o
|
|
88
|
+
* traço embaixo do número, como no Tremor.
|
|
89
|
+
*
|
|
90
|
+
* O dia escolhido é `data-picked`, e não `data-selected-*`: o styles.css
|
|
91
|
+
* registra a variante `data-selected`, e o Tailwind lê `data-selected-x:` como
|
|
92
|
+
* essa variante com um valor — a classe é descartada sem aviso. */
|
|
93
|
+
function CalendarDayButton({ className, day, modifiers, children, ...props }: DayButtonProps) {
|
|
94
|
+
const ref = React.useRef<HTMLButtonElement>(null)
|
|
95
|
+
React.useEffect(() => {
|
|
96
|
+
if (modifiers.focused) ref.current?.focus()
|
|
97
|
+
}, [modifiers.focused])
|
|
98
|
+
|
|
99
|
+
const single = modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<button
|
|
103
|
+
ref={ref}
|
|
104
|
+
type="button"
|
|
105
|
+
data-picked={single || undefined}
|
|
106
|
+
data-range-start={modifiers.range_start || undefined}
|
|
107
|
+
data-range-end={modifiers.range_end || undefined}
|
|
108
|
+
data-range-middle={modifiers.range_middle || undefined}
|
|
109
|
+
data-today={modifiers.today || undefined}
|
|
110
|
+
className={cn(
|
|
111
|
+
"relative flex size-9 cursor-pointer items-center justify-center rounded-md text-sm tabular-nums outline-none transition-colors",
|
|
112
|
+
"hover:bg-accent hover:text-accent-foreground focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-ring/50",
|
|
113
|
+
"disabled:pointer-events-none",
|
|
114
|
+
"data-today:font-semibold",
|
|
115
|
+
// Dourado só como preenchimento, com o texto escuro do primary-foreground.
|
|
116
|
+
"data-picked:bg-primary data-picked:text-primary-foreground data-picked:hover:bg-primary-hover",
|
|
117
|
+
"data-range-start:bg-primary data-range-start:text-primary-foreground data-range-start:hover:bg-primary-hover",
|
|
118
|
+
"data-range-end:bg-primary data-range-end:text-primary-foreground data-range-end:hover:bg-primary-hover",
|
|
119
|
+
"data-range-middle:rounded-none data-range-middle:hover:bg-transparent data-range-middle:hover:underline",
|
|
120
|
+
className
|
|
121
|
+
)}
|
|
122
|
+
{...props}
|
|
123
|
+
>
|
|
124
|
+
{children}
|
|
125
|
+
{modifiers.today && (
|
|
126
|
+
<span
|
|
127
|
+
aria-hidden
|
|
128
|
+
className={cn(
|
|
129
|
+
"absolute bottom-1.5 left-1/2 h-0.5 w-4 -translate-x-1/2 rounded-full",
|
|
130
|
+
// primary-strong: o dourado claro não chega a 3:1 sobre o branco.
|
|
131
|
+
single || modifiers.range_start || modifiers.range_end ? "bg-primary-foreground" : "bg-primary-strong"
|
|
132
|
+
)}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
</button>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { Calendar, CalendarDayButton }
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { CheckIcon, ChevronDownIcon, Loader2Icon, PlusIcon, SearchIcon, XIcon } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils.js"
|
|
7
|
+
import { Button } from "./button.js"
|
|
8
|
+
import { useFieldControl } from "./field.js"
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js"
|
|
10
|
+
import { selectTriggerVariants } from "./select.js"
|
|
11
|
+
|
|
12
|
+
export type ComboboxOption = {
|
|
13
|
+
value: string
|
|
14
|
+
label: string
|
|
15
|
+
/** Texto secundário (ex.: e-mail), só na lista. */
|
|
16
|
+
description?: string
|
|
17
|
+
/** Agrupa as opções sob um título. */
|
|
18
|
+
group?: string
|
|
19
|
+
/** Bolinha de cor antes do rótulo. */
|
|
20
|
+
color?: string
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Entry = { kind: "option"; option: ComboboxOption } | { kind: "create" }
|
|
25
|
+
|
|
26
|
+
function errorMessage(err: unknown, fallback: string) {
|
|
27
|
+
return err instanceof Error && err.message ? err.message : fallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Seleção de um item com busca, como o Select `searchable` do Mantine. Use no
|
|
32
|
+
* lugar do Select quando a lista é longa ou quando a tela precisa criar ou
|
|
33
|
+
* excluir opções ali mesmo.
|
|
34
|
+
*
|
|
35
|
+
* <Combobox
|
|
36
|
+
* options={setores.map(s => ({ value: s.id, label: s.name }))}
|
|
37
|
+
* value={setorId}
|
|
38
|
+
* onValueChange={setSetorId}
|
|
39
|
+
* clearable
|
|
40
|
+
* onCreate={async nome => (await criarSetor(nome)).id}
|
|
41
|
+
* />
|
|
42
|
+
*
|
|
43
|
+
* - `clearable`: um X no gatilho volta o valor para `null`.
|
|
44
|
+
* - `onCreate`: com texto na busca que não bate com nenhuma opção, aparece
|
|
45
|
+
* "Criar “texto”". Se a função devolver um valor, ele fica selecionado.
|
|
46
|
+
* - `onRemove`: cada opção ganha um X que pede confirmação na própria linha.
|
|
47
|
+
* Se a opção excluída era a selecionada, o valor volta para `null`.
|
|
48
|
+
*
|
|
49
|
+
* Erros de `onCreate`/`onRemove` (promessa rejeitada) aparecem no rodapé da
|
|
50
|
+
* lista, com a mensagem do erro.
|
|
51
|
+
*
|
|
52
|
+
* Teclado: digite para filtrar, ↑/↓ para andar, Enter para escolher, Delete
|
|
53
|
+
* (com a busca vazia) para excluir a opção ativa, Esc para fechar. Dentro de
|
|
54
|
+
* um <Field>, o gatilho recebe o id e os aria-*.
|
|
55
|
+
*/
|
|
56
|
+
function Combobox({
|
|
57
|
+
options,
|
|
58
|
+
value,
|
|
59
|
+
onValueChange,
|
|
60
|
+
placeholder = "Selecione",
|
|
61
|
+
searchPlaceholder = "Buscar…",
|
|
62
|
+
emptyText = "Nada encontrado.",
|
|
63
|
+
searchable = true,
|
|
64
|
+
clearable = false,
|
|
65
|
+
onCreate,
|
|
66
|
+
createLabel = q => <>Criar “{q}”</>,
|
|
67
|
+
onRemove,
|
|
68
|
+
removeLabel = o => `Excluir ${o.label}`,
|
|
69
|
+
removeConfirmLabel = o => <>Excluir “{o.label}”?</>,
|
|
70
|
+
size = "default",
|
|
71
|
+
disabled = false,
|
|
72
|
+
className,
|
|
73
|
+
wrapperClassName,
|
|
74
|
+
contentClassName,
|
|
75
|
+
onOpenChange,
|
|
76
|
+
...props
|
|
77
|
+
}: {
|
|
78
|
+
options: ComboboxOption[]
|
|
79
|
+
value: string | null
|
|
80
|
+
onValueChange: (value: string | null) => void
|
|
81
|
+
placeholder?: string
|
|
82
|
+
searchPlaceholder?: string
|
|
83
|
+
emptyText?: string
|
|
84
|
+
searchable?: boolean
|
|
85
|
+
clearable?: boolean
|
|
86
|
+
/** Cria uma opção com o texto da busca. Devolva o valor dela para
|
|
87
|
+
* selecioná-la. */
|
|
88
|
+
onCreate?: (label: string) => string | void | Promise<string | void>
|
|
89
|
+
createLabel?: (query: string) => React.ReactNode
|
|
90
|
+
/** Exclui a opção (o X na linha, depois de confirmar). */
|
|
91
|
+
onRemove?: (option: ComboboxOption) => void | Promise<void>
|
|
92
|
+
/** Rótulo acessível do X de cada opção. */
|
|
93
|
+
removeLabel?: (option: ComboboxOption) => string
|
|
94
|
+
/** Pergunta mostrada na linha ao confirmar a exclusão. */
|
|
95
|
+
removeConfirmLabel?: (option: ComboboxOption) => React.ReactNode
|
|
96
|
+
size?: "sm" | "default"
|
|
97
|
+
disabled?: boolean
|
|
98
|
+
/** Classes do gatilho. */
|
|
99
|
+
className?: string
|
|
100
|
+
/** Classes do invólucro, que só existe com `clearable`. Use para largura e
|
|
101
|
+
* posição no layout. */
|
|
102
|
+
wrapperClassName?: string
|
|
103
|
+
contentClassName?: string
|
|
104
|
+
onOpenChange?: (open: boolean) => void
|
|
105
|
+
id?: string
|
|
106
|
+
"aria-label"?: string
|
|
107
|
+
"aria-describedby"?: string
|
|
108
|
+
"aria-invalid"?: React.AriaAttributes["aria-invalid"]
|
|
109
|
+
}) {
|
|
110
|
+
const controlProps = useFieldControl(props, { withRequired: false })
|
|
111
|
+
const [open, setOpen] = React.useState(false)
|
|
112
|
+
const [query, setQuery] = React.useState("")
|
|
113
|
+
const [active, setActive] = React.useState(0)
|
|
114
|
+
const [creating, setCreating] = React.useState(false)
|
|
115
|
+
const [confirming, setConfirming] = React.useState<string | null>(null)
|
|
116
|
+
const [removing, setRemoving] = React.useState<string | null>(null)
|
|
117
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
118
|
+
const listId = React.useId()
|
|
119
|
+
const searchRef = React.useRef<HTMLInputElement>(null)
|
|
120
|
+
|
|
121
|
+
const selected = options.find(o => o.value === value)
|
|
122
|
+
const q = query.trim().toLowerCase()
|
|
123
|
+
const filtered = q ? options.filter(o => o.label.toLowerCase().includes(q)) : options
|
|
124
|
+
const canCreate = !!onCreate && !!q && !options.some(o => o.label.trim().toLowerCase() === q)
|
|
125
|
+
|
|
126
|
+
// Ordem do teclado: as opções habilitadas e, por último, o "Criar".
|
|
127
|
+
const entries: Entry[] = filtered.filter(o => !o.disabled).map(option => ({ kind: "option" as const, option }))
|
|
128
|
+
if (canCreate) entries.push({ kind: "create" })
|
|
129
|
+
const activeIndex = Math.min(active, entries.length - 1)
|
|
130
|
+
const entryId = (i: number) => `${listId}-${i}`
|
|
131
|
+
|
|
132
|
+
// Grupos na ordem em que aparecem nas opções.
|
|
133
|
+
const groups: { name: string | undefined; items: ComboboxOption[] }[] = []
|
|
134
|
+
for (const o of filtered) {
|
|
135
|
+
const g = groups.find(x => x.name === o.group)
|
|
136
|
+
if (g) g.items.push(o)
|
|
137
|
+
else groups.push({ name: o.group, items: [o] })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Mantém a opção ativa à vista ao andar com as setas.
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
if (!open || activeIndex < 0) return
|
|
143
|
+
document.getElementById(entryId(activeIndex))?.scrollIntoView({ block: "nearest" })
|
|
144
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
+
}, [open, activeIndex])
|
|
146
|
+
|
|
147
|
+
function handleOpenChange(o: boolean) {
|
|
148
|
+
setOpen(o)
|
|
149
|
+
if (!o) {
|
|
150
|
+
setQuery("")
|
|
151
|
+
setActive(0)
|
|
152
|
+
setConfirming(null)
|
|
153
|
+
setError(null)
|
|
154
|
+
} else {
|
|
155
|
+
// Abre com a opção selecionada ativa.
|
|
156
|
+
const i = entries.findIndex(e => e.kind === "option" && e.option.value === value)
|
|
157
|
+
setActive(Math.max(i, 0))
|
|
158
|
+
}
|
|
159
|
+
onOpenChange?.(o)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function choose(v: string) {
|
|
163
|
+
onValueChange(v)
|
|
164
|
+
handleOpenChange(false)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function create() {
|
|
168
|
+
if (!onCreate || creating) return
|
|
169
|
+
setCreating(true)
|
|
170
|
+
setError(null)
|
|
171
|
+
try {
|
|
172
|
+
const created = await onCreate(query.trim())
|
|
173
|
+
if (typeof created === "string") onValueChange(created)
|
|
174
|
+
handleOpenChange(false)
|
|
175
|
+
} catch (err) {
|
|
176
|
+
setError(errorMessage(err, "Não foi possível criar."))
|
|
177
|
+
} finally {
|
|
178
|
+
setCreating(false)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function remove(option: ComboboxOption) {
|
|
183
|
+
if (!onRemove) return
|
|
184
|
+
setRemoving(option.value)
|
|
185
|
+
setError(null)
|
|
186
|
+
try {
|
|
187
|
+
await onRemove(option)
|
|
188
|
+
if (option.value === value) onValueChange(null)
|
|
189
|
+
} catch (err) {
|
|
190
|
+
setError(errorMessage(err, "Não foi possível excluir."))
|
|
191
|
+
} finally {
|
|
192
|
+
setRemoving(null)
|
|
193
|
+
setConfirming(null)
|
|
194
|
+
searchRef.current?.focus()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function cancelConfirm() {
|
|
199
|
+
setConfirming(null)
|
|
200
|
+
searchRef.current?.focus()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function activate(entry: Entry | undefined) {
|
|
204
|
+
if (!entry) return
|
|
205
|
+
if (entry.kind === "create") void create()
|
|
206
|
+
else choose(entry.option.value)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function onKeyDown(e: React.KeyboardEvent) {
|
|
210
|
+
// Na confirmação, o teclado é dos botões Excluir/Cancelar.
|
|
211
|
+
if (confirming) return
|
|
212
|
+
if (e.key === "ArrowDown") { e.preventDefault(); setActive(Math.min(activeIndex + 1, entries.length - 1)) }
|
|
213
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); setActive(Math.max(activeIndex - 1, 0)) }
|
|
214
|
+
else if (e.key === "Enter") { e.preventDefault(); activate(entries[activeIndex]) }
|
|
215
|
+
else if (e.key === "Delete" && onRemove && !query) {
|
|
216
|
+
const entry = entries[activeIndex]
|
|
217
|
+
if (entry?.kind === "option") { e.preventDefault(); setConfirming(entry.option.value) }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const showClear = clearable && !!selected && !disabled
|
|
222
|
+
|
|
223
|
+
const trigger = (
|
|
224
|
+
<PopoverTrigger asChild disabled={disabled}>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
role="combobox"
|
|
228
|
+
aria-expanded={open}
|
|
229
|
+
aria-controls={listId}
|
|
230
|
+
aria-haspopup="listbox"
|
|
231
|
+
data-slot="combobox-trigger"
|
|
232
|
+
data-size={size}
|
|
233
|
+
data-placeholder={selected ? undefined : ""}
|
|
234
|
+
{...controlProps}
|
|
235
|
+
className={cn(
|
|
236
|
+
selectTriggerVariants({ size }),
|
|
237
|
+
"w-full min-w-0 cursor-pointer text-left",
|
|
238
|
+
showClear && "pr-9",
|
|
239
|
+
className
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
243
|
+
{selected?.color && <span className="size-2 shrink-0 rounded-full" style={{ background: selected.color }} />}
|
|
244
|
+
<span className="truncate">{selected?.label ?? placeholder}</span>
|
|
245
|
+
</span>
|
|
246
|
+
{!showClear && <ChevronDownIcon className="size-4 shrink-0 opacity-70" />}
|
|
247
|
+
</button>
|
|
248
|
+
</PopoverTrigger>
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
253
|
+
{clearable ? (
|
|
254
|
+
<div data-slot="combobox-wrapper" className={cn("relative w-full min-w-0", wrapperClassName)}>
|
|
255
|
+
{trigger}
|
|
256
|
+
{showClear && (
|
|
257
|
+
// Irmão do gatilho, não filho: botão dentro de botão não é HTML válido.
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
onClick={() => onValueChange(null)}
|
|
261
|
+
aria-label="Limpar"
|
|
262
|
+
className="absolute inset-y-0 right-1.5 my-auto flex size-6 cursor-pointer items-center justify-center rounded text-muted-foreground outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
263
|
+
>
|
|
264
|
+
<XIcon className="size-4" />
|
|
265
|
+
</button>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
) : trigger}
|
|
269
|
+
|
|
270
|
+
<PopoverContent
|
|
271
|
+
className={cn("w-(--radix-popover-trigger-width) min-w-60 p-0", contentClassName)}
|
|
272
|
+
onKeyDown={onKeyDown}
|
|
273
|
+
onEscapeKeyDown={e => { if (confirming) { e.preventDefault(); cancelConfirm() } }}
|
|
274
|
+
>
|
|
275
|
+
{searchable && (
|
|
276
|
+
<div className="flex items-center gap-2 border-b border-border px-3">
|
|
277
|
+
<SearchIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
278
|
+
<input
|
|
279
|
+
ref={searchRef}
|
|
280
|
+
autoFocus
|
|
281
|
+
value={query}
|
|
282
|
+
onChange={e => { setQuery(e.target.value); setActive(0); setError(null) }}
|
|
283
|
+
placeholder={searchPlaceholder}
|
|
284
|
+
aria-label={searchPlaceholder}
|
|
285
|
+
aria-controls={listId}
|
|
286
|
+
aria-activedescendant={activeIndex >= 0 && !confirming ? entryId(activeIndex) : undefined}
|
|
287
|
+
aria-autocomplete="list"
|
|
288
|
+
className="h-9 w-full bg-transparent text-base outline-none placeholder:text-muted-foreground md:text-sm"
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
<div id={listId} role="listbox" className="max-h-64 overflow-y-auto overscroll-contain p-1">
|
|
294
|
+
{filtered.length === 0 && !canCreate && (
|
|
295
|
+
<p className="px-2.5 py-6 text-center text-sm text-muted-foreground">{emptyText}</p>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{groups.map(g => (
|
|
299
|
+
<div key={g.name ?? "_"} role="group" aria-label={g.name}>
|
|
300
|
+
{g.name && <p className="px-2.5 pt-2 pb-1 text-xs font-medium text-muted-foreground">{g.name}</p>}
|
|
301
|
+
{g.items.map(o => {
|
|
302
|
+
const index = entries.findIndex(e => e.kind === "option" && e.option.value === o.value)
|
|
303
|
+
const isSelected = o.value === value
|
|
304
|
+
const isActive = index === activeIndex
|
|
305
|
+
|
|
306
|
+
if (confirming === o.value) return (
|
|
307
|
+
<div key={o.value} role="alert" className="flex items-center gap-1.5 rounded-md bg-destructive/10 py-1 pr-1 pl-2.5 text-sm">
|
|
308
|
+
<span className="min-w-0 flex-1 truncate">{removeConfirmLabel(o)}</span>
|
|
309
|
+
<Button
|
|
310
|
+
type="button"
|
|
311
|
+
size="sm"
|
|
312
|
+
variant="destructive"
|
|
313
|
+
autoFocus
|
|
314
|
+
disabled={removing === o.value}
|
|
315
|
+
onClick={() => remove(o)}
|
|
316
|
+
>
|
|
317
|
+
{removing === o.value ? <Loader2Icon className="animate-spin" /> : "Excluir"}
|
|
318
|
+
</Button>
|
|
319
|
+
<Button type="button" size="sm" variant="ghost" disabled={removing === o.value} onClick={cancelConfirm}>
|
|
320
|
+
Cancelar
|
|
321
|
+
</Button>
|
|
322
|
+
</div>
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div
|
|
327
|
+
key={o.value}
|
|
328
|
+
id={index >= 0 ? entryId(index) : undefined}
|
|
329
|
+
role="option"
|
|
330
|
+
aria-selected={isSelected}
|
|
331
|
+
aria-disabled={o.disabled || undefined}
|
|
332
|
+
data-active={isActive || undefined}
|
|
333
|
+
onMouseEnter={() => { if (index >= 0) setActive(index) }}
|
|
334
|
+
onClick={() => { if (!o.disabled) choose(o.value) }}
|
|
335
|
+
className="group/option flex cursor-pointer items-center gap-2 rounded-md py-1.5 pr-2 pl-2.5 text-sm select-none data-active:bg-accent data-active:text-accent-foreground aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
|
|
336
|
+
>
|
|
337
|
+
{o.color && <span className="size-2 shrink-0 rounded-full" style={{ background: o.color }} />}
|
|
338
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
339
|
+
<span className="truncate">{o.label}</span>
|
|
340
|
+
{o.description && <span className="truncate text-[11px] text-muted-foreground">{o.description}</span>}
|
|
341
|
+
</span>
|
|
342
|
+
{isSelected && <CheckIcon className="size-4 shrink-0" />}
|
|
343
|
+
{onRemove && !o.disabled && (
|
|
344
|
+
// Fora da ordem do Tab (a lista anda pelas setas); no
|
|
345
|
+
// teclado, a exclusão é o Delete na opção ativa.
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
tabIndex={-1}
|
|
349
|
+
aria-label={removeLabel(o)}
|
|
350
|
+
title={removeLabel(o)}
|
|
351
|
+
onClick={e => { e.stopPropagation(); setConfirming(o.value) }}
|
|
352
|
+
className="-m-1 flex size-6 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground opacity-60 outline-none hover:text-destructive hover:opacity-100 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/option:opacity-60 [@media(hover:hover)]:group-data-[active]/option:opacity-60"
|
|
353
|
+
>
|
|
354
|
+
<XIcon className="size-3.5" />
|
|
355
|
+
</button>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
})}
|
|
360
|
+
</div>
|
|
361
|
+
))}
|
|
362
|
+
|
|
363
|
+
{canCreate && (() => {
|
|
364
|
+
const index = entries.length - 1
|
|
365
|
+
return (
|
|
366
|
+
<div
|
|
367
|
+
id={entryId(index)}
|
|
368
|
+
role="option"
|
|
369
|
+
aria-selected={false}
|
|
370
|
+
data-active={index === activeIndex || undefined}
|
|
371
|
+
onMouseEnter={() => setActive(index)}
|
|
372
|
+
onClick={() => void create()}
|
|
373
|
+
className="flex cursor-pointer items-center gap-2 rounded-md py-1.5 pr-2 pl-2.5 text-sm font-medium text-primary-strong select-none data-active:bg-primary/10"
|
|
374
|
+
>
|
|
375
|
+
{creating ? <Loader2Icon className="size-4 shrink-0 animate-spin" /> : <PlusIcon className="size-4 shrink-0" />}
|
|
376
|
+
<span className="truncate">{createLabel(query.trim())}</span>
|
|
377
|
+
</div>
|
|
378
|
+
)
|
|
379
|
+
})()}
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{error && (
|
|
383
|
+
<p role="alert" className="border-t border-border px-3 py-2 text-xs text-destructive">{error}</p>
|
|
384
|
+
)}
|
|
385
|
+
</PopoverContent>
|
|
386
|
+
</Popover>
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export { Combobox }
|