@softize/opus 22.2.0 → 24.0.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 +120 -1
- package/bin/cli.mjs +3 -9
- package/bin/lib/check.mjs +242 -49
- package/bin/lib/copy.mjs +17 -35
- package/bin/lib/gen-runner.mjs +2 -0
- package/bin/lib/gen-stubs.mjs +3 -1
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/docs/code-style.md +1 -1
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/core/contracts.ts +1 -1
- package/src/core/runtime.ts +4 -37
- package/src/core/types.ts +5 -23
- package/src/design/index.ts +14 -3
- package/src/testing/index.ts +6 -26
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +64 -14
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +93 -70
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +58 -88
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +85 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/actions.md +6 -7
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +100 -58
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +34 -26
- package/src/ui/docs/content/pane.md +41 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/semantic-context.md +0 -1
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +82 -83
- package/src/ui/docs/content/testing.md +2 -3
- package/src/ui/docs/content/toggle.md +120 -63
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +4 -6
- package/src/ui/drivers/react.tsx +4 -3
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +42 -46
- package/src/ui/theme.css +311 -64
- package/src/vite/design.ts +23 -21
- package/src/ui/components/patterns/content-header.tsx +0 -532
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- package/src/ui/docs/content/content.md +0 -276
|
@@ -15,19 +15,30 @@ exemplos leem as variáveis ao vivo; troque o tema do app e a página acompanha.
|
|
|
15
15
|
|
|
16
16
|
```tsx preview
|
|
17
17
|
const SURFACES = [
|
|
18
|
-
[
|
|
19
|
-
[
|
|
20
|
-
[
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
[
|
|
26
|
-
|
|
18
|
+
["background", "foreground", "O fundo da página e o texto padrão."],
|
|
19
|
+
["card", "card-foreground", "Superfície elevada (Surface, painéis)."],
|
|
20
|
+
[
|
|
21
|
+
"popover",
|
|
22
|
+
"popover-foreground",
|
|
23
|
+
"Superfície flutuante (menus, popovers, dialogs).",
|
|
24
|
+
],
|
|
25
|
+
["muted", "muted-foreground", "Fundo discreto e texto secundário."],
|
|
26
|
+
[
|
|
27
|
+
"accent",
|
|
28
|
+
"accent-foreground",
|
|
29
|
+
"Alias de compatibilidade para hover neutro.",
|
|
30
|
+
],
|
|
31
|
+
["secondary", "secondary-foreground", "Ação secundária."],
|
|
32
|
+
["primary", "primary-foreground", "A ação principal — alto contraste."],
|
|
33
|
+
["destructive", "destructive-foreground", "Ação destrutiva e erro."],
|
|
34
|
+
];
|
|
27
35
|
render(
|
|
28
36
|
<div className="w-full overflow-hidden rounded-lg border border-border">
|
|
29
37
|
{SURFACES.map(([token, fg, desc]) => (
|
|
30
|
-
<div
|
|
38
|
+
<div
|
|
39
|
+
key={token}
|
|
40
|
+
className="flex items-center gap-4 border-b border-border px-4 py-3 last:border-b-0"
|
|
41
|
+
>
|
|
31
42
|
<div
|
|
32
43
|
className="flex h-10 w-28 shrink-0 items-center justify-center rounded-md border border-border text-xs font-medium"
|
|
33
44
|
style={{ backgroundColor: `var(--${token})`, color: `var(--${fg})` }}
|
|
@@ -35,15 +46,75 @@ render(
|
|
|
35
46
|
Texto
|
|
36
47
|
</div>
|
|
37
48
|
<div className="min-w-0">
|
|
38
|
-
<div className="font-mono text-xs"
|
|
49
|
+
<div className="font-mono text-xs">
|
|
50
|
+
--{token} · --{fg}
|
|
51
|
+
</div>
|
|
39
52
|
<div className="text-xs text-muted-foreground">{desc}</div>
|
|
40
53
|
</div>
|
|
41
54
|
</div>
|
|
42
55
|
))}
|
|
43
56
|
</div>,
|
|
44
|
-
)
|
|
57
|
+
);
|
|
45
58
|
```
|
|
46
59
|
|
|
60
|
+
### Controles e estados neutros
|
|
61
|
+
|
|
62
|
+
Canvas, superfície elevada, controle e interação formam uma escala opaca. Um input não muda de
|
|
63
|
+
tom ao sair do canvas e entrar num card, porque `control` representa o resultado final — não uma
|
|
64
|
+
cor translúcida a ser misturada com o ancestral. Da mesma forma, hover, seleção e pressão usam
|
|
65
|
+
papéis diferentes e preservam a mesma ordem comportamental nos dois temas.
|
|
66
|
+
|
|
67
|
+
```tsx preview
|
|
68
|
+
const NEUTRAL_STATES = [
|
|
69
|
+
["control", "Controle em repouso."],
|
|
70
|
+
["interaction-hover", "Resposta transitória ao apontador ou foco de item."],
|
|
71
|
+
["interaction-selected", "Seleção ou expansão persistente."],
|
|
72
|
+
["interaction-pressed", "Pressão transitória de um controle neutro."],
|
|
73
|
+
];
|
|
74
|
+
render(
|
|
75
|
+
<div className="grid w-full grid-cols-2 gap-2">
|
|
76
|
+
{NEUTRAL_STATES.map(([token, desc]) => (
|
|
77
|
+
<div
|
|
78
|
+
key={token}
|
|
79
|
+
className="rounded-md border border-border p-3"
|
|
80
|
+
style={{ backgroundColor: `var(--${token})` }}
|
|
81
|
+
>
|
|
82
|
+
<div className="font-mono text-xs">--{token}</div>
|
|
83
|
+
<div className="mt-1 text-xs text-muted-foreground">{desc}</div>
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
86
|
+
</div>,
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Ajuste de aparência
|
|
91
|
+
|
|
92
|
+
Os tokens neutros desta seção não são valores fixos: o tema os calcula a partir de quatro eixos,
|
|
93
|
+
declarados separadamente para o modo claro e para o modo escuro. Com os eixos no preset, o
|
|
94
|
+
resultado é o tema padrão. Uma aplicação que oferece preferências de aparência altera os eixos, e
|
|
95
|
+
toda a escala — superfícies, estados, bordas e texto secundário — se move em conjunto, sem perder a
|
|
96
|
+
ordem entre os papéis.
|
|
97
|
+
|
|
98
|
+
| Eixo | Custom property (`light` ou `dark`) | O que muda | Preset claro | Preset escuro |
|
|
99
|
+
| ----------------------- | ---------------------------------------- | ------------------------------------------------------ | ------------ | ------------- |
|
|
100
|
+
| Intensidade de cor | `--appearance-<modo>-color-intensity` | Quanto do matiz `--neutral-hue` aparece nos neutros | 0 | 0 |
|
|
101
|
+
| Contraste | `--appearance-<modo>-contrast` | Distância de hover, seleção, pressão, `muted` e bordas | 0 | 0 |
|
|
102
|
+
| Brilho das superfícies | `--appearance-<modo>-surface-brightness` | Luminosidade do canvas e das superfícies | 100 | 50 |
|
|
103
|
+
| Contraste entre camadas | `--appearance-<modo>-layer-contrast` | Distância entre canvas, controle e superfície elevada | 0 | 0 |
|
|
104
|
+
|
|
105
|
+
Cada eixo aceita um inteiro de 0 a 100. Os limites protegem o tema padrão: nenhum valor dessa faixa
|
|
106
|
+
reduz a separação entre os papéis nem o contraste do texto sobre as superfícies em repouso abaixo do
|
|
107
|
+
que o preset oferece. Por isso contraste e contraste entre camadas partem de zero e só aumentam, e o
|
|
108
|
+
texto secundário acompanha as superfícies quando o brilho muda.
|
|
109
|
+
|
|
110
|
+
A intensidade de cor tinge somente os neutros. `primary`, `destructive` e os contextos `info`,
|
|
111
|
+
`success`, `warning` e `danger` mantêm as próprias cores. O matiz aplicado vem de `--neutral-hue`,
|
|
112
|
+
que a aplicação define junto com a identidade.
|
|
113
|
+
|
|
114
|
+
Os nomes, os presets e a conversão de valores em custom properties ficam em
|
|
115
|
+
`@softize/opus/ui/appearance`. O passo a passo para aplicar e desfazer um ajuste está em
|
|
116
|
+
[Customização](/ui/customization). A ADR 0044 registra a decisão e os limites.
|
|
117
|
+
|
|
47
118
|
## Famílias contextuais
|
|
48
119
|
|
|
49
120
|
Os tokens `primary`, `secondary` e `destructive` acima permanecem fundações de superfície. A API
|
|
@@ -52,7 +123,7 @@ família oferece sólido, foreground, superfície sutil, ênfase e borda. Use as
|
|
|
52
123
|
`variant`; classes contextuais diretas ficam reservadas à implementação dos primitives.
|
|
53
124
|
|
|
54
125
|
```tsx preview
|
|
55
|
-
const CONTEXTS = [
|
|
126
|
+
const CONTEXTS = ["neutral", "primary", "info", "success", "warning", "danger"];
|
|
56
127
|
render(
|
|
57
128
|
<div className="grid w-full grid-cols-3 gap-2">
|
|
58
129
|
{CONTEXTS.map((context) => (
|
|
@@ -69,7 +140,7 @@ render(
|
|
|
69
140
|
</div>
|
|
70
141
|
))}
|
|
71
142
|
</div>,
|
|
72
|
-
)
|
|
143
|
+
);
|
|
73
144
|
```
|
|
74
145
|
|
|
75
146
|
## Linhas e foco
|
|
@@ -79,15 +150,21 @@ render(
|
|
|
79
150
|
|
|
80
151
|
```tsx preview
|
|
81
152
|
const LINES = [
|
|
82
|
-
[
|
|
83
|
-
[
|
|
84
|
-
[
|
|
85
|
-
]
|
|
153
|
+
["border", "Bordas de contêineres (cards, tabelas, separadores)."],
|
|
154
|
+
["input", "Borda de campos de formulário; o fundo usa control."],
|
|
155
|
+
["ring", "O anel de foco (focus-visible)."],
|
|
156
|
+
];
|
|
86
157
|
render(
|
|
87
158
|
<div className="w-full overflow-hidden rounded-lg border border-border">
|
|
88
159
|
{LINES.map(([token, desc]) => (
|
|
89
|
-
<div
|
|
90
|
-
|
|
160
|
+
<div
|
|
161
|
+
key={token}
|
|
162
|
+
className="flex items-center gap-4 border-b border-border px-4 py-3 last:border-b-0"
|
|
163
|
+
>
|
|
164
|
+
<div
|
|
165
|
+
className="h-10 w-28 shrink-0 rounded-md border-2"
|
|
166
|
+
style={{ borderColor: `var(--${token})` }}
|
|
167
|
+
/>
|
|
91
168
|
<div className="min-w-0">
|
|
92
169
|
<div className="font-mono text-xs">--{token}</div>
|
|
93
170
|
<div className="text-xs text-muted-foreground">{desc}</div>
|
|
@@ -95,7 +172,7 @@ render(
|
|
|
95
172
|
</div>
|
|
96
173
|
))}
|
|
97
174
|
</div>,
|
|
98
|
-
)
|
|
175
|
+
);
|
|
99
176
|
```
|
|
100
177
|
|
|
101
178
|
## Escala de elevação
|
|
@@ -107,16 +184,19 @@ render(
|
|
|
107
184
|
|
|
108
185
|
```tsx preview
|
|
109
186
|
const LEVELS = [
|
|
110
|
-
[
|
|
111
|
-
[
|
|
112
|
-
[
|
|
113
|
-
]
|
|
187
|
+
["sm", "Superfície em repouso."],
|
|
188
|
+
["md", "Flutuante intermediário."],
|
|
189
|
+
["lg", "Modal ou destaque forte."],
|
|
190
|
+
];
|
|
114
191
|
render(
|
|
115
192
|
<div className="flex flex-wrap gap-8 py-4">
|
|
116
193
|
{LEVELS.map(([level, desc]) => (
|
|
117
194
|
<div key={level} className="flex flex-col items-center gap-3">
|
|
118
195
|
<div
|
|
119
|
-
className={
|
|
196
|
+
className={
|
|
197
|
+
"flex h-24 w-40 items-center justify-center rounded-xl border border-border bg-card text-xs text-card-foreground shadow-" +
|
|
198
|
+
level
|
|
199
|
+
}
|
|
120
200
|
>
|
|
121
201
|
Texto
|
|
122
202
|
</div>
|
|
@@ -127,7 +207,7 @@ render(
|
|
|
127
207
|
</div>
|
|
128
208
|
))}
|
|
129
209
|
</div>,
|
|
130
|
-
)
|
|
210
|
+
);
|
|
131
211
|
```
|
|
132
212
|
|
|
133
213
|
## Escala relativa
|
|
@@ -172,7 +252,7 @@ render(
|
|
|
172
252
|
</p>
|
|
173
253
|
</div>
|
|
174
254
|
</div>,
|
|
175
|
-
)
|
|
255
|
+
);
|
|
176
256
|
```
|
|
177
257
|
|
|
178
258
|
Alterar `font-size` em `html` permanece válido quando a aplicação quer recalibrar **toda** a
|
|
@@ -190,15 +270,17 @@ o mecanismo para fazer `text-sm` se chamar `text-base`.
|
|
|
190
270
|
render(
|
|
191
271
|
<div className="flex flex-wrap gap-4">
|
|
192
272
|
{[
|
|
193
|
-
[
|
|
194
|
-
[
|
|
195
|
-
[
|
|
196
|
-
[
|
|
197
|
-
[
|
|
198
|
-
[
|
|
273
|
+
["rounded-xs", "radius − 0.375rem"],
|
|
274
|
+
["rounded-sm", "radius − 0.25rem"],
|
|
275
|
+
["rounded-md", "radius − 0.125rem"],
|
|
276
|
+
["rounded-lg", "radius (0.75rem)"],
|
|
277
|
+
["rounded-xl", "radius + 0.25rem"],
|
|
278
|
+
["rounded-2xl", "radius + 0.5rem"],
|
|
199
279
|
].map(([cls, calc]) => (
|
|
200
280
|
<div key={cls} className="flex items-center gap-3">
|
|
201
|
-
<div
|
|
281
|
+
<div
|
|
282
|
+
className={"h-12 w-12 border-2 border-foreground/30 bg-muted " + cls}
|
|
283
|
+
/>
|
|
202
284
|
<div>
|
|
203
285
|
<div className="font-mono text-xs">{cls}</div>
|
|
204
286
|
<div className="text-xs text-muted-foreground">{calc}</div>
|
|
@@ -206,7 +288,7 @@ render(
|
|
|
206
288
|
</div>
|
|
207
289
|
))}
|
|
208
290
|
</div>,
|
|
209
|
-
)
|
|
291
|
+
);
|
|
210
292
|
```
|
|
211
293
|
|
|
212
294
|
## Divergências declaradas vs shadcn
|
|
@@ -214,10 +296,12 @@ render(
|
|
|
214
296
|
> Qualquer diferença em relação ao shadcn que não esteja declarada aqui (e no `theme.css`) é uma
|
|
215
297
|
> divergência a sincronizar, conforme a skill `build-opus-ui`.
|
|
216
298
|
|
|
217
|
-
1. Formato HSL nos tokens (o upstream migrou para oklch), escolhido pela legibilidade
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
299
|
+
1. Formato HSL nos tokens (o upstream migrou para oklch), escolhido pela legibilidade.
|
|
300
|
+
2. Hierarquia opaca de superfícies e estados nos dois temas: canvas, card/popover, controle, hover,
|
|
301
|
+
seleção e pressão têm papéis próprios. No light, o canvas quase branco sustenta superfícies
|
|
302
|
+
brancas; no dark, cards e popovers permanecem acima do canvas. A ADR 0043 registra os valores e
|
|
303
|
+
as restrições de composição. A escala neutra é derivada dos eixos de aparência (ADR 0044); no
|
|
304
|
+
preset, os valores são os calibrados.
|
|
221
305
|
3. Radius mapeado (`xs` a `2xl`) preservando o sentido pré-v4 dos utilitários `rounded-*`. Esse
|
|
222
306
|
trecho derivado permanece monotônico quando a base muda; aplicações que também usam `3xl`
|
|
223
307
|
precisam revisar a transição para esse degrau nativo ao customizar `--radius`.
|
package/src/ui/docs/registry.tsx
CHANGED
|
@@ -61,7 +61,6 @@ import dockMd from "./content/dock.md?raw";
|
|
|
61
61
|
import breadcrumbMd from "./content/breadcrumb.md?raw";
|
|
62
62
|
import buttonMd from "./content/button.md?raw";
|
|
63
63
|
import calendarMd from "./content/calendar.md?raw";
|
|
64
|
-
import cardMd from "./content/card.md?raw";
|
|
65
64
|
import metricCardMd from "./content/metric-card.md?raw";
|
|
66
65
|
import carouselMd from "./content/carousel.md?raw";
|
|
67
66
|
import askMd from "./content/ask.md?raw";
|
|
@@ -125,7 +124,7 @@ import actionTriggerMd from "./content/action-trigger.md?raw";
|
|
|
125
124
|
import actionSearchDialogMd from "./content/action-list-dialog.md?raw";
|
|
126
125
|
import pageMd from "./content/page.md?raw";
|
|
127
126
|
import presentationMd from "./content/presentation.md?raw";
|
|
128
|
-
import
|
|
127
|
+
import surfaceMd from "./content/surface.md?raw";
|
|
129
128
|
import paneMd from "./content/pane.md?raw";
|
|
130
129
|
import sidebarMd from "./content/sidebar.md?raw";
|
|
131
130
|
import splitMd from "./content/split.md?raw";
|
|
@@ -454,9 +453,9 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
454
453
|
),
|
|
455
454
|
},
|
|
456
455
|
{
|
|
457
|
-
slug: "
|
|
458
|
-
title: "
|
|
459
|
-
render: pattern("
|
|
456
|
+
slug: "surface",
|
|
457
|
+
title: "Surface",
|
|
458
|
+
render: pattern("Surface", "surface", surfaceMd),
|
|
460
459
|
},
|
|
461
460
|
{
|
|
462
461
|
slug: "accordion",
|
|
@@ -468,7 +467,6 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
468
467
|
title: "Aspect Ratio",
|
|
469
468
|
render: comp("Aspect Ratio", "aspect-ratio", aspectRatioMd),
|
|
470
469
|
},
|
|
471
|
-
{ slug: "card", title: "Card", render: comp("Card", "card", cardMd) },
|
|
472
470
|
{
|
|
473
471
|
slug: "metric-card",
|
|
474
472
|
title: "Metric Card",
|
package/src/ui/drivers/react.tsx
CHANGED
|
@@ -51,11 +51,12 @@ import { objectSchemaShape } from '../lib/object-schema.ts'
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Dicionário estrutural pro resolvedor de `options: { kind: 'dictionary', ref }`:
|
|
54
|
-
* qualquer coisa com `options()` → `{ value, label }`. O `DictType` do `t.dict`
|
|
55
|
-
* satisfaz direto — sem este driver importar o driver de schema.
|
|
54
|
+
* qualquer coisa com `options()` → `{ value, label, icon? }`. O `DictType` do `t.dict`
|
|
55
|
+
* satisfaz direto — sem este driver importar o driver de schema. A metadata opcional
|
|
56
|
+
* continua sendo declarada pelo dicionário; o provider não inventa apresentação.
|
|
56
57
|
*/
|
|
57
58
|
export interface DictLike {
|
|
58
|
-
options(): Array<{ value: string; label: string }>
|
|
59
|
+
options(): Array<{ value: string; label: string; icon?: string }>
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
interface OpusContextValue {
|
package/src/ui/index.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* Aqui ficam apenas types/contratos compartilhados.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ActionError, Paginated } from
|
|
8
|
+
import type { ActionError, Paginated } from "../core/index.ts";
|
|
9
|
+
|
|
10
|
+
// Personalização de aparência (ADR 0044): sem React, também disponível em `./ui/appearance`.
|
|
11
|
+
export * from "./appearance.ts";
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Metadado de descoberta de um componente — cada componente co-localiza um `meta`
|
|
@@ -13,18 +16,18 @@ import type { ActionError, Paginated } from '../core/index.ts'
|
|
|
13
16
|
*/
|
|
14
17
|
export interface ComponentMeta {
|
|
15
18
|
/** Nome canônico (kebab), ex.: 'button', 'dropdown-menu'. */
|
|
16
|
-
name: string
|
|
19
|
+
name: string;
|
|
17
20
|
/** Origem: descendente do shadcn (forkado) ou nativo do Opus. */
|
|
18
|
-
ancestry:
|
|
21
|
+
ancestry: "shadcn" | "opus";
|
|
19
22
|
/** Quando usar / quando não — guia o agente de design a escolher certo. */
|
|
20
|
-
whenToUse: string
|
|
23
|
+
whenToUse: string;
|
|
21
24
|
/** Aviso de retirada no catálogo, com o caminho de migração. */
|
|
22
25
|
deprecated?: {
|
|
23
26
|
/** Alternativa recomendada; aceita `código` inline como `whenToUse`. */
|
|
24
|
-
alternative: string
|
|
27
|
+
alternative: string;
|
|
25
28
|
/** Versão em que a retirada começou, quando houver. */
|
|
26
|
-
since?: string
|
|
27
|
-
}
|
|
29
|
+
since?: string;
|
|
30
|
+
};
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -32,23 +35,23 @@ export interface ComponentMeta {
|
|
|
32
35
|
* shape derivado disto.
|
|
33
36
|
*/
|
|
34
37
|
export interface ActionState<TData> {
|
|
35
|
-
data: TData | undefined
|
|
36
|
-
error: ActionError | undefined
|
|
37
|
-
isIdle: boolean
|
|
38
|
-
isLoading: boolean
|
|
39
|
-
isSuccess: boolean
|
|
40
|
-
isError: boolean
|
|
38
|
+
data: TData | undefined;
|
|
39
|
+
error: ActionError | undefined;
|
|
40
|
+
isIdle: boolean;
|
|
41
|
+
isLoading: boolean;
|
|
42
|
+
isSuccess: boolean;
|
|
43
|
+
isError: boolean;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export interface ListState<TItem> {
|
|
44
|
-
items: TItem[]
|
|
45
|
-
cursor: { next: string | null; prev?: string | null }
|
|
46
|
-
total: number | undefined
|
|
47
|
-
error: ActionError | undefined
|
|
48
|
-
isIdle: boolean
|
|
49
|
-
isLoading: boolean
|
|
50
|
-
isSuccess: boolean
|
|
51
|
-
isError: boolean
|
|
47
|
+
items: TItem[];
|
|
48
|
+
cursor: { next: string | null; prev?: string | null };
|
|
49
|
+
total: number | undefined;
|
|
50
|
+
error: ActionError | undefined;
|
|
51
|
+
isIdle: boolean;
|
|
52
|
+
isLoading: boolean;
|
|
53
|
+
isSuccess: boolean;
|
|
54
|
+
isError: boolean;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
/**
|
|
@@ -62,7 +65,7 @@ export function idleAction<T>(): ActionState<T> {
|
|
|
62
65
|
isLoading: false,
|
|
63
66
|
isSuccess: false,
|
|
64
67
|
isError: false,
|
|
65
|
-
}
|
|
68
|
+
};
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export function idleLookup<T>(): ListState<T> {
|
|
@@ -75,7 +78,7 @@ export function idleLookup<T>(): ListState<T> {
|
|
|
75
78
|
isLoading: false,
|
|
76
79
|
isSuccess: false,
|
|
77
80
|
isError: false,
|
|
78
|
-
}
|
|
81
|
+
};
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
/**
|
|
@@ -83,10 +86,10 @@ export function idleLookup<T>(): ListState<T> {
|
|
|
83
86
|
*/
|
|
84
87
|
export function isPaginated(value: unknown): value is Paginated<unknown> {
|
|
85
88
|
return (
|
|
86
|
-
typeof value ===
|
|
89
|
+
typeof value === "object" &&
|
|
87
90
|
value !== null &&
|
|
88
|
-
|
|
91
|
+
"items" in value &&
|
|
89
92
|
Array.isArray((value as { items: unknown }).items) &&
|
|
90
|
-
|
|
91
|
-
)
|
|
93
|
+
"cursor" in value
|
|
94
|
+
);
|
|
92
95
|
}
|
package/src/ui/meta.ts
CHANGED
|
@@ -11,11 +11,11 @@ import type { ComponentMeta } from "./index.ts";
|
|
|
11
11
|
|
|
12
12
|
/** Todos os metas, chaveados pelo nome canônico (kebab). Fonte única — zero duplicação. */
|
|
13
13
|
export const componentMeta = {
|
|
14
|
-
|
|
15
|
-
name: "
|
|
14
|
+
surface: {
|
|
15
|
+
name: "surface",
|
|
16
16
|
ancestry: "opus",
|
|
17
17
|
whenToUse:
|
|
18
|
-
|
|
18
|
+
'Delimite e nomeie um trecho da tela: uma região da página, um grupo de campos, um registro, um item de grade ou um bloco clicável. `as` escolhe o elemento (`div`, `section`, `article` ou `fieldset`) e `asChild` entrega a superfície a um botão ou link. `framed` acrescenta a moldura, `elevation="raised"` dá fundo próprio e `layout` escolhe entre ritmo contínuo e faixas com divisores. No caso comum, declare `title`, `description`, `count`, `actions` e `media` na própria Surface; componha SurfaceHeader, SurfaceBody e SurfaceFooter quando precisar controlar a estrutura. Numa página centrada em uma coleção, `level={1}` faz da Surface o heading principal; nas demais, ele vem de PageTitle. Para um modal use Dialog, para um painel lateral use Drawer e para uma coluna de layout use Pane.',
|
|
19
19
|
},
|
|
20
20
|
ask: {
|
|
21
21
|
name: "ask",
|
|
@@ -77,17 +77,11 @@ export const componentMeta = {
|
|
|
77
77
|
whenToUse:
|
|
78
78
|
"Inicie uma ação com um controle clicável. Use `context` para o significado, `variant` para o tratamento visual e `size` para a escala compartilhada (`xs`…`lg` e os quadrados `icon-*`); `icon` recebe um nó e `busy` comunica o andamento e impede um novo acionamento. Para ações relacionadas, use ButtonGroup conectado, espaçado ou dividido.",
|
|
79
79
|
},
|
|
80
|
-
card: {
|
|
81
|
-
name: "card",
|
|
82
|
-
ancestry: "shadcn",
|
|
83
|
-
whenToUse:
|
|
84
|
-
"Agrupe conteúdo relacionado em uma superfície delimitada. Use Card diretamente para um bloco simples ou componha cabeçalho, corpo e rodapé quando houver hierarquia e ações. CardContent permanece apenas como alias temporário de compatibilidade.",
|
|
85
|
-
},
|
|
86
80
|
"metric-card": {
|
|
87
81
|
name: "metric-card",
|
|
88
82
|
ancestry: "opus",
|
|
89
83
|
whenToUse:
|
|
90
|
-
"Apresente uma medida resumida, como um total ou uma contagem, com rótulo, valor em destaque e descrição opcional. O consumidor calcula e formata os valores. Use `secondaryMetric` para uma segunda leitura da mesma dimensão, exibida como lâmina posterior com rótulo próprio; `context` realça somente o ícone, `action` acrescenta uma ação diretamente relacionada e `loading` substitui rótulo e valor durante a carga. Com `onActivate`, `active` e `activationLabel`, informados juntos, o card vira um controle que aplica o recorte que a medida resume e expõe o estado por `aria-pressed`. Para conteúdo geral ou estrutura livre, use
|
|
84
|
+
"Apresente uma medida resumida, como um total ou uma contagem, com rótulo, valor em destaque e descrição opcional. O consumidor calcula e formata os valores. Use `secondaryMetric` para uma segunda leitura da mesma dimensão, exibida como lâmina posterior com rótulo próprio; `context` realça somente o ícone, `action` acrescenta uma ação diretamente relacionada e `loading` substitui rótulo e valor durante a carga. Com `onActivate`, `active` e `activationLabel`, informados juntos, o card vira um controle que aplica o recorte que a medida resume e expõe o estado por `aria-pressed`. Para conteúdo geral ou estrutura livre, use Surface.",
|
|
91
85
|
},
|
|
92
86
|
chat: {
|
|
93
87
|
name: "chat",
|
|
@@ -119,12 +113,6 @@ export const componentMeta = {
|
|
|
119
113
|
whenToUse:
|
|
120
114
|
"Receba texto para envio sem montar uma conversa completa. Enter envia, Shift+Enter quebra a linha e `actions` acrescenta uma faixa para seletores relacionados. O componente é controlado; use Chat quando também precisar de mensagens e estado da conversa.",
|
|
121
115
|
},
|
|
122
|
-
"content-header": {
|
|
123
|
-
name: "content-header",
|
|
124
|
-
ancestry: "opus",
|
|
125
|
-
whenToUse:
|
|
126
|
-
'Estruture uma região com título e corpo. No caso comum, declare título, mídia, descrição, contador e ações diretamente em Content; use `framed` quando a região precisar de limite próprio, `layout="sectioned"` quando o filho controlar o recuo do body, `frameVariant="legend"` para integrar visualmente o header à borda e `as="fieldset"` somente para agrupar campos sob uma legenda nativa. Componha ContentHeader apenas quando precisar controlar a anatomia.',
|
|
127
|
-
},
|
|
128
116
|
copyable: {
|
|
129
117
|
name: "copyable",
|
|
130
118
|
ancestry: "opus",
|
|
@@ -177,7 +165,7 @@ export const componentMeta = {
|
|
|
177
165
|
name: "separator",
|
|
178
166
|
ancestry: "shadcn",
|
|
179
167
|
whenToUse:
|
|
180
|
-
'Marque a fronteira entre blocos de conteúdo que não pertencem a uma superfície com regiões. A linha horizontal fica entre blocos empilhados; com `orientation="vertical"`, ela separa itens lado a lado e herda a altura do contêiner. A linha é decorativa por padrão e fica fora da árvore de acessibilidade; use `decorative={false}` quando a divisão tiver significado. Não use Separator entre cabeçalho, corpo e rodapé de
|
|
168
|
+
'Marque a fronteira entre blocos de conteúdo que não pertencem a uma superfície com regiões. A linha horizontal fica entre blocos empilhados; com `orientation="vertical"`, ela separa itens lado a lado e herda a altura do contêiner. A linha é decorativa por padrão e fica fora da árvore de acessibilidade; use `decorative={false}` quando a divisão tiver significado. Não use Separator entre cabeçalho, corpo e rodapé de Surface, Dialog ou Drawer: nessas superfícies, os divisores são derivados das regiões com `layout="sectioned"`.',
|
|
181
169
|
},
|
|
182
170
|
skeleton: {
|
|
183
171
|
name: "skeleton",
|
|
@@ -201,7 +189,7 @@ export const componentMeta = {
|
|
|
201
189
|
name: "tabs",
|
|
202
190
|
ancestry: "shadcn",
|
|
203
191
|
whenToUse:
|
|
204
|
-
|
|
192
|
+
"Navegue entre painéis relacionados no mesmo nível. Cada TabsListItem aponta para um TabsPanel pelo mesmo `value`; TabsList usa uma linha para indicar a aba ativa, enquanto Tabs define orientação, escala, distribuição e moldura. Use ToggleGroup para escolher valores sem navegar entre painéis.",
|
|
205
193
|
},
|
|
206
194
|
textarea: {
|
|
207
195
|
name: "textarea",
|
|
@@ -285,7 +273,7 @@ export const componentMeta = {
|
|
|
285
273
|
name: "field",
|
|
286
274
|
ancestry: "shadcn",
|
|
287
275
|
whenToUse:
|
|
288
|
-
'Componha rótulo, controle, ajuda e erro com espaçamento consistente. Use a orientação vertical no caso comum e a horizontal quando controle e texto precisarem ficar lado a lado. FieldGroup organiza vários campos; quando o conjunto precisar de título, componha-o dentro de
|
|
276
|
+
'Componha rótulo, controle, ajuda e erro com espaçamento consistente. Use a orientação vertical no caso comum e a horizontal quando controle e texto precisarem ficar lado a lado. FieldGroup organiza vários campos; quando o conjunto precisar de título, componha-o dentro de Surface com `as="fieldset"`.',
|
|
289
277
|
},
|
|
290
278
|
"input-otp": {
|
|
291
279
|
name: "input-otp",
|
|
@@ -327,19 +315,19 @@ export const componentMeta = {
|
|
|
327
315
|
name: "split",
|
|
328
316
|
ancestry: "opus",
|
|
329
317
|
whenToUse:
|
|
330
|
-
"Divide uma área em panes em sequência horizontal ou vertical. Use `resizable` quando a pessoa deve ajustar a fronteira; o mesmo `<Split>` vira flex simples sem ele. Cada `<Pane>` declara
|
|
318
|
+
"Divide uma área em panes em sequência horizontal ou vertical. Use `resizable` quando a pessoa deve ajustar a fronteira; o mesmo `<Split>` vira flex simples sem ele. Cada `<Pane>` declara apenas dimensão e crescimento; o filho compõe apresentação e rolagem. SurfaceAssistant especializa esse layout para assistência contextual dentro de Page, Dialog ou Drawer.",
|
|
331
319
|
},
|
|
332
320
|
sidebar: {
|
|
333
321
|
name: "sidebar",
|
|
334
322
|
ancestry: "opus",
|
|
335
323
|
whenToUse:
|
|
336
|
-
"Organize navegação global ou contextual em uma coluna lateral. Split e Pane definem posição e largura; Sidebar fornece a
|
|
324
|
+
"Organize navegação global ou contextual em uma coluna lateral. Split e Pane definem posição e largura; Sidebar fornece a coluna e o colapso. Use SidebarNav para grupos planos e componha árvores com SidebarItem e SidebarTreeGroup; quando precisar de header, body ou footer visuais, envolva a Sidebar com Surface.",
|
|
337
325
|
},
|
|
338
326
|
pane: {
|
|
339
327
|
name: "pane",
|
|
340
328
|
ancestry: "opus",
|
|
341
329
|
whenToUse:
|
|
342
|
-
"
|
|
330
|
+
"Declare o tamanho de uma região dentro de Split. Pane cuida somente de dimensão, limites e crescimento; o filho decide superfície, organização e rolagem. Use Surface quando a região precisar de header, body, footer, borda ou elevação, e Sidebar quando ela for uma navegação lateral.",
|
|
343
331
|
},
|
|
344
332
|
"scroll-area": {
|
|
345
333
|
name: "scroll-area",
|
|
@@ -363,7 +351,7 @@ export const componentMeta = {
|
|
|
363
351
|
name: "toggle",
|
|
364
352
|
ancestry: "shadcn",
|
|
365
353
|
whenToUse:
|
|
366
|
-
|
|
354
|
+
'Alterne um estado diretamente no contexto atual. ToggleGroup reúne escolhas únicas ou múltiplas com a mesma gramática estrutural de ButtonGroup; `mode="segmented"` mantém alternativas curtas visíveis numa superfície compartilhada, enquanto `variant` trata cada Toggle. Para navegar entre painéis, use Tabs.',
|
|
367
355
|
},
|
|
368
356
|
|
|
369
357
|
confirm: {
|
|
@@ -388,13 +376,13 @@ export const componentMeta = {
|
|
|
388
376
|
name: "action-form-card",
|
|
389
377
|
ancestry: "opus",
|
|
390
378
|
whenToUse:
|
|
391
|
-
"Apresente uma FormAction como seção delimitada da página. ActionFormCard combina a estrutura
|
|
379
|
+
"Apresente uma FormAction como seção delimitada da página. ActionFormCard combina a estrutura de uma Surface elevada com o comportamento de ActionForm. Para um modal, use ActionFormDialog; sem superfície adicional, use ActionForm.",
|
|
392
380
|
},
|
|
393
381
|
"action-list": {
|
|
394
382
|
name: "action-list",
|
|
395
383
|
ancestry: "opus",
|
|
396
384
|
whenToUse:
|
|
397
|
-
"Apresente uma coleção pesquisável a partir de uma ListAction. O contrato declara colunas, filtros, período e busca; ActionList coordena visualizações, paginação, seleção em lote e sincronização com a URL. A tabela derivada não cria moldura própria. Em uma página, envolva a lista com
|
|
385
|
+
"Apresente uma coleção pesquisável a partir de uma ListAction. O contrato declara colunas, filtros, período e busca; ActionList coordena visualizações, paginação, seleção em lote e sincronização com a URL. A tabela derivada não cria moldura própria. Em uma página, envolva a lista com Surface para manter título e criação junto da coleção. Para um único recurso, use ActionView.",
|
|
398
386
|
},
|
|
399
387
|
"action-trigger": {
|
|
400
388
|
name: "action-trigger",
|
|
@@ -412,13 +400,13 @@ export const componentMeta = {
|
|
|
412
400
|
name: "page",
|
|
413
401
|
ancestry: "opus",
|
|
414
402
|
whenToUse:
|
|
415
|
-
"Estruture uma página com cabeçalho, estado e conteúdo na mesma anatomia. Page renderiza o `<main>` com um container centralizado de até 80rem (`max-w-7xl`); PageHeader reúne navegação contextual opcional, título e ações, enquanto PageBody recebe o conteúdo. PageShell pode fornecer navegação e ações conhecidas pela aplicação sem criar uma barra visual. Uma região interna pode fornecer o heading principal com
|
|
403
|
+
"Estruture uma página com cabeçalho, estado e conteúdo na mesma anatomia. Page renderiza o `<main>` com um container centralizado de até 80rem (`max-w-7xl`); PageHeader reúne navegação contextual opcional, título e ações, enquanto PageBody recebe o conteúdo. PageShell pode fornecer navegação e ações conhecidas pela aplicação sem criar uma barra visual. Uma região interna pode fornecer o heading principal com `Surface level={1}`. PageActionsTarget fica restrito a workspaces imersivos. PageState substitui o header e o conteúdo durante carregamento, falha ou ausência, por isso informações indispensáveis entram no corpo, como Alert ou texto. Para uma falha parcial, mantenha o estado na seção com DataState; para uma região disponível para criação ou vínculo, use Empty.",
|
|
416
404
|
},
|
|
417
405
|
presentation: {
|
|
418
406
|
name: "presentation",
|
|
419
407
|
ancestry: "opus",
|
|
420
408
|
whenToUse:
|
|
421
|
-
"Descreva um recurso que precisa manter a mesma anatomia ao aparecer como Page, Dialog ou Drawer. Presentation organiza navegação, título, ações de cabeçalho, body e ações de rodapé sem redefinir o conteúdo para cada superfície. Em Page de listagem,
|
|
409
|
+
"Descreva um recurso que precisa manter a mesma anatomia ao aparecer como Page, Dialog ou Drawer. Presentation organiza navegação, título, ações de cabeçalho, body e ações de rodapé sem redefinir o conteúdo para cada superfície. Em Page de listagem, Surface mantém título e ações junto do ActionList; modais usam o header da superfície. Route pode declarar URL, surface e origem do registro para deep links. A definição persistente liga esses slots a actions Opus; a invocação carrega o estado JSON da execução. PresentationInspector abre um snapshot isolado, enquanto a Lens apresenta o inventário estático projetado no manifest.",
|
|
422
410
|
},
|
|
423
411
|
router: {
|
|
424
412
|
name: "router",
|