@softize/opus 31.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 +7 -0
- package/bin/lib/copy.mjs +3 -0
- package/docs/adr/0066-civil-date-input-and-calendar-list.md +28 -0
- package/docs/adr/README.md +2 -0
- package/package.json +1 -1
- package/src/core/types.ts +1 -0
- package/src/ui/components/patterns/form.tsx +20 -1
- package/src/ui/components/patterns/list-calendar.tsx +129 -0
- package/src/ui/components/patterns/list.tsx +46 -30
- package/src/ui/components/primitives/calendar.tsx +39 -18
- package/src/ui/components/primitives/date-input.tsx +276 -0
- package/src/ui/docs/content/action-form.md +5 -1
- package/src/ui/docs/content/action-list.md +22 -0
- 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/date-input.md +67 -0
- package/src/ui/docs/content/input.md +3 -0
- package/src/ui/docs/registry.tsx +38 -9
- package/src/ui/meta.ts +12 -1
- package/src/ui/react.tsx +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 31.1.0 — 2026-10-22
|
|
11
|
+
|
|
12
|
+
- `DateInput` reúne digitação com máscara brasileira e seleção no calendário. Valores válidos usam datas civis `AAAA-MM-DD`, sem conversão de fuso. `ActionForm` reconhece `widget: 'date'`; entradas incompletas continuam visíveis para validação pelo contrato.
|
|
13
|
+
- `ActionList` e `ActionFilterBar` aceitam `periodControl`, respeitando a posição e a composição dos filtros. Com esse controle, presets do contrato e do estado da lista deixam de sobrescrever o intervalo fornecido pelo consumidor. `CalendarMonthView` oferece uma grade mensal para `ActionList.views`, com agrupamento por data e navegação por teclado; consulta, erro e carregamento permanecem na lista.
|
|
14
|
+
- `Calendar` apresenta mês e ano como `Setembro/2026` por padrão, preservando formatters customizados. A raiz mantém sua identidade entre renderizações para conservar o foco e o primeiro clique de navegação.
|
|
15
|
+
- Dias selecionados e extremidades de intervalos usam o par semântico do botão primário sólido, corrigindo contraste no hover em tema escuro. O comportamento também se aplica aos calendários dos filtros existentes.
|
|
16
|
+
|
|
10
17
|
## 31.0.0 — 2026-10-20
|
|
11
18
|
|
|
12
19
|
- `PageBack` aceita `onBack` e `disabled` para retorno controlado por uma pilha. `href` continua disponível para destinos navegáveis; `PageBackProps` é uma união que impede combinar as duas formas.
|
package/bin/lib/copy.mjs
CHANGED
|
@@ -278,6 +278,7 @@ const JSX_PROP_ROLES = new Map([
|
|
|
278
278
|
]),
|
|
279
279
|
],
|
|
280
280
|
["Input", new Map([["placeholder", "placeholder"]])],
|
|
281
|
+
["DateInput", new Map([["placeholder", "placeholder"]])],
|
|
281
282
|
["InputGroupInput", new Map([["placeholder", "placeholder"]])],
|
|
282
283
|
["InputGroupTextarea", new Map([["placeholder", "placeholder"]])],
|
|
283
284
|
// A ajuda vive num tooltip (portal): não herda transformação nem classe do label.
|
|
@@ -397,6 +398,7 @@ const JSX_PROP_CLASS = new Map([
|
|
|
397
398
|
],
|
|
398
399
|
["CommandInput", new Map([["placeholder", "className"]])],
|
|
399
400
|
["Input", new Map([["placeholder", "className"]])],
|
|
401
|
+
["DateInput", new Map([["placeholder", "className"]])],
|
|
400
402
|
["InputGroupInput", new Map([["placeholder", "className"]])],
|
|
401
403
|
["InputGroupTextarea", new Map([["placeholder", "className"]])],
|
|
402
404
|
[
|
|
@@ -431,6 +433,7 @@ const JSX_PROP_STYLE = new Map([
|
|
|
431
433
|
],
|
|
432
434
|
["CommandInput", new Map([["placeholder", "style"]])],
|
|
433
435
|
["Input", new Map([["placeholder", "style"]])],
|
|
436
|
+
["DateInput", new Map([["placeholder", "style"]])],
|
|
434
437
|
["InputGroupInput", new Map([["placeholder", "style"]])],
|
|
435
438
|
["InputGroupTextarea", new Map([["placeholder", "style"]])],
|
|
436
439
|
[
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ADR 0066 — Datas civis e calendário mensal na lista
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita para consolidação; publicação é uma etapa separada.
|
|
4
|
+
- **Data:** 2026-10-22.
|
|
5
|
+
|
|
6
|
+
## Contexto
|
|
7
|
+
|
|
8
|
+
A gestão de expediente precisa escolher datas sem conversão de fuso e consultar um mês inteiro junto dos filtros de unidade e equipe. O input nativo varia entre navegadores, e uma grade implementada em cada consumidor repetiria navegação e acessibilidade. A experimentação no consumidor Grand Brasil validou a composição antes da consolidação desta API.
|
|
9
|
+
|
|
10
|
+
## Decisão
|
|
11
|
+
|
|
12
|
+
`DateInput` compõe Input, Popover e Calendar. Aceita digitação com máscara `DD/MM/AAAA`, colagem ISO e seleção por calendário; datas válidas são emitidas como `AAAA-MM-DD`. Entradas incompletas ou impossíveis permanecem visíveis e são emitidas sem convertê-las numa data válida, para o contrato poder rejeitá-las. O campo recebe foco; o ícone é um adorno. Clique abre o picker sem impedir digitação, Alt+Seta para baixo transfere o foco, e Escape fecha somente o popover. O renderer de `ActionForm` reconhece `widget: 'date'`, preservando controle, reset, validação e acessibilidade do formulário.
|
|
13
|
+
|
|
14
|
+
`ActionList` e `ActionFilterBar` recebem `periodControl?: ReactNode`. A extensão substitui o seletor padrão no mesmo espaço, respeitando `periodPosition` e a composição com `period.basisFilter`. O consumidor controla o valor temporal e seu estado navegável. Com periodControl, ActionList ignora o preset padrão e os valores period/from/to do estado; somente o input fornecido pelo consumidor define o intervalo. O helper listStateToInput aceita applyPeriod: false para reproduzir esse recorte em detalhes roteados. Sem a propriedade, a API anterior mantém seu comportamento.
|
|
15
|
+
|
|
16
|
+
`CalendarMonthView<TItem>` é uma visualização, consumida por `ActionList.views`. Recebe mês civil, itens, extração de data, conteúdo, nome acessível e ação por dia. Gera todas as datas do mês e agrupa os itens. A navegação usa setas, Home e End, com uma única parada de Tab. Consulta, filtros, loading, erro e vazio continuam pertencendo à lista. Regras de expediente e feriados ficam no domínio consumidor. A grade é separada do DayPicker por representar uma coleção com conteúdo e ação próprios em cada dia, sem modelo de seleção de datas. Adaptar os botões e o estado de seleção do DayPicker duplicaria essa semântica; a exceção preserva teclado e roles de grid sem copiar a primitiva. A primeira versão usa cabeçalhos em pt-BR e semanas iniciadas no domingo, explicitados na documentação.
|
|
17
|
+
|
|
18
|
+
O Calendar compartilhado apresenta o mês com inicial maiúscula e barra antes do ano, preservando os formatters customizados. A raiz estável evita perder foco ao renderizar novamente. Dias selecionados usam o par de cores do botão primário sólido, incluindo as extremidades de intervalos.
|
|
19
|
+
|
|
20
|
+
## Alternativas e consequências
|
|
21
|
+
|
|
22
|
+
O input nativo foi experimentado e descartado pela inconsistência com os outros campos. Converter datas civis para timestamps criaria dependência de fuso sem necessidade. Copiar a barra de filtros ou inserir um calendário com consulta própria duplicaria responsabilidades já cobertas por ActionList.
|
|
23
|
+
|
|
24
|
+
As extensões são aditivas. A mudança de legenda e de contraste alcança todos os consumidores do Calendar, portanto inclui regressão do modo de intervalo e dos formatters. Mês inválido em CalendarMonthView é erro de programação; consumidores validam sua entrada antes de renderizar.
|
|
25
|
+
|
|
26
|
+
## Verificação
|
|
27
|
+
|
|
28
|
+
Testes de DateInput cobrem máscara, cursor, teclado, datas inválidas, reset e integração com ActionForm. Testes da lista cobrem posicionamento do período e grade com navegação. Testes de Calendar cobrem identidade da raiz, formatter e seleção. Os gates da release incluem tipagem, suíte completa, Chromium e instalação de um consumidor a partir do tarball.
|
package/docs/adr/README.md
CHANGED
|
@@ -24,3 +24,5 @@ estar documentado na superfície correspondente, sem exigir a reconstrução des
|
|
|
24
24
|
- [ADR 0064 — ActionList apresenta itens e conecta fontes externas](0064-action-list-com-itens-e-fontes-externas.md).
|
|
25
25
|
|
|
26
26
|
- [ADR 0065 — Presentation compõe seções declaradas](0065-presentation-composes-settings-sections.md).
|
|
27
|
+
|
|
28
|
+
- [ADR 0066 — Datas civis e calendário mensal na lista](0066-civil-date-input-and-calendar-list.md).
|
package/package.json
CHANGED
package/src/core/types.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { createContext, useContext, useEffect, useId } from "react";
|
|
21
|
-
import type
|
|
21
|
+
import { Controller, type UseFormReturn } from "react-hook-form";
|
|
22
22
|
import {
|
|
23
23
|
getLogicalType,
|
|
24
24
|
type FieldWidget,
|
|
@@ -38,6 +38,7 @@ import { toast } from "../primitives/sonner.tsx";
|
|
|
38
38
|
import { Button, type ButtonVariant } from "../primitives/button.tsx";
|
|
39
39
|
import { ButtonGroup } from "../primitives/button-group.tsx";
|
|
40
40
|
import { Input } from "../primitives/input.tsx";
|
|
41
|
+
import { DateInput } from "../primitives/date-input.tsx";
|
|
41
42
|
import { Textarea } from "../primitives/textarea.tsx";
|
|
42
43
|
import { Checkbox } from "../primitives/checkbox.tsx";
|
|
43
44
|
import { IconPicker } from "../primitives/icon-picker.tsx";
|
|
@@ -680,6 +681,24 @@ export function ActionFormField({
|
|
|
680
681
|
aria-describedby={describedBy}
|
|
681
682
|
{...form.register(name)}
|
|
682
683
|
/>
|
|
684
|
+
) : spec.widget === "date" ? (
|
|
685
|
+
<Controller
|
|
686
|
+
name={name}
|
|
687
|
+
control={form.control}
|
|
688
|
+
render={({ field }) => (
|
|
689
|
+
<DateInput
|
|
690
|
+
id={name}
|
|
691
|
+
name={field.name}
|
|
692
|
+
ref={field.ref}
|
|
693
|
+
value={(field.value as string | undefined) ?? ""}
|
|
694
|
+
onValueChange={field.onChange}
|
|
695
|
+
onBlur={field.onBlur}
|
|
696
|
+
placeholder={spec.placeholder}
|
|
697
|
+
aria-invalid={errorMessage !== undefined}
|
|
698
|
+
aria-describedby={describedBy}
|
|
699
|
+
/>
|
|
700
|
+
)}
|
|
701
|
+
/>
|
|
683
702
|
) : (
|
|
684
703
|
<Input
|
|
685
704
|
id={name}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../../lib/cn.ts";
|
|
3
|
+
|
|
4
|
+
export interface CalendarMonthViewProps<TItem> {
|
|
5
|
+
/** Mês civil no formato YYYY-MM. */
|
|
6
|
+
month: string;
|
|
7
|
+
items: readonly TItem[];
|
|
8
|
+
dateOf: (item: TItem) => string;
|
|
9
|
+
renderDay: (date: string, items: readonly TItem[]) => React.ReactNode;
|
|
10
|
+
dayLabel: (date: string, items: readonly TItem[]) => string;
|
|
11
|
+
onDayClick: (date: string, items: readonly TItem[]) => void;
|
|
12
|
+
label: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Grade mensal para ActionList.views. Consulta e estados continuam pertencendo à lista. */
|
|
17
|
+
export function CalendarMonthView<TItem>({
|
|
18
|
+
month,
|
|
19
|
+
items,
|
|
20
|
+
dateOf,
|
|
21
|
+
renderDay,
|
|
22
|
+
dayLabel,
|
|
23
|
+
onDayClick,
|
|
24
|
+
label,
|
|
25
|
+
className,
|
|
26
|
+
}: CalendarMonthViewProps<TItem>) {
|
|
27
|
+
const root = React.useRef<HTMLDivElement>(null);
|
|
28
|
+
const [focused, setFocused] = React.useState(`${month}-01`);
|
|
29
|
+
if (!/^[1-9]\d{3}-(?:0[1-9]|1[0-2])$/.test(month))
|
|
30
|
+
throw new RangeError("CalendarMonthView requires YYYY-MM");
|
|
31
|
+
const first = new Date(`${month}-01T00:00:00Z`);
|
|
32
|
+
const last = new Date(first);
|
|
33
|
+
last.setUTCMonth(last.getUTCMonth() + 1, 0);
|
|
34
|
+
const length = last.getUTCDate();
|
|
35
|
+
const offset = first.getUTCDay();
|
|
36
|
+
const rows = Math.ceil((offset + length) / 7);
|
|
37
|
+
const focusDate = focused.startsWith(`${month}-`) ? focused : `${month}-01`;
|
|
38
|
+
const grouped = new Map<string, TItem[]>();
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
const date = dateOf(item);
|
|
41
|
+
const group = grouped.get(date) ?? [];
|
|
42
|
+
group.push(item);
|
|
43
|
+
grouped.set(date, group);
|
|
44
|
+
}
|
|
45
|
+
function move(event: React.KeyboardEvent, day: number) {
|
|
46
|
+
const weekday = (offset + day - 1) % 7;
|
|
47
|
+
const delta = {
|
|
48
|
+
ArrowLeft: -1,
|
|
49
|
+
ArrowRight: 1,
|
|
50
|
+
ArrowUp: -7,
|
|
51
|
+
ArrowDown: 7,
|
|
52
|
+
Home: -weekday,
|
|
53
|
+
End: 6 - weekday,
|
|
54
|
+
}[event.key];
|
|
55
|
+
if (delta === undefined) return;
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
const next = Math.max(1, Math.min(length, day + delta));
|
|
58
|
+
const date = `${month}-${String(next).padStart(2, "0")}`;
|
|
59
|
+
setFocused(date);
|
|
60
|
+
root.current
|
|
61
|
+
?.querySelector<HTMLButtonElement>(`[data-calendar-date="${date}"]`)
|
|
62
|
+
?.focus();
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<div className={cn("overflow-x-auto", className)}>
|
|
66
|
+
<div
|
|
67
|
+
ref={root}
|
|
68
|
+
data-slot="calendar-month-view"
|
|
69
|
+
role="grid"
|
|
70
|
+
aria-label={label}
|
|
71
|
+
className="min-w-[35rem] overflow-hidden rounded-lg border"
|
|
72
|
+
>
|
|
73
|
+
<div role="row" className="grid grid-cols-7 border-b">
|
|
74
|
+
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
|
|
75
|
+
<div
|
|
76
|
+
role="columnheader"
|
|
77
|
+
key={day}
|
|
78
|
+
className="p-2 text-center text-sm text-muted-foreground"
|
|
79
|
+
>
|
|
80
|
+
{day}
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
{Array.from({ length: rows }, (_, row) => (
|
|
85
|
+
<div
|
|
86
|
+
role="row"
|
|
87
|
+
key={row}
|
|
88
|
+
className="grid grid-cols-7 not-last:border-b"
|
|
89
|
+
>
|
|
90
|
+
{Array.from({ length: 7 }, (_, column) => {
|
|
91
|
+
const day = row * 7 + column - offset + 1;
|
|
92
|
+
if (day < 1 || day > length)
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
role="gridcell"
|
|
96
|
+
key={column}
|
|
97
|
+
className="bg-muted/20 not-last:border-r"
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
const date = `${month}-${String(day).padStart(2, "0")}`;
|
|
101
|
+
const entries = grouped.get(date) ?? [];
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
role="gridcell"
|
|
105
|
+
key={column}
|
|
106
|
+
className="min-w-0 not-last:border-r"
|
|
107
|
+
>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
data-calendar-date={date}
|
|
111
|
+
tabIndex={date === focusDate ? 0 : -1}
|
|
112
|
+
aria-label={dayLabel(date, entries)}
|
|
113
|
+
onFocus={() => setFocused(date)}
|
|
114
|
+
onKeyDown={(event) => move(event, day)}
|
|
115
|
+
onClick={() => onDayClick(date, entries)}
|
|
116
|
+
className="flex min-h-28 w-full flex-col gap-2 p-3 text-left text-sm text-foreground hover:bg-muted/40 focus-visible:relative focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-ring focus-visible:-outline-offset-2"
|
|
117
|
+
>
|
|
118
|
+
<span className="font-medium">{day}</span>
|
|
119
|
+
{renderDay(date, entries)}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -315,6 +315,8 @@ export interface ActionFilterBarProps {
|
|
|
315
315
|
refreshing?: boolean;
|
|
316
316
|
/** Posição do período em relação aos filtros inline. Também pode ancorá-lo após um filtro. */
|
|
317
317
|
periodPosition?: "before" | "after" | { afterFilter: string };
|
|
318
|
+
/** Substitui o seletor de período; estado e input temporal ficam com o consumidor. */
|
|
319
|
+
periodControl?: ReactNode;
|
|
318
320
|
/** Composição do período com uma dimensão temporal declarada como filtro inline. */
|
|
319
321
|
period?: PeriodLayout;
|
|
320
322
|
/** Controles auxiliares, agrupados com o refresh em um ButtonGroup espaçado. */
|
|
@@ -416,6 +418,8 @@ interface ActionListBaseProps<TInput, TItem> {
|
|
|
416
418
|
advancedFilters?: AdvancedFiltersLayout;
|
|
417
419
|
/** Posição do período em relação aos filtros inline. Também pode ancorá-lo após um filtro. */
|
|
418
420
|
periodPosition?: "before" | "after" | { afterFilter: string };
|
|
421
|
+
/** Substitui o seletor de período; estado e input temporal ficam com o consumidor. */
|
|
422
|
+
periodControl?: ReactNode;
|
|
419
423
|
/** Composição do período com uma dimensão temporal declarada como filtro inline. */
|
|
420
424
|
period?: PeriodLayout;
|
|
421
425
|
/** Views alternativas da mesma listagem — o segment na toolbar alterna entre a
|
|
@@ -1414,6 +1418,7 @@ export function ActionFilterBar({
|
|
|
1414
1418
|
onRefresh,
|
|
1415
1419
|
refreshing = false,
|
|
1416
1420
|
periodPosition = "before",
|
|
1421
|
+
periodControl: customPeriodControl,
|
|
1417
1422
|
period,
|
|
1418
1423
|
controls,
|
|
1419
1424
|
actions,
|
|
@@ -1433,8 +1438,9 @@ export function ActionFilterBar({
|
|
|
1433
1438
|
periods[0]?.value ??
|
|
1434
1439
|
null;
|
|
1435
1440
|
const activePeriod = state.period ?? defaultPeriod;
|
|
1441
|
+
const hasPeriod = customPeriodControl != null || periods.length > 0;
|
|
1436
1442
|
const periodBasisEntry =
|
|
1437
|
-
|
|
1443
|
+
hasPeriod && period !== undefined
|
|
1438
1444
|
? inlineSpecs.find(([name]) => name === period.basisFilter)
|
|
1439
1445
|
: undefined;
|
|
1440
1446
|
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
@@ -1484,18 +1490,18 @@ export function ActionFilterBar({
|
|
|
1484
1490
|
});
|
|
1485
1491
|
const inlineControlCount =
|
|
1486
1492
|
inlineSpecs.filter(([name]) => name !== periodBasisEntry?.[0]).length +
|
|
1487
|
-
(
|
|
1493
|
+
(hasPeriod ? 1 : 0) +
|
|
1488
1494
|
(hasSearch ? 1 : 0);
|
|
1489
1495
|
const showInlineLabels = inlineControlCount > 1;
|
|
1490
|
-
const searchAtActionsEnd =
|
|
1491
|
-
hasSearch && (inlineSpecs.length > 0 || periods.length > 0);
|
|
1496
|
+
const searchAtActionsEnd = hasSearch && (inlineSpecs.length > 0 || hasPeriod);
|
|
1492
1497
|
const periodAfterFilter =
|
|
1493
1498
|
typeof periodPosition === "object" ? periodPosition.afterFilter : null;
|
|
1494
1499
|
const anchoredPeriodExists = inlineSpecs.some(
|
|
1495
1500
|
([name]) => name === periodAfterFilter,
|
|
1496
1501
|
);
|
|
1497
1502
|
const periodField =
|
|
1498
|
-
|
|
1503
|
+
customPeriodControl ??
|
|
1504
|
+
(periods.length > 0 ? (
|
|
1499
1505
|
<PeriodControl
|
|
1500
1506
|
periods={periods}
|
|
1501
1507
|
period={activePeriod}
|
|
@@ -1518,7 +1524,7 @@ export function ActionFilterBar({
|
|
|
1518
1524
|
})
|
|
1519
1525
|
}
|
|
1520
1526
|
/>
|
|
1521
|
-
) : null;
|
|
1527
|
+
) : null);
|
|
1522
1528
|
const periodControl =
|
|
1523
1529
|
periodField === null || periodBasisEntry !== undefined ? null : (
|
|
1524
1530
|
<Labeled
|
|
@@ -1585,24 +1591,26 @@ export function ActionFilterBar({
|
|
|
1585
1591
|
aria-hidden="true"
|
|
1586
1592
|
/>
|
|
1587
1593
|
<div className="pl-2">
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1594
|
+
{customPeriodControl ?? (
|
|
1595
|
+
<PeriodControl
|
|
1596
|
+
periods={periods}
|
|
1597
|
+
period={activePeriod}
|
|
1598
|
+
from={state.from}
|
|
1599
|
+
to={state.to}
|
|
1600
|
+
clearable={false}
|
|
1601
|
+
embedded
|
|
1602
|
+
onChange={(next) =>
|
|
1603
|
+
onStateChange({
|
|
1604
|
+
...state,
|
|
1605
|
+
...next,
|
|
1606
|
+
period:
|
|
1607
|
+
next.period === defaultPeriod
|
|
1608
|
+
? null
|
|
1609
|
+
: next.period,
|
|
1610
|
+
})
|
|
1611
|
+
}
|
|
1612
|
+
/>
|
|
1613
|
+
)}
|
|
1606
1614
|
</div>
|
|
1607
1615
|
<Separator
|
|
1608
1616
|
data-slot="action-filter-period-separator"
|
|
@@ -1831,6 +1839,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1831
1839
|
filterIcons,
|
|
1832
1840
|
advancedFilters,
|
|
1833
1841
|
periodPosition,
|
|
1842
|
+
periodControl,
|
|
1834
1843
|
period,
|
|
1835
1844
|
views,
|
|
1836
1845
|
batch,
|
|
@@ -1966,6 +1975,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1966
1975
|
hasSearch ||
|
|
1967
1976
|
filterSpecs.length > 0 ||
|
|
1968
1977
|
periods.length > 0 ||
|
|
1978
|
+
periodControl != null ||
|
|
1969
1979
|
hasSegment ||
|
|
1970
1980
|
viewControls != null ||
|
|
1971
1981
|
(title === undefined && toolbarActions != null) ||
|
|
@@ -1977,8 +1987,9 @@ export function ActionList<TInput, TItem>({
|
|
|
1977
1987
|
input: input as Record<string, unknown>,
|
|
1978
1988
|
filterDefaults,
|
|
1979
1989
|
pageSize,
|
|
1990
|
+
applyPeriod: periodControl == null,
|
|
1980
1991
|
}) as TInput,
|
|
1981
|
-
[input, state, pageSize, filterDefaults, action],
|
|
1992
|
+
[input, state, pageSize, filterDefaults, action, periodControl],
|
|
1982
1993
|
);
|
|
1983
1994
|
|
|
1984
1995
|
const inputKey = JSON.stringify([effectiveInput, activeGroupBy]);
|
|
@@ -2262,6 +2273,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2262
2273
|
filterIcons={filterIcons}
|
|
2263
2274
|
advancedFilters={advancedFilters}
|
|
2264
2275
|
periodPosition={periodPosition}
|
|
2276
|
+
periodControl={periodControl}
|
|
2265
2277
|
period={period}
|
|
2266
2278
|
controls={
|
|
2267
2279
|
<>
|
|
@@ -3283,10 +3295,13 @@ export function listStateToInput(
|
|
|
3283
3295
|
input = {},
|
|
3284
3296
|
filterDefaults,
|
|
3285
3297
|
pageSize = 50,
|
|
3298
|
+
applyPeriod = true,
|
|
3286
3299
|
}: {
|
|
3287
3300
|
input?: Record<string, unknown>;
|
|
3288
3301
|
filterDefaults?: Record<string, string>;
|
|
3289
3302
|
pageSize?: number;
|
|
3303
|
+
/** False quando o consumidor fornece integralmente o intervalo temporal em input. */
|
|
3304
|
+
applyPeriod?: boolean;
|
|
3290
3305
|
} = {},
|
|
3291
3306
|
): Record<string, unknown> {
|
|
3292
3307
|
const sort = state.sort ?? action.sort?.default?.[0] ?? null;
|
|
@@ -3306,11 +3321,12 @@ export function listStateToInput(
|
|
|
3306
3321
|
}
|
|
3307
3322
|
// Período: preset materializa AQUI (a URL guarda o preset relativo); custom usa o
|
|
3308
3323
|
// range. state.period null = o default do contrato (sempre aplicado).
|
|
3309
|
-
const effectivePeriod =
|
|
3310
|
-
state.period ??
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3324
|
+
const effectivePeriod = applyPeriod
|
|
3325
|
+
? (state.period ??
|
|
3326
|
+
action.periods?.find((p) => p.default === true)?.value ??
|
|
3327
|
+
action.periods?.[0]?.value ??
|
|
3328
|
+
null)
|
|
3329
|
+
: null;
|
|
3314
3330
|
let range: { from: string; to: string } | null = null;
|
|
3315
3331
|
if (
|
|
3316
3332
|
effectivePeriod === "custom" &&
|
|
@@ -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
|
)}
|
|
@@ -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,11 @@ referência; itens estáticos permanecem como declarados no contrato.
|
|
|
122
122
|
|
|
123
123
|
## Widgets declarativos
|
|
124
124
|
|
|
125
|
-
`FieldSpec.widget` aceita `password`, `textarea`, `code`, `lines`, `refItems`, `icon` e `toggle-group`.
|
|
125
|
+
`FieldSpec.widget` aceita `date`, `password`, `textarea`, `code`, `lines`, `refItems`, `icon` e `toggle-group`.
|
|
126
|
+
Para uma data civil, declare `widget: 'date'` e use uma string `YYYY-MM-DD` no schema e no
|
|
127
|
+
valor inicial. O formulário usa o [`DateInput`](/ui/date-input), com digitação em `DD/MM/AAAA`
|
|
128
|
+
e calendário em pt-BR. O valor válido enviado continua sendo a data civil, sem conversão para UTC.
|
|
129
|
+
Texto incompleto ou inválido chega ao schema para validação; use um schema de data civil.
|
|
126
130
|
`password` usa um input de senha com `autocomplete="new-password"`, adequado a cadastro e redefinição.
|
|
127
131
|
O literal canônico do grupo de escolhas é sempre kebab-case: `widget: 'toggle-group'`. Em enum ele
|
|
128
132
|
renderiza seleção única; em array de enum, seleção múltipla. As opções seguem a mesma precedência e
|
|
@@ -568,6 +568,26 @@ render(
|
|
|
568
568
|
);
|
|
569
569
|
```
|
|
570
570
|
|
|
571
|
+
## Período controlado pelo consumidor
|
|
572
|
+
|
|
573
|
+
Use `periodControl` quando a coleção precisar de uma navegação temporal própria, como um mês de expediente. O controle ocupa o mesmo espaço do seletor padrão e respeita `periodPosition`, inclusive a composição com `period.basisFilter`.
|
|
574
|
+
|
|
575
|
+
Com uma action que recebe `month`, mantenha o valor no consumidor e envie-o no input:
|
|
576
|
+
|
|
577
|
+
```tsx
|
|
578
|
+
const [month, setMonth] = useState("2026-11")
|
|
579
|
+
|
|
580
|
+
<ActionList
|
|
581
|
+
action={deliveryListContract}
|
|
582
|
+
input={{ month }}
|
|
583
|
+
periodControl={<Input type="month" value={month} onChange={(event) => setMonth(event.target.value)} />}
|
|
584
|
+
/>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Quando `periodControl` está presente, ActionList ignora os presets de `action.periods` e os campos `period`, `from` e `to` do estado da lista, inclusive os recuperados da URL. O intervalo vem do `input` do consumidor; não há filtro temporal oculto. Sem o controle personalizado, o comportamento padrão permanece. Ao hidratar detalhes pelo helper `listStateToInput`, passe `applyPeriod: false` para manter o mesmo recorte.
|
|
588
|
+
|
|
589
|
+
Para distribuir os resultados pelos dias, componha a extensão `views` com [CalendarMonthView](/ui/calendar-month-view). A lista continua responsável pela consulta e pelos estados assíncronos.
|
|
590
|
+
|
|
571
591
|
## Propriedades de ActionFilterBar
|
|
572
592
|
|
|
573
593
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -580,6 +600,7 @@ render(
|
|
|
580
600
|
| `filterIcons` | `Record<string, ReactNode>` | | Associa um ícone leading ao controle de cada filtro. |
|
|
581
601
|
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3; sectionIcons?: Record<string, ReactNode>; sectionVariant?: 'plain' \| 'framed' }` | `{ columns: 1, sectionVariant: 'plain' }` | Define as colunas, os ícones e o tratamento das seções; o Opus deriva a largura correspondente. |
|
|
582
602
|
| `periodPosition` | `'before' \| 'after' \| { afterFilter: string }` | `'before'` | Posiciona o período antes, depois ou imediatamente após um filtro inline. |
|
|
603
|
+
| `periodControl` | `ReactNode` | — | Controle temporal do consumidor. Substitui o seletor padrão; em ActionList, também desativa a aplicação automática dos presets ao input. |
|
|
583
604
|
| `period` | `{ basisFilter: string }` | | Reúne o período e o filtro inline indicado em um único controle. |
|
|
584
605
|
| `onRefresh` | `() => Promise<void> \| void` | | Exibe a ação de recarregar e executa a consulta do consumidor. |
|
|
585
606
|
| `refreshing` | `boolean` | `false` | Desabilita e anima a ação de recarregar durante a consulta. |
|
|
@@ -636,6 +657,7 @@ para fora de `toolbarActions`.
|
|
|
636
657
|
| `filterIcons` | `Record<string, ReactNode>` | | Ícones leading dos filtros, indexados pelo nome declarado no contrato. |
|
|
637
658
|
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3; sectionIcons?: Record<string, ReactNode>; sectionVariant?: 'plain' \| 'framed' }` | `{ columns: 1, sectionVariant: 'plain' }` | Define as colunas, os ícones e o tratamento dos grupos; o Opus deriva a largura correspondente do diálogo. |
|
|
638
659
|
| `periodPosition` | `'before' \| 'after' \| { afterFilter: string }` | `'before'` | Posiciona o período antes, depois ou imediatamente após um filtro inline. |
|
|
660
|
+
| `periodControl` | `ReactNode` | — | Controle temporal do consumidor. Substitui o seletor padrão; em ActionList, também desativa a aplicação automática dos presets ao input. |
|
|
639
661
|
| `period` | `{ basisFilter: string }` | | Reúne o período e o filtro inline indicado em um único controle. |
|
|
640
662
|
| `views` | `Record<string, ActionListView<TItem>>` | | Visualizações alternativas `{ label, icon?, render }`; o seletor alterna entre a tabela e estas. |
|
|
641
663
|
| `beforeContent` | `ReactNode \| (input: TInput) => ReactNode` | | Conteúdo entre a toolbar e o corpo; a função recebe o mesmo input efetivo da consulta, para resumos que acompanham o recorte. |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
## Registros distribuídos pelos dias
|
|
2
|
+
|
|
3
|
+
Use `CalendarMonthView` para consultar uma agenda ou expediente com conteúdo em cada dia. Para escolher uma data num formulário, use [DateInput](/ui/date-input); para selecionar um intervalo, use [Calendar](/ui/calendar).
|
|
4
|
+
|
|
5
|
+
O exemplo mostra um registro no mês inteiro. Clique ou use o teclado para abrir um dia:
|
|
6
|
+
|
|
7
|
+
```tsx preview
|
|
8
|
+
const [selected, setSelected] = useState("")
|
|
9
|
+
const entries = [{ date: "2026-11-02", title: "Expediente especial" }]
|
|
10
|
+
|
|
11
|
+
render(
|
|
12
|
+
<div className="space-y-3">
|
|
13
|
+
<CalendarMonthView
|
|
14
|
+
month="2026-11"
|
|
15
|
+
label="Agenda de novembro"
|
|
16
|
+
items={entries}
|
|
17
|
+
dateOf={(entry) => entry.date}
|
|
18
|
+
renderDay={(_date, items) => items.map((entry) => <span key={entry.title}>{entry.title}</span>)}
|
|
19
|
+
dayLabel={(date, items) => `${date}: ${items.map((entry) => entry.title).join(", ") || "Sem registros"}`}
|
|
20
|
+
onDayClick={(date) => setSelected(date)}
|
|
21
|
+
/>
|
|
22
|
+
{selected && <p>Data selecionada: {selected}</p>}
|
|
23
|
+
</div>,
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A grade sempre mostra todos os dias do mês, mesmo sem itens. Ela agrupa os registros por `dateOf`, sem consultar dados ou calcular regras de expediente. Datas são strings civis; o componente não aplica o fuso do navegador.
|
|
28
|
+
|
|
29
|
+
## Dentro de ActionList
|
|
30
|
+
|
|
31
|
+
Forneça a grade no renderer de `views`. A [ActionList](/ui/action-list) mantém consulta, filtros, carregamento, erro e vazio; use `paginate: false` no contrato para que uma página de resultados não esconda dias com registros. `periodControl` permite navegar pelo mês no espaço de período da lista, com o valor temporal controlado pelo consumidor.
|
|
32
|
+
|
|
33
|
+
## Teclado e limites
|
|
34
|
+
|
|
35
|
+
Tab entra em um único dia. Setas esquerda e direita percorrem dias; cima e baixo avançam uma semana. Home e End levam ao início ou fim da semana visível. Enter e Espaço acionam o dia focado. A navegação fica dentro do mês: o consumidor fornece os controles para trocar de mês.
|
|
36
|
+
|
|
37
|
+
A semana começa no domingo, com cabeçalhos em pt-BR. Mês inválido é erro de programação; valide valores da URL antes de renderizar. O padrão não marca hoje automaticamente nem implementa seleção de datas ou PageUp/PageDown. A raiz da grade expõe `data-slot="calendar-month-view"`.
|
|
38
|
+
|
|
39
|
+
## Propriedades de CalendarMonthView
|
|
40
|
+
|
|
41
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `month` | `string` | Obrigatória | Mês civil `AAAA-MM`, de 1000-01 a 9999-12. |
|
|
44
|
+
| `items` | `readonly TItem[]` | Obrigatória | Registros do mês, fornecidos pela coleção. |
|
|
45
|
+
| `dateOf` | `(item: TItem) => string` | Obrigatória | Data civil `AAAA-MM-DD` de cada registro. |
|
|
46
|
+
| `renderDay` | `(date: string, items: readonly TItem[]) => ReactNode` | Obrigatória | Conteúdo do dia; evite controles interativos dentro do botão. |
|
|
47
|
+
| `dayLabel` | `(date: string, items: readonly TItem[]) => string` | Obrigatória | Nome acessível do botão, incluindo data e contexto útil. |
|
|
48
|
+
| `onDayClick` | `(date: string, items: readonly TItem[]) => void` | Obrigatória | Abre ou executa a ação do dia. |
|
|
49
|
+
| `label` | `string` | Obrigatória | Nome acessível da grade. |
|
|
50
|
+
| `className` | `string` | — | Classes do contêiner externo, que permite rolagem horizontal em telas estreitas. |
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
## Dia único
|
|
2
2
|
|
|
3
3
|
Use `Calendar` quando a pessoa precisar escolher datas vendo o mês inteiro, como a data de uma
|
|
4
|
-
entrega ou a janela de um relatório. Para
|
|
4
|
+
entrega ou a janela de um relatório. Para preencher uma data com digitação e calendário em um
|
|
5
|
+
campo, use [`DateInput`](/ui/date-input). Composições próprias podem ancorá-lo em um
|
|
5
6
|
[`Popover`](/ui/popover).
|
|
6
7
|
|
|
7
8
|
Com `mode="single"`, a seleção é uma `Date`, controlada por `selected` e `onSelect`. `defaultMonth`
|
|
@@ -23,6 +24,8 @@ render(
|
|
|
23
24
|
O calendário usa pt-BR por padrão: os nomes de mês e de dia vêm do `locale` (`ptBR`, de
|
|
24
25
|
`date-fns/locale`) e os controles de navegação têm nomes acessíveis em português. Passe outro
|
|
25
26
|
`locale` para trocar o idioma; `labels` substitui nomes acessíveis individualmente.
|
|
27
|
+
O título usa mês com inicial maiúscula e barra antes do ano, como `Setembro/2026`.
|
|
28
|
+
Esse formato também aparece nos campos `DateInput`; `formatters.formatCaption` permite substituí-lo.
|
|
26
29
|
|
|
27
30
|
## Intervalo
|
|
28
31
|
|
|
@@ -181,3 +184,5 @@ no nome acessível do dia por `labels.labelDayButton`.
|
|
|
181
184
|
| `className` | `string` | | Classes somadas ao botão do dia. |
|
|
182
185
|
|
|
183
186
|
`CalendarDayButton` aceita também as props nativas de `<button>`, repassadas pelo `Calendar`.
|
|
187
|
+
|
|
188
|
+
Para consultar registros por dia, use [CalendarMonthView](/ui/calendar-month-view). A grade recebe um mês civil válido no formato `AAAA-MM`. Os cabeçalhos estão em pt-BR, e a semana começa no domingo. A consulta e os estados de carregamento e erro pertencem à ActionList.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
## Digitar ou escolher uma data
|
|
2
|
+
|
|
3
|
+
Use `DateInput` para preencher uma data civil, como uma entrega ou um feriado. O campo
|
|
4
|
+
exibe `DD/MM/AAAA` e abre o calendário em português ao clicar em qualquer parte do controle.
|
|
5
|
+
O clique mantém o foco na digitação. O ícone é decorativo e não adiciona uma parada de Tab;
|
|
6
|
+
o atalho `Alt` + seta para baixo leva o foco para o calendário.
|
|
7
|
+
|
|
8
|
+
```tsx preview col md
|
|
9
|
+
function Demo() {
|
|
10
|
+
const [date, setDate] = useState("2026-11-02")
|
|
11
|
+
return (
|
|
12
|
+
<div className="grid gap-2">
|
|
13
|
+
<Label htmlFor="delivery-date">Data de entrega</Label>
|
|
14
|
+
<DateInput id="delivery-date" value={date} onValueChange={setDate} />
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
render(<Demo />)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A máscara insere as barras durante a digitação: basta informar `02112026`. Também é possível
|
|
22
|
+
digitar `02/11/2026`, colar `2026-11-02` ou escolher o dia no
|
|
23
|
+
calendário. Uma data válida é emitida como `2026-11-02`; apagar o campo emite `''`. O componente
|
|
24
|
+
mantém o dia civil, sem transformá-lo em um instante UTC.
|
|
25
|
+
|
|
26
|
+
Texto incompleto ou uma data impossível permanecem no campo com a máscara, sem conversão para ISO.
|
|
27
|
+
Valide o valor antes de persistir: o componente não troca `31/02/2026` por um dia de março nem
|
|
28
|
+
preserva silenciosamente a data anterior. No `ActionForm`, o schema do contrato mantém essa
|
|
29
|
+
responsabilidade.
|
|
30
|
+
|
|
31
|
+
## Formulário declarativo
|
|
32
|
+
|
|
33
|
+
Declare `widget: 'date'` no campo do contrato e use um schema de data civil. O
|
|
34
|
+
[`ActionForm`](/ui/action-form) compõe o `DateInput` com rótulo, hint, erro e foco de validação.
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
const fields = { date: { label: "Data", widget: "date" as const } }
|
|
38
|
+
const input = z.object({ date: z.string().date("Informe uma data válida.") })
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Teclado e estados
|
|
42
|
+
|
|
43
|
+
Com foco no campo, `Alt` + seta para baixo abre o calendário e leva o foco até ele. As setas navegam pelos dias;
|
|
44
|
+
selecionar uma data fecha o calendário e devolve o foco ao campo. `Escape` fecha o calendário
|
|
45
|
+
sem fechar o diálogo que o contém. É possível substituir trechos selecionados e apagar com
|
|
46
|
+
`Backspace` ou `Delete`, sem ficar preso às barras da máscara. `disabled` e `readOnly` impedem
|
|
47
|
+
a abertura pelo campo e pelo atalho.
|
|
48
|
+
Associe `Label.htmlFor` ao `id` e use `aria-describedby` para ajuda ou erro.
|
|
49
|
+
|
|
50
|
+
Para manter um mês inteiro visível, use [`Calendar`](/ui/calendar). Para um campo com estado
|
|
51
|
+
interno, forneça `defaultValue`; `value` e `onValueChange` tornam o componente controlado.
|
|
52
|
+
|
|
53
|
+
## Propriedades de DateInput
|
|
54
|
+
|
|
55
|
+
| Propriedade | Tipo | Padrão | Efeito |
|
|
56
|
+
| --- | --- | --- | --- |
|
|
57
|
+
| `value` | `string` | — | Valor controlado: ISO quando válido, texto durante digitação inválida ou incompleta. |
|
|
58
|
+
| `defaultValue` | `string` | `''` | Valor inicial no modo não controlado; restaurado no reset do formulário nativo. |
|
|
59
|
+
| `onValueChange` | `(value: string) => void` | — | Recebe a data civil, o texto em edição ou `''`. |
|
|
60
|
+
| `name` | `string` | — | Envia o valor por um campo oculto no formulário nativo, sem converter para a apresentação brasileira. |
|
|
61
|
+
| `placeholder` | `string` | `DD/MM/AAAA` | Exemplo de formato no campo vazio. |
|
|
62
|
+
| `disabled`, `readOnly` | `boolean` | `false` | Impedem edição e abertura do calendário. |
|
|
63
|
+
| `id`, `ref`, `form`, `onBlur`, `aria-*` | Atributos do input | — | Identificação, foco, vínculo com formulário e descrição acessível do campo visível. |
|
|
64
|
+
| `className` | `string` | — | Classes da moldura do campo, seguindo o Input com adorno. |
|
|
65
|
+
|
|
66
|
+
O componente não aceita `type`, `onChange`, `icon`, `trailing`, `min`, `max` ou `step`.
|
|
67
|
+
O contrato define os limites de negócio; `onValueChange` substitui o evento de alteração nativo.
|
|
@@ -70,6 +70,9 @@ Associe `htmlFor` no `Label` ao `id` do campo para que o clique no rótulo mova
|
|
|
70
70
|
`type` é repassado ao `<input>` nativo, permitindo que o navegador escolha teclado e validação
|
|
71
71
|
adequados.
|
|
72
72
|
|
|
73
|
+
Para datas com digitação em formato brasileiro e calendário do Opus, use
|
|
74
|
+
[`DateInput`](/ui/date-input).
|
|
75
|
+
|
|
73
76
|
```tsx preview col md
|
|
74
77
|
<Input type="email" placeholder="Ex.: joao@softize.com.br" />
|
|
75
78
|
<Input type="password" placeholder="Token de serviço" />
|
package/src/ui/docs/registry.tsx
CHANGED
|
@@ -67,6 +67,8 @@ import dockMd from "./content/dock.md?raw";
|
|
|
67
67
|
import breadcrumbMd from "./content/breadcrumb.md?raw";
|
|
68
68
|
import buttonMd from "./content/button.md?raw";
|
|
69
69
|
import calendarMd from "./content/calendar.md?raw";
|
|
70
|
+
import dateInputMd from "./content/date-input.md?raw";
|
|
71
|
+
import calendarMonthViewMd from "./content/calendar-month-view.md?raw";
|
|
70
72
|
import metricCardMd from "./content/metric-card.md?raw";
|
|
71
73
|
import carouselMd from "./content/carousel.md?raw";
|
|
72
74
|
import askMd from "./content/ask.md?raw";
|
|
@@ -152,9 +154,15 @@ export const iconScope: Record<string, unknown> = Object.fromEntries(
|
|
|
152
154
|
);
|
|
153
155
|
|
|
154
156
|
/** Página conceitual (Começar/Framework/Design/Intro): markdown direto (o H1/lead moram no md). */
|
|
155
|
-
const doc =
|
|
156
|
-
|
|
157
|
-
)
|
|
157
|
+
const doc =
|
|
158
|
+
(content: string, codeCollapsible = true) =>
|
|
159
|
+
(): React.ReactElement => (
|
|
160
|
+
<DocMarkdown
|
|
161
|
+
content={content}
|
|
162
|
+
scope={iconScope}
|
|
163
|
+
codeCollapsible={codeCollapsible}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
158
166
|
|
|
159
167
|
/**
|
|
160
168
|
* Página de componente: o cabeçalho (título + badge de origem + whenToUse) vem do `meta` central
|
|
@@ -305,7 +313,11 @@ export const PROTOCOL_SECTIONS: DocSection[] = [
|
|
|
305
313
|
searchText: mcpMd,
|
|
306
314
|
render: doc(mcpMd),
|
|
307
315
|
},
|
|
308
|
-
{
|
|
316
|
+
{
|
|
317
|
+
slug: "ui",
|
|
318
|
+
title: "Interface com Opus ↗",
|
|
319
|
+
render: () => <UiRedirect />,
|
|
320
|
+
},
|
|
309
321
|
{
|
|
310
322
|
slug: "documentation-portal",
|
|
311
323
|
title: "Publicação de documentação",
|
|
@@ -585,6 +597,11 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
585
597
|
actionFormDialogMd,
|
|
586
598
|
),
|
|
587
599
|
},
|
|
600
|
+
{
|
|
601
|
+
slug: "calendar-month-view",
|
|
602
|
+
title: "CalendarMonthView",
|
|
603
|
+
render: comp("CalendarMonthView", "calendar-month-view", calendarMonthViewMd),
|
|
604
|
+
},
|
|
588
605
|
{
|
|
589
606
|
slug: "action-list",
|
|
590
607
|
title: "Action List",
|
|
@@ -637,6 +654,11 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
637
654
|
title: "Calendar",
|
|
638
655
|
render: comp("Calendar", "calendar", calendarMd),
|
|
639
656
|
},
|
|
657
|
+
{
|
|
658
|
+
slug: "date-input",
|
|
659
|
+
title: "DateInput",
|
|
660
|
+
render: comp("DateInput", "date-input", dateInputMd),
|
|
661
|
+
},
|
|
640
662
|
{
|
|
641
663
|
slug: "checkbox",
|
|
642
664
|
title: "Checkbox",
|
|
@@ -890,8 +912,9 @@ export const DOC_SECTIONS: DocSection[] = [
|
|
|
890
912
|
{
|
|
891
913
|
pages: [
|
|
892
914
|
...["getting-started", "cycle", "upgrading"].map((slug) =>
|
|
893
|
-
PROTOCOL_SECTIONS.flatMap((section) =>
|
|
894
|
-
.
|
|
915
|
+
PROTOCOL_SECTIONS.flatMap((section) =>
|
|
916
|
+
section.groups.flatMap((group) => group.pages),
|
|
917
|
+
).find((page) => page.slug === slug)!,
|
|
895
918
|
),
|
|
896
919
|
{
|
|
897
920
|
slug: "changelog",
|
|
@@ -902,7 +925,9 @@ export const DOC_SECTIONS: DocSection[] = [
|
|
|
902
925
|
},
|
|
903
926
|
{
|
|
904
927
|
label: "Introdução ao SDK",
|
|
905
|
-
pages: PROTOCOL_SECTIONS[0]!.groups[0]!.pages.filter(
|
|
928
|
+
pages: PROTOCOL_SECTIONS[0]!.groups[0]!.pages.filter(
|
|
929
|
+
(page) => page.slug !== "getting-started",
|
|
930
|
+
),
|
|
906
931
|
},
|
|
907
932
|
],
|
|
908
933
|
},
|
|
@@ -913,8 +938,12 @@ export const DOC_SECTIONS: DocSection[] = [
|
|
|
913
938
|
label: s.label === "Testes e evolução" ? "Testes" : s.label,
|
|
914
939
|
groups: s.groups.map((g) => ({
|
|
915
940
|
...g,
|
|
916
|
-
pages: g.pages.filter(
|
|
941
|
+
pages: g.pages.filter(
|
|
942
|
+
(p) => !["ui", "cycle", "upgrading"].includes(p.slug),
|
|
943
|
+
),
|
|
917
944
|
})),
|
|
918
945
|
})),
|
|
919
|
-
...UI_SECTIONS.map((section, index) =>
|
|
946
|
+
...UI_SECTIONS.map((section, index) =>
|
|
947
|
+
index === 0 ? { ...section, label: "Introdução à UI" } : section,
|
|
948
|
+
),
|
|
920
949
|
];
|
package/src/ui/meta.ts
CHANGED
|
@@ -131,6 +131,12 @@ export const componentMeta = {
|
|
|
131
131
|
whenToUse:
|
|
132
132
|
"Receba texto em uma única linha com os atributos nativos de `input`. Use `icon` para identidade e `trailing` para uma ação no fim do campo; associe uma Label. Quando precisar combinar múltiplos adornos ou controles na mesma moldura, use InputGroup.",
|
|
133
133
|
},
|
|
134
|
+
"date-input": {
|
|
135
|
+
name: "date-input",
|
|
136
|
+
ancestry: "opus",
|
|
137
|
+
whenToUse:
|
|
138
|
+
"Receba uma data por digitação em DD/MM/AAAA ou escolha no calendário. O valor válido permanece uma string civil AAAA-MM-DD, sem conversão de fuso horário. Associe uma Label; no ActionForm, declare widget: 'date'.",
|
|
139
|
+
},
|
|
134
140
|
label: {
|
|
135
141
|
name: "label",
|
|
136
142
|
ancestry: "shadcn",
|
|
@@ -239,11 +245,16 @@ export const componentMeta = {
|
|
|
239
245
|
whenToUse:
|
|
240
246
|
"Mostre onde a pessoa está quando a página fica vários níveis abaixo da entrada da seção e permita voltar a um nível intermediário. Use BreadcrumbLink nos níveis navegáveis, BreadcrumbPage no nível atual, que não é clicável, BreadcrumbSeparator entre eles e BreadcrumbEllipsis para colapsar trilhas longas. Quando basta voltar ao nível imediatamente acima, use PageBack; para alternar painéis no mesmo nível, Tabs.",
|
|
241
247
|
},
|
|
248
|
+
"calendar-month-view": {
|
|
249
|
+
name: "calendar-month-view",
|
|
250
|
+
ancestry: "opus",
|
|
251
|
+
whenToUse: "Consulte registros distribuídos pelos dias de um mês, como expediente e agenda. Use como visualização de ActionList para preservar consulta, filtros e estados da coleção.",
|
|
252
|
+
},
|
|
242
253
|
calendar: {
|
|
243
254
|
name: "calendar",
|
|
244
255
|
ancestry: "shadcn",
|
|
245
256
|
whenToUse:
|
|
246
|
-
'Permita escolher datas vendo o mês inteiro, como a data de uma entrega ou o período de um relatório. `mode` define a seleção — `single`, `multiple` ou `range` — e o formato de `selected` e `onSelect` (Date, Date[] ou { from, to }). `captionLayout="dropdown"` troca o título do mês por seletores de mês e ano, útil para chegar a um período distante; `numberOfMonths` mostra meses lado a lado e `disabled` impede a escolha de datas específicas. Os textos usam pt-BR por padrão. Para
|
|
257
|
+
'Permita escolher datas vendo o mês inteiro, como a data de uma entrega ou o período de um relatório. `mode` define a seleção — `single`, `multiple` ou `range` — e o formato de `selected` e `onSelect` (Date, Date[] ou { from, to }). `captionLayout="dropdown"` troca o título do mês por seletores de mês e ano, útil para chegar a um período distante; `numberOfMonths` mostra meses lado a lado e `disabled` impede a escolha de datas específicas. Os textos usam pt-BR por padrão. Para preencher uma data em um campo com digitação e calendário, use DateInput. Use Popover para composições próprias.',
|
|
247
258
|
},
|
|
248
259
|
carousel: {
|
|
249
260
|
name: "carousel",
|
package/src/ui/react.tsx
CHANGED
|
@@ -54,6 +54,10 @@ export type {
|
|
|
54
54
|
} from "./components/primitives/control.ts";
|
|
55
55
|
|
|
56
56
|
export { Input } from "./components/primitives/input.tsx";
|
|
57
|
+
export {
|
|
58
|
+
DateInput,
|
|
59
|
+
type DateInputProps,
|
|
60
|
+
} from "./components/primitives/date-input.tsx";
|
|
57
61
|
|
|
58
62
|
export { Chat } from "./components/primitives/chat.tsx";
|
|
59
63
|
export type {
|
|
@@ -652,3 +656,6 @@ export type {
|
|
|
652
656
|
PresentationListContentAdapter,
|
|
653
657
|
PresentationRowContentAdapter,
|
|
654
658
|
} from "./components/patterns/presentation-sections.tsx";
|
|
659
|
+
|
|
660
|
+
export { CalendarMonthView } from "./components/patterns/list-calendar.tsx";
|
|
661
|
+
export type { CalendarMonthViewProps } from "./components/patterns/list-calendar.tsx";
|