@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
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,9 +681,29 @@ 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}
|
|
705
|
+
type={spec.widget === "password" ? "password" : undefined}
|
|
706
|
+
autoComplete={spec.widget === "password" ? "new-password" : undefined}
|
|
686
707
|
placeholder={spec.placeholder ?? ""}
|
|
687
708
|
aria-invalid={errorMessage !== undefined}
|
|
688
709
|
aria-describedby={describedBy}
|
|
@@ -15,7 +15,7 @@ export interface ListActionGroup<TItem> {
|
|
|
15
15
|
|
|
16
16
|
/** Implementação interna do ActionList para executar um recorte paginado por grupo. */
|
|
17
17
|
export function useGroupedListQueries<TItem = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(
|
|
18
|
-
client: ClientAdapter,
|
|
18
|
+
client: ClientAdapter | null,
|
|
19
19
|
action: { name: string; kind: 'list' },
|
|
20
20
|
input: TInput,
|
|
21
21
|
field: string | null,
|
|
@@ -52,7 +52,7 @@ export function useGroupedListQueries<TItem = unknown, TInput extends Record<str
|
|
|
52
52
|
return {
|
|
53
53
|
queryKey: [action.name, groupInput],
|
|
54
54
|
queryFn: async (): Promise<Paginated<TItem>> => {
|
|
55
|
-
const result = await client
|
|
55
|
+
const result = await client!.run<Paginated<TItem>>(action as unknown as ActionDef, groupInput)
|
|
56
56
|
if (!result.ok) throw result.error
|
|
57
57
|
return result.data
|
|
58
58
|
},
|
|
@@ -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
|
+
}
|