@softize/opus 30.0.0 → 31.1.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/CHANGELOG.md +123 -0
- package/bin/lib/check.mjs +109 -101
- package/bin/lib/copy.mjs +2973 -1801
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
- package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
- package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
- package/docs/adr/0066-civil-date-input-and-calendar-list.md +28 -0
- package/docs/adr/README.md +6 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +21 -19
- package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
- package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +2 -0
- package/src/ui/components/patterns/form.tsx +22 -1
- package/src/ui/components/patterns/grouped-list.ts +2 -2
- package/src/ui/components/patterns/list-calendar.tsx +129 -0
- package/src/ui/components/patterns/list.tsx +612 -382
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +227 -197
- package/src/ui/components/patterns/presentation-fields.tsx +246 -0
- package/src/ui/components/patterns/presentation-sections.tsx +494 -0
- package/src/ui/components/patterns/presentation.tsx +1142 -229
- package/src/ui/components/patterns/structural-header.tsx +23 -32
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +19 -29
- package/src/ui/components/patterns/trigger.tsx +10 -2
- package/src/ui/components/primitives/calendar.tsx +39 -18
- package/src/ui/components/primitives/close-button.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -8
- package/src/ui/components/primitives/date-input.tsx +276 -0
- package/src/ui/components/primitives/detail.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +19 -81
- package/src/ui/components/primitives/drawer.tsx +17 -79
- package/src/ui/components/primitives/field.tsx +1 -1
- package/src/ui/components/primitives/pagination.tsx +3 -2
- package/src/ui/components/primitives/table.tsx +1 -1
- package/src/ui/components/primitives/tabs.tsx +13 -2
- package/src/ui/docs/content/action-form.md +6 -1
- package/src/ui/docs/content/action-list.md +156 -68
- package/src/ui/docs/content/action-trigger.md +5 -0
- package/src/ui/docs/content/button.md +5 -6
- package/src/ui/docs/content/calendar-month-view.md +50 -0
- package/src/ui/docs/content/calendar.md +6 -1
- package/src/ui/docs/content/command.md +62 -50
- package/src/ui/docs/content/compositions.md +129 -459
- package/src/ui/docs/content/date-input.md +67 -0
- package/src/ui/docs/content/dialog.md +44 -112
- package/src/ui/docs/content/drawer.md +44 -103
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/input.md +3 -0
- package/src/ui/docs/content/page.md +204 -412
- package/src/ui/docs/content/presentation.md +371 -23
- package/src/ui/docs/content/separator.md +1 -1
- package/src/ui/docs/content/surface.md +65 -120
- package/src/ui/docs/content/tokens.md +18 -4
- package/src/ui/docs/registry.tsx +38 -9
- package/src/ui/drivers/context.tsx +11 -4
- package/src/ui/drivers/react.tsx +27 -7
- package/src/ui/meta.ts +17 -6
- package/src/ui/react.tsx +41 -8
- package/src/ui/theme.css +44 -33
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
import { ptBR } from "date-fns/locale";
|
|
8
8
|
import {
|
|
9
9
|
DayPicker,
|
|
10
|
+
DateLib,
|
|
10
11
|
getDefaultClassNames,
|
|
11
12
|
type DayButton,
|
|
13
|
+
type RootProps,
|
|
12
14
|
} from "react-day-picker";
|
|
13
15
|
|
|
14
16
|
import { cn } from "../../lib/cn.ts";
|
|
@@ -16,7 +18,9 @@ import { Button, buttonVariants } from "./button.tsx";
|
|
|
16
18
|
|
|
17
19
|
/** Rótulos acessíveis da navegação em pt-BR — o react-day-picker só traduz nomes de mês e
|
|
18
20
|
* dia pelo `locale`; os aria-labels dos controles continuariam em inglês. */
|
|
19
|
-
const LABELS_PT_BR: NonNullable<
|
|
21
|
+
const LABELS_PT_BR: NonNullable<
|
|
22
|
+
React.ComponentProps<typeof DayPicker>["labels"]
|
|
23
|
+
> = {
|
|
20
24
|
labelPrevious: () => "Mês anterior",
|
|
21
25
|
labelNext: () => "Próximo mês",
|
|
22
26
|
labelMonthDropdown: () => "Escolher o mês",
|
|
@@ -34,6 +38,17 @@ const LABELS_PT_BR: NonNullable<React.ComponentProps<typeof DayPicker>["labels"]
|
|
|
34
38
|
},
|
|
35
39
|
};
|
|
36
40
|
|
|
41
|
+
function CalendarRoot({ className, rootRef, ...props }: RootProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-slot="calendar"
|
|
45
|
+
ref={rootRef}
|
|
46
|
+
className={cn(className)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
function Calendar({
|
|
38
53
|
className,
|
|
39
54
|
classNames,
|
|
@@ -63,17 +78,27 @@ function Calendar({
|
|
|
63
78
|
)}
|
|
64
79
|
captionLayout={captionLayout}
|
|
65
80
|
formatters={{
|
|
81
|
+
formatCaption: (date, options, dateLib) => {
|
|
82
|
+
const caption = (
|
|
83
|
+
dateLib ??
|
|
84
|
+
new DateLib({
|
|
85
|
+
...options,
|
|
86
|
+
locale: { ...ptBR, ...locale, ...options?.locale },
|
|
87
|
+
})
|
|
88
|
+
).format(date, "LLLL/yyyy");
|
|
89
|
+
return (
|
|
90
|
+
caption.charAt(0).toLocaleUpperCase(locale.code) + caption.slice(1)
|
|
91
|
+
);
|
|
92
|
+
},
|
|
66
93
|
// O dateLib já carrega o `locale` em vigor: o dropdown fala a mesma língua da legenda.
|
|
67
94
|
formatMonthDropdown: (date, dateLib) =>
|
|
68
|
-
dateLib?.format(date, "LLL") ??
|
|
95
|
+
dateLib?.format(date, "LLL") ??
|
|
96
|
+
date.toLocaleString("pt-BR", { month: "short" }),
|
|
69
97
|
...formatters,
|
|
70
98
|
}}
|
|
71
99
|
classNames={{
|
|
72
100
|
root: cn("w-fit", defaultClassNames.root),
|
|
73
|
-
months: cn(
|
|
74
|
-
"relative flex flex-row gap-4",
|
|
75
|
-
defaultClassNames.months,
|
|
76
|
-
),
|
|
101
|
+
months: cn("relative flex flex-row gap-4", defaultClassNames.months),
|
|
77
102
|
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
|
78
103
|
nav: cn(
|
|
79
104
|
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
|
@@ -156,16 +181,7 @@ function Calendar({
|
|
|
156
181
|
...classNames,
|
|
157
182
|
}}
|
|
158
183
|
components={{
|
|
159
|
-
Root:
|
|
160
|
-
return (
|
|
161
|
-
<div
|
|
162
|
-
data-slot="calendar"
|
|
163
|
-
ref={rootRef}
|
|
164
|
-
className={cn(className)}
|
|
165
|
-
{...props}
|
|
166
|
-
/>
|
|
167
|
-
);
|
|
168
|
-
},
|
|
184
|
+
Root: CalendarRoot,
|
|
169
185
|
Chevron: ({ className, orientation, ...props }) => {
|
|
170
186
|
if (orientation === "left") {
|
|
171
187
|
return (
|
|
@@ -219,7 +235,12 @@ function CalendarDayButton({
|
|
|
219
235
|
return (
|
|
220
236
|
<Button
|
|
221
237
|
ref={ref}
|
|
222
|
-
|
|
238
|
+
context={
|
|
239
|
+
modifiers.selected && !modifiers.range_middle ? "primary" : "neutral"
|
|
240
|
+
}
|
|
241
|
+
variant={
|
|
242
|
+
modifiers.selected && !modifiers.range_middle ? "solid" : "ghost"
|
|
243
|
+
}
|
|
223
244
|
size="icon"
|
|
224
245
|
data-day={day.date.toLocaleDateString()}
|
|
225
246
|
data-selected-single={
|
|
@@ -232,7 +253,7 @@ function CalendarDayButton({
|
|
|
232
253
|
data-range-end={modifiers.range_end}
|
|
233
254
|
data-range-middle={modifiers.range_middle}
|
|
234
255
|
className={cn(
|
|
235
|
-
"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[0.1875rem] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-
|
|
256
|
+
"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[0.1875rem] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
|
236
257
|
defaultClassNames.day,
|
|
237
258
|
className,
|
|
238
259
|
)}
|
|
@@ -3,22 +3,30 @@ import { XIcon } from "lucide-react";
|
|
|
3
3
|
|
|
4
4
|
import { Button, type ButtonProps } from "./button.tsx";
|
|
5
5
|
|
|
6
|
-
export type CloseButtonSize = "xs" | "sm";
|
|
6
|
+
export type CloseButtonSize = "xs" | "sm" | "default";
|
|
7
7
|
|
|
8
8
|
const buttonSizeByCloseSize = {
|
|
9
|
+
default: "icon",
|
|
9
10
|
xs: "icon-xs",
|
|
10
11
|
sm: "icon-sm",
|
|
11
12
|
} as const;
|
|
12
13
|
|
|
13
14
|
export interface CloseButtonProps extends Omit<
|
|
14
15
|
ButtonProps,
|
|
15
|
-
|
|
16
|
+
| "busy"
|
|
17
|
+
| "children"
|
|
18
|
+
| "context"
|
|
19
|
+
| "icon"
|
|
20
|
+
| "shape"
|
|
21
|
+
| "size"
|
|
22
|
+
| "type"
|
|
23
|
+
| "variant"
|
|
16
24
|
> {
|
|
17
25
|
size?: CloseButtonSize;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export function CloseButton({
|
|
21
|
-
size = "
|
|
29
|
+
size = "xs",
|
|
22
30
|
disabled = false,
|
|
23
31
|
"aria-label": ariaLabel = "Fechar",
|
|
24
32
|
...props
|
|
@@ -38,9 +38,6 @@ function CommandDialog({
|
|
|
38
38
|
return (
|
|
39
39
|
<Dialog {...props}>
|
|
40
40
|
<DialogContent
|
|
41
|
-
// A paleta é uma superfície contínua: a lista já separa os grupos, e uma faixa
|
|
42
|
-
// sob o cabeçalho competiria com ela.
|
|
43
|
-
layout="continuous"
|
|
44
41
|
className={cn("overflow-hidden p-0", className)}
|
|
45
42
|
showCloseButton={showCloseButton}
|
|
46
43
|
aria-describedby={descriptionId}
|
|
@@ -50,11 +47,8 @@ function CommandDialog({
|
|
|
50
47
|
<p id={descriptionId} className="sr-only">
|
|
51
48
|
{description}
|
|
52
49
|
</p>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
superfície do SDK que diverge nos dois, e por isso está escrito aqui. */}
|
|
56
|
-
<DialogHeader className="px-3 py-2">
|
|
57
|
-
<DialogTitle className="text-sm">{title}</DialogTitle>
|
|
50
|
+
<DialogHeader>
|
|
51
|
+
<DialogTitle>{title}</DialogTitle>
|
|
58
52
|
</DialogHeader>
|
|
59
53
|
<Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
60
54
|
{children}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { CalendarDays } from "lucide-react";
|
|
3
|
+
import { Input, type InputProps } from "./input.tsx";
|
|
4
|
+
import { Calendar } from "./calendar.tsx";
|
|
5
|
+
import { Popover, PopoverAnchor, PopoverContent } from "./popover.tsx";
|
|
6
|
+
|
|
7
|
+
export interface DateInputProps extends Omit<
|
|
8
|
+
InputProps,
|
|
9
|
+
| "type"
|
|
10
|
+
| "value"
|
|
11
|
+
| "defaultValue"
|
|
12
|
+
| "onChange"
|
|
13
|
+
| "icon"
|
|
14
|
+
| "trailing"
|
|
15
|
+
| "min"
|
|
16
|
+
| "max"
|
|
17
|
+
| "step"
|
|
18
|
+
> {
|
|
19
|
+
/** Data civil ISO; durante a digitação, pode conter texto ainda inválido. */
|
|
20
|
+
value?: string;
|
|
21
|
+
defaultValue?: string;
|
|
22
|
+
onValueChange?: (value: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseDate(value: string): Date | undefined {
|
|
26
|
+
const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
27
|
+
if (!parts) return undefined;
|
|
28
|
+
const [, year, month, day] = parts.map(Number);
|
|
29
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31)
|
|
30
|
+
return undefined;
|
|
31
|
+
const date = new Date(0);
|
|
32
|
+
date.setHours(0, 0, 0, 0);
|
|
33
|
+
date.setFullYear(year, month - 1, day);
|
|
34
|
+
return date.getFullYear() === year &&
|
|
35
|
+
date.getMonth() === month - 1 &&
|
|
36
|
+
date.getDate() === day
|
|
37
|
+
? date
|
|
38
|
+
: undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function serializeDate(date: Date): string {
|
|
42
|
+
return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function valueFromText(text: string): string {
|
|
46
|
+
if (parseDate(text)) return text;
|
|
47
|
+
const parts =
|
|
48
|
+
/^(\d{2})\/(\d{2})\/(\d{4})$/.exec(text) ??
|
|
49
|
+
/^(\d{2})(\d{2})(\d{4})$/.exec(text);
|
|
50
|
+
if (!parts) return text;
|
|
51
|
+
const candidate = `${parts[3]}-${parts[2]}-${parts[1]}`;
|
|
52
|
+
return parseDate(candidate) ? candidate : text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function maskDate(text: string): string {
|
|
56
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
57
|
+
const digits = (
|
|
58
|
+
iso ? `${iso[3]}${iso[2]}${iso[1]}` : text.replace(/\D/g, "")
|
|
59
|
+
).slice(0, 8);
|
|
60
|
+
return (
|
|
61
|
+
digits.slice(0, 2) +
|
|
62
|
+
(digits.length >= 2 ? "/" : "") +
|
|
63
|
+
digits.slice(2, 4) +
|
|
64
|
+
(digits.length >= 4 ? "/" : "") +
|
|
65
|
+
digits.slice(4)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function positionAfterDigits(count: number): number {
|
|
70
|
+
return count + (count >= 2 ? 1 : 0) + (count >= 4 ? 1 : 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
|
|
74
|
+
function DateInput(
|
|
75
|
+
{
|
|
76
|
+
value,
|
|
77
|
+
defaultValue = "",
|
|
78
|
+
onValueChange,
|
|
79
|
+
disabled,
|
|
80
|
+
readOnly,
|
|
81
|
+
name,
|
|
82
|
+
form,
|
|
83
|
+
placeholder = "DD/MM/AAAA",
|
|
84
|
+
onKeyDown,
|
|
85
|
+
...props
|
|
86
|
+
},
|
|
87
|
+
ref,
|
|
88
|
+
) {
|
|
89
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
90
|
+
const calendarId = React.useId();
|
|
91
|
+
const fieldRef = React.useRef<HTMLDivElement>(null);
|
|
92
|
+
const restoreInputFocus = React.useRef(false);
|
|
93
|
+
React.useImperativeHandle(ref, () => inputRef.current!);
|
|
94
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
95
|
+
const currentValue = value ?? internalValue;
|
|
96
|
+
const selected = parseDate(currentValue);
|
|
97
|
+
const text = selected
|
|
98
|
+
? currentValue.split("-").reverse().join("/")
|
|
99
|
+
: currentValue;
|
|
100
|
+
const [open, setOpen] = React.useState(false);
|
|
101
|
+
const openRef = React.useRef(open);
|
|
102
|
+
openRef.current = open;
|
|
103
|
+
const [focusCalendar, setFocusCalendar] = React.useState(false);
|
|
104
|
+
const [calendarSession, setCalendarSession] = React.useState(0);
|
|
105
|
+
const [caret, setCaret] = React.useState<{ position: number }>();
|
|
106
|
+
const [month, setMonth] = React.useState(() => selected ?? new Date());
|
|
107
|
+
|
|
108
|
+
React.useLayoutEffect(() => {
|
|
109
|
+
const input = inputRef.current;
|
|
110
|
+
if (caret && input && document.activeElement === input) {
|
|
111
|
+
input.setSelectionRange(caret.position, caret.position);
|
|
112
|
+
}
|
|
113
|
+
}, [caret]);
|
|
114
|
+
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
const ownerForm = inputRef.current?.form;
|
|
117
|
+
if (value !== undefined || !ownerForm) return;
|
|
118
|
+
const reset = () => setInternalValue(defaultValue);
|
|
119
|
+
ownerForm.addEventListener("reset", reset);
|
|
120
|
+
return () => ownerForm.removeEventListener("reset", reset);
|
|
121
|
+
}, [value, defaultValue, form]);
|
|
122
|
+
|
|
123
|
+
const change = (next: string) => {
|
|
124
|
+
if (value === undefined) setInternalValue(next);
|
|
125
|
+
onValueChange?.(next);
|
|
126
|
+
};
|
|
127
|
+
const changeOpen = (next: boolean) => {
|
|
128
|
+
if (
|
|
129
|
+
next &&
|
|
130
|
+
(disabled || readOnly || inputRef.current?.matches(":disabled"))
|
|
131
|
+
)
|
|
132
|
+
return;
|
|
133
|
+
if (next) {
|
|
134
|
+
setMonth(selected ?? new Date());
|
|
135
|
+
if (!open) setCalendarSession((session) => session + 1);
|
|
136
|
+
}
|
|
137
|
+
setOpen(next);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Popover open={open && !disabled && !readOnly} onOpenChange={changeOpen}>
|
|
142
|
+
{name && (
|
|
143
|
+
<input
|
|
144
|
+
type="hidden"
|
|
145
|
+
name={name}
|
|
146
|
+
form={form}
|
|
147
|
+
value={currentValue}
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
<PopoverAnchor asChild>
|
|
152
|
+
<div
|
|
153
|
+
ref={fieldRef}
|
|
154
|
+
onClick={(event) => {
|
|
155
|
+
if (event.defaultPrevented) return;
|
|
156
|
+
if (
|
|
157
|
+
disabled ||
|
|
158
|
+
readOnly ||
|
|
159
|
+
inputRef.current?.matches(":disabled")
|
|
160
|
+
)
|
|
161
|
+
return;
|
|
162
|
+
inputRef.current?.focus();
|
|
163
|
+
setFocusCalendar(false);
|
|
164
|
+
changeOpen(true);
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<Input
|
|
168
|
+
{...props}
|
|
169
|
+
ref={inputRef}
|
|
170
|
+
form={form}
|
|
171
|
+
type="text"
|
|
172
|
+
inputMode="numeric"
|
|
173
|
+
value={text}
|
|
174
|
+
placeholder={placeholder}
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
readOnly={readOnly}
|
|
177
|
+
role="combobox"
|
|
178
|
+
aria-expanded={open}
|
|
179
|
+
aria-haspopup="dialog"
|
|
180
|
+
aria-controls={open ? calendarId : undefined}
|
|
181
|
+
aria-keyshortcuts="Alt+ArrowDown"
|
|
182
|
+
onChange={(event) => {
|
|
183
|
+
const input = event.target;
|
|
184
|
+
let raw = input.value;
|
|
185
|
+
let cursor = input.selectionStart ?? raw.length;
|
|
186
|
+
const inputType = (event.nativeEvent as InputEvent).inputType;
|
|
187
|
+
// A barra pertence à máscara: apagar junto dela também remove o dígito adjacente,
|
|
188
|
+
// inclusive em teclados virtuais que não emitem keydown de Backspace ou Delete.
|
|
189
|
+
if (
|
|
190
|
+
raw.length === text.length - 1 &&
|
|
191
|
+
raw.replace(/\D/g, "") === text.replace(/\D/g, "")
|
|
192
|
+
) {
|
|
193
|
+
if (inputType === "deleteContentBackward" && cursor > 0) {
|
|
194
|
+
raw = raw.slice(0, cursor - 1) + raw.slice(cursor);
|
|
195
|
+
cursor -= 1;
|
|
196
|
+
} else if (inputType === "deleteContentForward") {
|
|
197
|
+
raw = raw.slice(0, cursor) + raw.slice(cursor + 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const masked = maskDate(raw);
|
|
201
|
+
const digitCount = raw
|
|
202
|
+
.slice(0, cursor)
|
|
203
|
+
.replace(/\D/g, "").length;
|
|
204
|
+
setCaret({
|
|
205
|
+
position: Math.min(
|
|
206
|
+
masked.length,
|
|
207
|
+
positionAfterDigits(digitCount),
|
|
208
|
+
),
|
|
209
|
+
});
|
|
210
|
+
change(valueFromText(masked));
|
|
211
|
+
}}
|
|
212
|
+
onKeyDown={(event) => {
|
|
213
|
+
onKeyDown?.(event);
|
|
214
|
+
if (event.defaultPrevented) return;
|
|
215
|
+
if (event.altKey && event.key === "ArrowDown") {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
setFocusCalendar(true);
|
|
218
|
+
changeOpen(true);
|
|
219
|
+
}
|
|
220
|
+
}}
|
|
221
|
+
trailing={<CalendarDays aria-hidden="true" className="size-4" />}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
</PopoverAnchor>
|
|
225
|
+
<PopoverContent
|
|
226
|
+
id={calendarId}
|
|
227
|
+
className="w-auto p-0"
|
|
228
|
+
align="start"
|
|
229
|
+
aria-label="Escolher data"
|
|
230
|
+
onOpenAutoFocus={(event) => {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
if (focusCalendar) {
|
|
233
|
+
(event.target as HTMLElement)
|
|
234
|
+
.querySelector<HTMLButtonElement>('[data-day][tabindex="0"]')
|
|
235
|
+
?.focus();
|
|
236
|
+
}
|
|
237
|
+
}}
|
|
238
|
+
onInteractOutside={(event) => {
|
|
239
|
+
if (fieldRef.current?.contains(event.target as Node))
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
}}
|
|
242
|
+
onEscapeKeyDown={() => {
|
|
243
|
+
restoreInputFocus.current = true;
|
|
244
|
+
}}
|
|
245
|
+
onCloseAutoFocus={(event) => {
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
// O fechamento anterior pode terminar depois de uma reabertura rápida.
|
|
248
|
+
if (restoreInputFocus.current && !openRef.current) {
|
|
249
|
+
inputRef.current?.focus();
|
|
250
|
+
}
|
|
251
|
+
restoreInputFocus.current = false;
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
<Calendar
|
|
255
|
+
// DayPicker aplica autoFocus apenas na montagem, inclusive em reaberturas durante a animação.
|
|
256
|
+
key={`${calendarSession}-${focusCalendar}`}
|
|
257
|
+
mode="single"
|
|
258
|
+
required
|
|
259
|
+
autoFocus={focusCalendar}
|
|
260
|
+
startMonth={parseDate("0001-01-01")}
|
|
261
|
+
endMonth={parseDate("9999-12-01")}
|
|
262
|
+
month={month}
|
|
263
|
+
onMonthChange={setMonth}
|
|
264
|
+
selected={selected}
|
|
265
|
+
onSelect={(date) => {
|
|
266
|
+
if (inputRef.current?.matches(":disabled")) return;
|
|
267
|
+
change(serializeDate(date));
|
|
268
|
+
restoreInputFocus.current = true;
|
|
269
|
+
setOpen(false);
|
|
270
|
+
}}
|
|
271
|
+
/>
|
|
272
|
+
</PopoverContent>
|
|
273
|
+
</Popover>
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
);
|
|
@@ -122,7 +122,7 @@ function DetailField({
|
|
|
122
122
|
!dividedHorizontalField &&
|
|
123
123
|
"group-data-[orientation=horizontal]/detail-group:grid-cols-[minmax(0,var(--detail-label-width))_minmax(0,1fr)] group-data-[orientation=horizontal]/detail-group:items-baseline group-data-[orientation=horizontal]/detail-group:gap-x-3",
|
|
124
124
|
dividedHorizontalField && "items-stretch gap-x-0",
|
|
125
|
-
"group-data-[orientation=vertical]/detail-group:gap-y-1",
|
|
125
|
+
"group-data-[orientation=vertical]/detail-group:gap-y-1.5",
|
|
126
126
|
"group-data-[dividers=true]/detail-group:outline group-data-[dividers=true]/detail-group:outline-1 group-data-[dividers=true]/detail-group:outline-border",
|
|
127
127
|
"group-data-[variant=framed]/detail-group:bg-transparent",
|
|
128
128
|
className,
|