@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/bin/lib/check.mjs +109 -101
  3. package/bin/lib/copy.mjs +2973 -1801
  4. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
  5. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
  6. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  7. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  8. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  9. package/docs/adr/0066-civil-date-input-and-calendar-list.md +28 -0
  10. package/docs/adr/README.md +6 -0
  11. package/package.json +1 -1
  12. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  13. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  15. package/src/core/presentation.ts +938 -32
  16. package/src/core/types.ts +2 -0
  17. package/src/ui/components/patterns/form.tsx +22 -1
  18. package/src/ui/components/patterns/grouped-list.ts +2 -2
  19. package/src/ui/components/patterns/list-calendar.tsx +129 -0
  20. package/src/ui/components/patterns/list.tsx +612 -382
  21. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  22. package/src/ui/components/patterns/page.tsx +227 -197
  23. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  24. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  25. package/src/ui/components/patterns/presentation.tsx +1142 -229
  26. package/src/ui/components/patterns/structural-header.tsx +23 -32
  27. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  28. package/src/ui/components/patterns/surface.tsx +19 -29
  29. package/src/ui/components/patterns/trigger.tsx +10 -2
  30. package/src/ui/components/primitives/calendar.tsx +39 -18
  31. package/src/ui/components/primitives/close-button.tsx +11 -3
  32. package/src/ui/components/primitives/command.tsx +2 -8
  33. package/src/ui/components/primitives/date-input.tsx +276 -0
  34. package/src/ui/components/primitives/detail.tsx +1 -1
  35. package/src/ui/components/primitives/dialog.tsx +19 -81
  36. package/src/ui/components/primitives/drawer.tsx +17 -79
  37. package/src/ui/components/primitives/field.tsx +1 -1
  38. package/src/ui/components/primitives/pagination.tsx +3 -2
  39. package/src/ui/components/primitives/table.tsx +1 -1
  40. package/src/ui/components/primitives/tabs.tsx +13 -2
  41. package/src/ui/docs/content/action-form.md +6 -1
  42. package/src/ui/docs/content/action-list.md +156 -68
  43. package/src/ui/docs/content/action-trigger.md +5 -0
  44. package/src/ui/docs/content/button.md +5 -6
  45. package/src/ui/docs/content/calendar-month-view.md +50 -0
  46. package/src/ui/docs/content/calendar.md +6 -1
  47. package/src/ui/docs/content/command.md +62 -50
  48. package/src/ui/docs/content/compositions.md +129 -459
  49. package/src/ui/docs/content/date-input.md +67 -0
  50. package/src/ui/docs/content/dialog.md +44 -112
  51. package/src/ui/docs/content/drawer.md +44 -103
  52. package/src/ui/docs/content/field.md +2 -1
  53. package/src/ui/docs/content/input.md +3 -0
  54. package/src/ui/docs/content/page.md +204 -412
  55. package/src/ui/docs/content/presentation.md +371 -23
  56. package/src/ui/docs/content/separator.md +1 -1
  57. package/src/ui/docs/content/surface.md +65 -120
  58. package/src/ui/docs/content/tokens.md +18 -4
  59. package/src/ui/docs/registry.tsx +38 -9
  60. package/src/ui/drivers/context.tsx +11 -4
  61. package/src/ui/drivers/react.tsx +27 -7
  62. package/src/ui/meta.ts +17 -6
  63. package/src/ui/react.tsx +41 -8
  64. package/src/ui/theme.css +44 -33
package/src/core/types.ts CHANGED
@@ -598,6 +598,8 @@ export type OptionsSpec =
598
598
 
599
599
  /** Overrides declarativos nativos reconhecidos pelo ActionForm. */
600
600
  export type BuiltInFieldWidget =
601
+ | 'date'
602
+ | 'password'
601
603
  | 'textarea'
602
604
  | 'code'
603
605
  | 'lines'
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { createContext, useContext, useEffect, useId } from "react";
21
- import type { UseFormReturn } from "react-hook-form";
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.run<Paginated<TItem>>(action as unknown as ActionDef, groupInput)
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
+ }