@softize/opus 12.10.0 → 12.11.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 (76) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bin/lib/check.mjs +1103 -310
  3. package/bin/lib/copy.mjs +11 -0
  4. package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +65 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
  7. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
  8. package/package.json +1 -1
  9. package/registry/instructions/opus.md +5 -0
  10. package/registry/skills/build-opus-ui/SKILL.md +27 -16
  11. package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
  12. package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
  13. package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
  14. package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
  15. package/registry/templates/app/src/App.tsx +1 -1
  16. package/src/core/dictionary.ts +52 -14
  17. package/src/core/index.ts +10 -0
  18. package/src/core/ui-context.ts +29 -0
  19. package/src/schema/drivers/zod.ts +17 -8
  20. package/src/ui/components/patterns/action-form-card.tsx +18 -12
  21. package/src/ui/components/patterns/confirm.tsx +26 -3
  22. package/src/ui/components/patterns/content-header.tsx +335 -61
  23. package/src/ui/components/patterns/data-state.tsx +23 -10
  24. package/src/ui/components/patterns/list.tsx +1096 -777
  25. package/src/ui/components/patterns/page-state.tsx +115 -0
  26. package/src/ui/components/patterns/page.tsx +231 -41
  27. package/src/ui/components/patterns/sidebar.tsx +354 -80
  28. package/src/ui/components/patterns/trigger.tsx +13 -9
  29. package/src/ui/components/patterns/view.tsx +7 -11
  30. package/src/ui/components/primitives/alert-dialog.tsx +7 -5
  31. package/src/ui/components/primitives/alert.tsx +298 -110
  32. package/src/ui/components/primitives/ask.tsx +2 -1
  33. package/src/ui/components/primitives/badge.tsx +91 -30
  34. package/src/ui/components/primitives/button.tsx +99 -60
  35. package/src/ui/components/primitives/calendar.tsx +39 -39
  36. package/src/ui/components/primitives/card.tsx +96 -23
  37. package/src/ui/components/primitives/detail.tsx +2 -2
  38. package/src/ui/components/primitives/dictionary-value.tsx +9 -14
  39. package/src/ui/components/primitives/dot.tsx +74 -21
  40. package/src/ui/components/primitives/drawer.tsx +33 -20
  41. package/src/ui/components/primitives/item.tsx +137 -81
  42. package/src/ui/components/primitives/menu.tsx +11 -3
  43. package/src/ui/components/primitives/metric-card.tsx +133 -0
  44. package/src/ui/components/primitives/table.tsx +2 -2
  45. package/src/ui/docs/DocBrowser.tsx +3 -3
  46. package/src/ui/docs/changelog.tsx +1 -1
  47. package/src/ui/docs/content/action-form-card.md +1 -1
  48. package/src/ui/docs/content/alert-dialog.md +8 -8
  49. package/src/ui/docs/content/alert.md +49 -25
  50. package/src/ui/docs/content/badge.md +18 -19
  51. package/src/ui/docs/content/button.md +12 -9
  52. package/src/ui/docs/content/card.md +5 -5
  53. package/src/ui/docs/content/content.md +44 -0
  54. package/src/ui/docs/content/customization.md +2 -2
  55. package/src/ui/docs/content/detail.md +5 -2
  56. package/src/ui/docs/content/dialog.md +2 -2
  57. package/src/ui/docs/content/dictionary-value.md +11 -10
  58. package/src/ui/docs/content/dot.md +7 -7
  59. package/src/ui/docs/content/drawer.md +6 -3
  60. package/src/ui/docs/content/input-group.md +3 -2
  61. package/src/ui/docs/content/item.md +47 -21
  62. package/src/ui/docs/content/menu.md +5 -4
  63. package/src/ui/docs/content/metric-card.md +41 -0
  64. package/src/ui/docs/content/page-state.md +45 -0
  65. package/src/ui/docs/content/page.md +48 -10
  66. package/src/ui/docs/content/semantic-context.md +63 -0
  67. package/src/ui/docs/content/sidebar.md +4 -4
  68. package/src/ui/docs/content/skeleton.md +2 -2
  69. package/src/ui/docs/content/table.md +3 -3
  70. package/src/ui/docs/content/tokens.md +28 -0
  71. package/src/ui/docs/doc-client.tsx +2 -2
  72. package/src/ui/docs/registry.tsx +596 -228
  73. package/src/ui/lib/semantic-context.ts +30 -0
  74. package/src/ui/meta.ts +292 -270
  75. package/src/ui/react.tsx +378 -111
  76. package/src/ui/theme.css +66 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * <PageState /> — estado integral da área de conteúdo de uma Page.
3
+ *
4
+ * Page continua responsável somente pelo esqueleto. Na forma curta, PageState pode ser escrito
5
+ * como filho direto porque Page cria PageBody; na composição explícita, ele fica dentro de
6
+ * PageBody. Estados de uma seção ou coleção continuam em DataState, ActionView e ActionList.
7
+ */
8
+
9
+ import type { ReactNode } from "react";
10
+ import { TriangleAlert } from "lucide-react";
11
+ import { cn } from "../../lib/cn.ts";
12
+ import { Alert } from "../primitives/alert.tsx";
13
+ import {
14
+ Empty,
15
+ EmptyContent,
16
+ EmptyDescription,
17
+ EmptyHeader,
18
+ EmptyMedia,
19
+ EmptyTitle,
20
+ } from "../primitives/empty.tsx";
21
+ import { Spinner } from "../primitives/spinner.tsx";
22
+
23
+ export type PageStateStatus = "loading" | "error" | "empty" | "ready";
24
+
25
+ export interface PageStateProps {
26
+ status: PageStateStatus;
27
+ /** Título específico do estado. Defaults seguros existem para loading, error e empty. */
28
+ title?: ReactNode;
29
+ /** Contexto e próximo passo, quando acrescentarem informação ao título. */
30
+ description?: ReactNode;
31
+ /** Ícone decorativo. Error usa TriangleAlert por default; empty não inventa um ícone. */
32
+ icon?: ReactNode;
33
+ /** Ação aplicável ao estado, como tentar novamente, escolher ou criar. */
34
+ action?: ReactNode;
35
+ /** Conteúdo normal da página, renderizado somente em ready. */
36
+ children?: ReactNode;
37
+ className?: string;
38
+ }
39
+
40
+ export function PageState({
41
+ status,
42
+ title,
43
+ description,
44
+ icon,
45
+ action,
46
+ children,
47
+ className,
48
+ }: PageStateProps): React.ReactElement {
49
+ if (status === "ready") return <>{children}</>;
50
+
51
+ if (status === "loading") {
52
+ return (
53
+ <div
54
+ data-slot="page-state"
55
+ data-status="loading"
56
+ role="status"
57
+ aria-live="polite"
58
+ className={cn(
59
+ "flex min-h-64 flex-col items-center justify-center gap-3 text-center",
60
+ className,
61
+ )}
62
+ >
63
+ <Spinner aria-hidden="true" aria-label={undefined} role="presentation" />
64
+ <div className="max-w-md space-y-1">
65
+ <p data-slot="page-state-title" className="text-sm font-medium">
66
+ {title ?? "Carregando…"}
67
+ </p>
68
+ {description !== undefined && (
69
+ <p
70
+ data-slot="page-state-description"
71
+ className="text-sm text-muted-foreground"
72
+ >
73
+ {description}
74
+ </p>
75
+ )}
76
+ </div>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ if (status === "error") {
82
+ return (
83
+ <div
84
+ data-slot="page-state"
85
+ data-status="error"
86
+ className={cn("flex min-h-64 items-center justify-center", className)}
87
+ >
88
+ <Alert
89
+ className="max-w-xl"
90
+ context="danger"
91
+ icon={icon ?? <TriangleAlert />}
92
+ title={title ?? "Não foi possível carregar esta página"}
93
+ description={description}
94
+ >
95
+ {action}
96
+ </Alert>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ return (
102
+ <div data-slot="page-state" data-status="empty" className={className}>
103
+ <Empty className="min-h-64">
104
+ <EmptyHeader>
105
+ {icon !== undefined && <EmptyMedia variant="icon">{icon}</EmptyMedia>}
106
+ <EmptyTitle>{title ?? "Nada por aqui"}</EmptyTitle>
107
+ {description !== undefined && (
108
+ <EmptyDescription>{description}</EmptyDescription>
109
+ )}
110
+ </EmptyHeader>
111
+ {action !== undefined && <EmptyContent>{action}</EmptyContent>}
112
+ </Empty>
113
+ </div>
114
+ );
115
+ }
@@ -1,52 +1,242 @@
1
1
  /**
2
- * <Page /> — o esqueleto de página do back-office.
2
+ * <Page /> — esqueleto composto de página do back-office.
3
3
  *
4
- * Padroniza o que toda página repete: <main> + container centralizado com teto de 72rem
5
- * (`max-w-6xl`; quem precisar de outra largura sobrescreve por `className`) + header
6
- * (título, descrição e ações à direita, alinhadas ao fim desse bloco — em geral o botão
7
- * de criar). O conteúdo é
8
- * children, sem nada imposto. Presentacional (sem contrato) de propósito: uma página
9
- * agrega N fontes; o que é action-driven mora dentro (ActionList, listas query-backed).
4
+ * A forma curta cobre o caso comum; a forma explícita expõe a mesma anatomia para composições
5
+ * especiais. Ambas preservam o container centralizado com teto de 80rem.
10
6
  */
11
7
 
12
- import type { ReactNode } from 'react'
13
- import { cn } from '../../lib/cn.ts'
14
- import { ContentHeaderFrame } from './content-header.tsx'
8
+ import {
9
+ Children,
10
+ createContext,
11
+ isValidElement,
12
+ useContext,
13
+ type HTMLAttributes,
14
+ type ReactElement,
15
+ type ReactNode,
16
+ } from "react";
17
+ import { cn } from "../../lib/cn.ts";
15
18
 
16
- export interface PageProps {
17
- title: string
18
- /** Total de itens ao lado do título (o estilo do Maestro: mono, esmaecido). */
19
- count?: number
20
- /** Linha de contexto sob o título. */
21
- description?: ReactNode
22
- /** Ações à direita do header, alinhadas ao fim do bloco de contexto. */
23
- actions?: ReactNode
24
- /** Classes do container. Default: centralizado com `max-w-6xl` (72rem). */
25
- className?: string
26
- children: ReactNode
19
+ const PageContext = createContext(false);
20
+ const PageHeaderContext = createContext(false);
21
+
22
+ function requireParent(
23
+ inside: boolean,
24
+ component: string,
25
+ parent: string,
26
+ ): void {
27
+ if (!inside) throw new Error(`${component} deve estar dentro de ${parent}.`);
28
+ }
29
+
30
+ interface PageBaseProps extends Omit<
31
+ HTMLAttributes<HTMLElement>,
32
+ "children" | "title"
33
+ > {
34
+ /** Classes do container. Default: centralizado com `max-w-7xl` (80rem). */
35
+ className?: string;
36
+ }
37
+
38
+ interface PageShorthandProps extends PageBaseProps {
39
+ title: ReactNode;
40
+ /** Total de itens ao lado do título. */
41
+ count?: number;
42
+ description?: ReactNode;
43
+ actions?: ReactNode;
44
+ children: ReactNode;
45
+ }
46
+
47
+ interface PageComposedProps extends PageBaseProps {
48
+ title?: never;
49
+ count?: never;
50
+ description?: never;
51
+ actions?: never;
52
+ children: ReactNode;
27
53
  }
28
54
 
29
- export function Page({ title, count, description, actions, className, children }: PageProps): React.ReactElement {
55
+ export type PageProps = PageShorthandProps | PageComposedProps;
56
+
57
+ export function Page({
58
+ title,
59
+ count,
60
+ description,
61
+ actions,
62
+ className,
63
+ children,
64
+ ...props
65
+ }: PageProps): ReactElement {
66
+ const shorthand = title !== undefined;
67
+ const nodes = Children.toArray(children);
68
+ if (
69
+ shorthand &&
70
+ nodes.some((node) => isValidElement(node) && node.type === PageHeader)
71
+ ) {
72
+ throw new Error(
73
+ "Page não permite misturar propriedades de shorthand com PageHeader explícito.",
74
+ );
75
+ }
76
+ if (!shorthand) {
77
+ const headers = nodes.filter(
78
+ (node) => isValidElement(node) && node.type === PageHeader,
79
+ );
80
+ const bodies = nodes.filter(
81
+ (node) => isValidElement(node) && node.type === PageBody,
82
+ );
83
+ if (
84
+ headers.length !== 1 ||
85
+ bodies.length !== 1 ||
86
+ headers.length + bodies.length !== nodes.length
87
+ ) {
88
+ throw new Error(
89
+ "Page explícito exige exatamente um PageHeader e um PageBody como filhos diretos.",
90
+ );
91
+ }
92
+ }
93
+
94
+ return (
95
+ <PageContext.Provider value>
96
+ <main data-slot="page" className="min-w-0 flex-1" {...props}>
97
+ <div className={cn("mx-auto max-w-7xl px-8 py-8", className)}>
98
+ {shorthand ? (
99
+ <>
100
+ <PageHeader>
101
+ <PageTitle>{title}</PageTitle>
102
+ {count !== undefined && <PageMeta>{count}</PageMeta>}
103
+ {description !== undefined && (
104
+ <PageDescription>{description}</PageDescription>
105
+ )}
106
+ {actions !== undefined && <PageActions>{actions}</PageActions>}
107
+ </PageHeader>
108
+ <PageBody>{children}</PageBody>
109
+ </>
110
+ ) : (
111
+ children
112
+ )}
113
+ </div>
114
+ </main>
115
+ </PageContext.Provider>
116
+ );
117
+ }
118
+
119
+ export function PageHeader({
120
+ className,
121
+ children,
122
+ ...props
123
+ }: HTMLAttributes<HTMLDivElement>): ReactElement {
124
+ requireParent(useContext(PageContext), "PageHeader", "Page");
125
+ const nodes = Children.toArray(children);
126
+ const titles = nodes.filter(
127
+ (node) => isValidElement(node) && node.type === PageTitle,
128
+ );
129
+ const metas = nodes.filter(
130
+ (node) => isValidElement(node) && node.type === PageMeta,
131
+ );
132
+ const descriptions = nodes.filter(
133
+ (node) => isValidElement(node) && node.type === PageDescription,
134
+ );
135
+ const actionSlots = nodes.filter(
136
+ (node) => isValidElement(node) && node.type === PageActions,
137
+ );
138
+ const recognized =
139
+ titles.length + metas.length + descriptions.length + actionSlots.length;
140
+ if (
141
+ titles.length !== 1 ||
142
+ metas.length > 1 ||
143
+ descriptions.length > 1 ||
144
+ actionSlots.length > 1 ||
145
+ recognized !== nodes.length
146
+ ) {
147
+ throw new Error(
148
+ "PageHeader exige um PageTitle e aceita no máximo um PageDescription, PageMeta e PageActions como filhos diretos.",
149
+ );
150
+ }
151
+
30
152
  return (
31
- <main data-slot="page" className="min-w-0 flex-1">
32
- <div className={cn('mx-auto max-w-6xl px-8 py-8', className)}>
33
- <div data-slot="page-header" className="mb-6">
34
- <ContentHeaderFrame
35
- title={title}
36
- meta={
37
- count === undefined ? undefined : (
38
- <span className="font-mono text-base text-muted-foreground/60">{count}</span>
39
- )
40
- }
41
- description={description}
42
- actions={actions}
43
- level={1}
44
- variant="page"
45
- slots={{ heading: 'page-heading', actions: 'page-actions' }}
46
- />
153
+ <PageHeaderContext.Provider value>
154
+ <div
155
+ data-slot="page-header"
156
+ className={cn(
157
+ "mb-6",
158
+ actionSlots.length > 0 &&
159
+ "flex flex-col items-start gap-4 sm:flex-row sm:items-end sm:justify-between",
160
+ className,
161
+ )}
162
+ {...props}
163
+ >
164
+ <div data-slot="page-heading" className="min-w-0">
165
+ <div className="flex items-baseline gap-2">
166
+ {titles}
167
+ {metas}
168
+ </div>
169
+ {descriptions}
47
170
  </div>
48
- {children}
171
+ {actionSlots}
49
172
  </div>
50
- </main>
51
- )
173
+ </PageHeaderContext.Provider>
174
+ );
175
+ }
176
+
177
+ export function PageTitle({
178
+ className,
179
+ ...props
180
+ }: HTMLAttributes<HTMLHeadingElement>): ReactElement {
181
+ requireParent(useContext(PageHeaderContext), "PageTitle", "PageHeader");
182
+ return (
183
+ <h1
184
+ data-slot="page-title"
185
+ className={cn("text-2xl font-semibold tracking-tight", className)}
186
+ {...props}
187
+ />
188
+ );
189
+ }
190
+
191
+ export function PageDescription({
192
+ className,
193
+ ...props
194
+ }: HTMLAttributes<HTMLParagraphElement>): ReactElement {
195
+ requireParent(useContext(PageHeaderContext), "PageDescription", "PageHeader");
196
+ return (
197
+ <p
198
+ data-slot="page-description"
199
+ className={cn("mt-1 truncate text-sm text-muted-foreground", className)}
200
+ {...props}
201
+ />
202
+ );
203
+ }
204
+
205
+ export function PageMeta({
206
+ className,
207
+ ...props
208
+ }: HTMLAttributes<HTMLSpanElement>): ReactElement {
209
+ requireParent(useContext(PageHeaderContext), "PageMeta", "PageHeader");
210
+ return (
211
+ <span
212
+ data-slot="page-meta"
213
+ className={cn("font-mono text-base text-muted-foreground/60", className)}
214
+ {...props}
215
+ />
216
+ );
217
+ }
218
+
219
+ export function PageActions({
220
+ className,
221
+ ...props
222
+ }: HTMLAttributes<HTMLDivElement>): ReactElement {
223
+ requireParent(useContext(PageHeaderContext), "PageActions", "PageHeader");
224
+ return (
225
+ <div
226
+ data-slot="page-actions"
227
+ className={cn(
228
+ "flex w-full shrink-0 items-center gap-4 sm:w-auto",
229
+ className,
230
+ )}
231
+ {...props}
232
+ />
233
+ );
234
+ }
235
+
236
+ export function PageBody({
237
+ className,
238
+ ...props
239
+ }: HTMLAttributes<HTMLDivElement>): ReactElement {
240
+ requireParent(useContext(PageContext), "PageBody", "Page");
241
+ return <div data-slot="page-body" className={className} {...props} />;
52
242
  }