@softize/opus 29.0.0 → 31.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/bin/lib/check.mjs +178 -94
  3. package/bin/lib/copy.mjs +2970 -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/0045-surface-replaces-content-and-card.md +3 -0
  7. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  8. package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
  9. package/docs/adr/0063-metadados-das-superficies.md +44 -0
  10. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  11. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  12. package/docs/adr/README.md +8 -0
  13. package/package.json +1 -1
  14. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  15. package/registry/skills/build-opus-ui/references/evaluations.md +21 -10
  16. package/registry/skills/build-opus-ui/references/ui-patterns.md +33 -28
  17. package/src/core/presentation.ts +938 -32
  18. package/src/core/types.ts +2 -1
  19. package/src/ui/components/patterns/form.tsx +2 -0
  20. package/src/ui/components/patterns/grouped-list.ts +2 -2
  21. package/src/ui/components/patterns/list.tsx +588 -365
  22. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  23. package/src/ui/components/patterns/page.tsx +238 -181
  24. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  25. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  26. package/src/ui/components/patterns/presentation.tsx +1142 -229
  27. package/src/ui/components/patterns/structural-header.tsx +47 -34
  28. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  29. package/src/ui/components/patterns/surface.tsx +55 -37
  30. package/src/ui/components/patterns/trigger.tsx +10 -2
  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/detail.tsx +1 -1
  34. package/src/ui/components/primitives/dialog.tsx +59 -89
  35. package/src/ui/components/primitives/drawer.tsx +52 -87
  36. package/src/ui/components/primitives/field.tsx +1 -1
  37. package/src/ui/components/primitives/pagination.tsx +3 -2
  38. package/src/ui/components/primitives/table.tsx +3 -3
  39. package/src/ui/components/primitives/tabs.tsx +13 -2
  40. package/src/ui/docs/content/action-form.md +2 -1
  41. package/src/ui/docs/content/action-list.md +146 -73
  42. package/src/ui/docs/content/action-trigger.md +5 -0
  43. package/src/ui/docs/content/button.md +5 -6
  44. package/src/ui/docs/content/command.md +62 -50
  45. package/src/ui/docs/content/compositions.md +129 -459
  46. package/src/ui/docs/content/dialog.md +72 -101
  47. package/src/ui/docs/content/drawer.md +69 -92
  48. package/src/ui/docs/content/field.md +2 -1
  49. package/src/ui/docs/content/page.md +207 -386
  50. package/src/ui/docs/content/presentation.md +371 -23
  51. package/src/ui/docs/content/separator.md +1 -1
  52. package/src/ui/docs/content/surface.md +94 -116
  53. package/src/ui/docs/content/table.md +7 -0
  54. package/src/ui/docs/content/tokens.md +18 -4
  55. package/src/ui/drivers/context.tsx +11 -4
  56. package/src/ui/drivers/react.tsx +27 -7
  57. package/src/ui/meta.ts +5 -5
  58. package/src/ui/react.tsx +38 -8
  59. package/src/ui/theme.css +44 -33
@@ -3,20 +3,17 @@ import { Dialog as DrawerPrimitive } from "radix-ui";
3
3
 
4
4
  import { cn } from "../../lib/cn.ts";
5
5
  import {
6
- SurfaceLayoutProvider,
7
6
  surfaceActionsClasses,
8
7
  surfaceDescriptionClasses,
9
8
  surfaceHeaderRowClasses,
10
9
  surfaceRegionClasses,
11
10
  surfaceTitleClasses,
12
- useSurfaceLayout,
13
- type SurfaceLayout,
14
11
  } from "../patterns/surface-anatomy.ts";
12
+ import { FillBoundary } from "../patterns/fill-region.ts";
15
13
  import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
16
14
  import {
17
15
  StructuralHeader,
18
- structuralHeaderClasses,
19
- validateSurfaceIntro,
16
+ structuralMetaClasses,
20
17
  } from "../patterns/structural-header.tsx";
21
18
  import {
22
19
  StructuralMedia,
@@ -47,10 +44,10 @@ interface DrawerContentContextValue {
47
44
  const DrawerContentContext = React.createContext<DrawerContentContextValue>({
48
45
  showCloseButton: false,
49
46
  closeDisabled: false,
50
- closeSize: "sm",
47
+ closeSize: "xs",
51
48
  });
52
49
 
53
- const DrawerIntroContext = React.createContext(false);
50
+ const DrawerMetaContext = React.createContext(false);
54
51
 
55
52
  /** Estado de aberto que a pilha de modais da camada flutuante acompanha (ADR 0052). */
56
53
  const DrawerStateContext = React.createContext({ open: false, modal: true });
@@ -148,8 +145,7 @@ function DrawerContent({
148
145
  side = "right",
149
146
  showCloseButton = true,
150
147
  closeDisabled = false,
151
- closeSize = "sm",
152
- layout = "sectioned",
148
+ closeSize = "xs",
153
149
  "aria-describedby": ariaDescribedBy,
154
150
  onEscapeKeyDown,
155
151
  onPointerDownOutside,
@@ -168,8 +164,6 @@ function DrawerContent({
168
164
  */
169
165
  closeDisabled?: boolean;
170
166
  closeSize?: CloseButtonSize;
171
- /** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
172
- layout?: SurfaceLayout;
173
167
  }) {
174
168
  const { open, modal } = React.useContext(DrawerStateContext);
175
169
  const layer = useLayerPortal();
@@ -207,7 +201,6 @@ function DrawerContent({
207
201
  <DrawerPrimitive.Content
208
202
  ref={composedRef}
209
203
  data-slot="drawer-content"
210
- data-layout={layout}
211
204
  {...{ [LAYER_MODAL_ATTRIBUTE]: internal || undefined }}
212
205
  className={cn(
213
206
  "fixed z-50 flex flex-col overflow-hidden rounded-xl border bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
@@ -235,13 +228,13 @@ function DrawerContent({
235
228
  {...props}
236
229
  >
237
230
  <LayerPortalProvider value={nestedPortal}>
238
- <SurfaceLayoutProvider value={layout}>
231
+ <FillBoundary>
239
232
  <DrawerContentContext.Provider
240
233
  value={{ showCloseButton, closeDisabled, closeSize }}
241
234
  >
242
235
  {children}
243
236
  </DrawerContentContext.Provider>
244
- </SurfaceLayoutProvider>
237
+ </FillBoundary>
245
238
  </LayerPortalProvider>
246
239
  </DrawerPrimitive.Content>
247
240
  </DrawerPortal>
@@ -251,39 +244,45 @@ function DrawerContent({
251
244
  function DrawerHeader({
252
245
  className,
253
246
  children,
247
+ tabs,
254
248
  ...props
255
- }: React.ComponentProps<"div">) {
256
- const layout = useSurfaceLayout("drawer");
249
+ }: React.ComponentProps<"div"> & {
250
+ tabs?: React.ReactNode;
251
+ }) {
257
252
  const { showCloseButton, closeDisabled, closeSize } =
258
253
  React.useContext(DrawerContentContext);
259
254
  return (
260
- <StructuralHeader
261
- name="DrawerHeader"
262
- slot="drawer"
263
- slots={{
264
- leading: DrawerNavigation,
265
- media: DrawerMedia,
266
- title: DrawerTitle,
267
- description: DrawerDescription,
268
- actions: DrawerActions,
269
- }}
270
- leadingPlacement="inline"
271
- titleRequired={false}
272
- rowClassName={surfaceHeaderRowClasses("drawer")}
273
- className={cn(
274
- "text-left",
275
- surfaceRegionClasses("drawer", layout, "header"),
276
- className,
277
- )}
278
- trailing={
279
- showCloseButton ? (
280
- <DrawerCloseButton disabled={closeDisabled} size={closeSize} />
281
- ) : undefined
282
- }
283
- {...props}
284
- >
285
- {children}
286
- </StructuralHeader>
255
+ <DrawerMetaContext.Provider value>
256
+ <StructuralHeader
257
+ tabs={tabs}
258
+ name="DrawerHeader"
259
+ slot="drawer"
260
+ slots={{
261
+ leading: DrawerNavigation,
262
+ media: DrawerMedia,
263
+ title: DrawerTitle,
264
+ description: DrawerDescription,
265
+ meta: DrawerMeta,
266
+ actions: DrawerActions,
267
+ }}
268
+ leadingPlacement="inline"
269
+ titleRequired
270
+ rowClassName={surfaceHeaderRowClasses("drawer")}
271
+ className={cn(
272
+ "text-left",
273
+ surfaceRegionClasses("drawer", "header"),
274
+ className,
275
+ )}
276
+ trailing={
277
+ showCloseButton ? (
278
+ <DrawerCloseButton disabled={closeDisabled} size={closeSize} />
279
+ ) : undefined
280
+ }
281
+ {...props}
282
+ >
283
+ {children}
284
+ </StructuralHeader>
285
+ </DrawerMetaContext.Provider>
287
286
  );
288
287
  }
289
288
 
@@ -317,42 +316,17 @@ function DrawerActions({ className, ...props }: React.ComponentProps<"div">) {
317
316
  );
318
317
  }
319
318
 
320
- /** Título e ações da composição principal, como primeiro bloco de DrawerBody. */
321
- function DrawerIntro({
322
- className,
323
- children,
324
- ...props
325
- }: React.ComponentProps<"div">) {
326
- return (
327
- <DrawerIntroContext.Provider value>
328
- <StructuralHeader
329
- name="DrawerIntro"
330
- slot="drawer-intro"
331
- slots={{
332
- title: DrawerTitle,
333
- description: DrawerDescription,
334
- actions: DrawerIntroActions,
335
- }}
336
- leadingPlacement="inline"
337
- titleRequired
338
- className={cn("text-left", className)}
339
- {...props}
340
- >
341
- {children}
342
- </StructuralHeader>
343
- </DrawerIntroContext.Provider>
344
- );
345
- }
346
-
347
- /** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
348
- function DrawerIntroActions({
319
+ /** Badges, datas e autoria abaixo do título e da orientação. */
320
+ export function DrawerMeta({
349
321
  className,
350
322
  ...props
351
323
  }: React.ComponentProps<"div">) {
324
+ const inside = React.useContext(DrawerMetaContext);
325
+ if (!inside) throw new Error("DrawerMeta deve estar dentro de DrawerHeader.");
352
326
  return (
353
327
  <div
354
- data-slot="drawer-intro-actions"
355
- className={cn(surfaceActionsClasses, className)}
328
+ data-slot="drawer-meta"
329
+ className={cn(structuralMetaClasses, className)}
356
330
  {...props}
357
331
  />
358
332
  );
@@ -364,14 +338,12 @@ function DrawerBody({
364
338
  style,
365
339
  ...props
366
340
  }: React.ComponentProps<"div"> & { flush?: boolean }) {
367
- const layout = useSurfaceLayout("drawer");
368
- validateSurfaceIntro(props.children, DrawerIntro, "DrawerBody");
369
341
  return (
370
342
  <div
371
343
  data-slot="drawer-body"
372
344
  className={cn(
373
345
  "min-h-0 flex-1 overflow-y-auto",
374
- surfaceRegionClasses("drawer", layout, "body", { flush }),
346
+ surfaceRegionClasses("drawer", "body", { flush }),
375
347
  className,
376
348
  )}
377
349
  style={style}
@@ -382,7 +354,6 @@ function DrawerBody({
382
354
  }
383
355
 
384
356
  function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
385
- const layout = useSurfaceLayout("drawer");
386
357
  return (
387
358
  <div
388
359
  data-slot="drawer-footer"
@@ -391,7 +362,7 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
391
362
  // preenche a altura da janela, e sem isso o rodapé subiria atrás do corpo. No
392
363
  // Dialog a altura é do conteúdo, então ele não precisa (ADR 0023).
393
364
  "mt-auto flex shrink-0 flex-row justify-end gap-2",
394
- surfaceRegionClasses("drawer", layout, "footer"),
365
+ surfaceRegionClasses("drawer", "footer"),
395
366
  className,
396
367
  )}
397
368
  {...props}
@@ -408,13 +379,9 @@ function DrawerTitle({
408
379
  /** Hierarquia do documento. Sem ele vale o `h2` que a primitiva emite. */
409
380
  level?: HeadingLevel;
410
381
  }) {
411
- const intro = React.useContext(DrawerIntroContext);
412
- const layout = useSurfaceLayout("drawer");
413
382
  const classes = cn(
414
- intro ? "min-w-0 whitespace-normal" : "min-w-0 truncate",
415
- intro
416
- ? structuralHeaderClasses.page.title
417
- : surfaceTitleClasses("drawer", layout),
383
+ "min-w-0 truncate",
384
+ surfaceTitleClasses("drawer"),
418
385
  className,
419
386
  );
420
387
  // O nível troca a TAG do mesmo nó, via `asChild` — a escala continua vindo do layout.
@@ -441,8 +408,6 @@ function DrawerTitle({
441
408
 
442
409
  export {
443
410
  DrawerActions,
444
- DrawerIntro,
445
- DrawerIntroActions,
446
411
  DrawerMedia,
447
412
  DrawerNavigation,
448
413
  Drawer,
@@ -184,7 +184,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
184
184
  <p
185
185
  data-slot="field-description"
186
186
  className={cn(
187
- "text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
187
+ "text-xs leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
188
188
  "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
189
189
  "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
190
190
  className
@@ -32,7 +32,7 @@ function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
32
32
  }
33
33
 
34
34
  export interface PaginationLinkProps extends Omit<React.ComponentProps<'a'>, 'children'> {
35
- /** Página atual: vira `outline` e ganha `aria-current="page"`. */
35
+ /** Página atual: usa fundo `subtle` e ganha `aria-current="page"`. */
36
36
  isActive?: boolean
37
37
  /** A escala de control.ts, via Button. `default` (números: altura fixa, largura mínima
38
38
  * quadrada que cresce com os dígitos) ou um `icon*` (setas quadradas, sem padding). */
@@ -59,7 +59,8 @@ function PaginationLink({ className, isActive, page, disabled, href, size = 'def
59
59
  'data-slot': 'pagination-link',
60
60
  'data-active': isActive,
61
61
  className: cn(
62
- buttonVariants({ variant: isActive ? 'outline' : 'ghost', size }),
62
+ buttonVariants({ context: 'neutral', variant: isActive ? 'subtle' : 'ghost', size }),
63
+ isActive && 'font-semibold',
63
64
  // Número: a largura mínima é quadrada e o conteúdo manda no resto. Escala `icon*`
64
65
  // fica quadrada de verdade (sem `has-[>svg]:px-*` do `default`).
65
66
  !square && cn('w-auto px-2 tabular-nums', minWidth),
@@ -24,7 +24,7 @@ function Table({
24
24
  data-slot="table-container"
25
25
  data-variant={variant}
26
26
  className={cn(
27
- "relative w-full overflow-x-auto",
27
+ "relative w-full min-w-0 max-w-full overflow-x-auto",
28
28
  variant === "framed" && "rounded-lg border border-border",
29
29
  containerClassName,
30
30
  )}
@@ -95,7 +95,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
95
95
  <th
96
96
  data-slot="table-head"
97
97
  className={cn(
98
- "h-10 px-3 text-left align-middle font-medium whitespace-nowrap text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
98
+ "h-10 px-3 py-2 text-left align-middle font-medium whitespace-normal text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
99
99
  className,
100
100
  )}
101
101
  {...props}
@@ -108,7 +108,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
108
108
  <td
109
109
  data-slot="table-cell"
110
110
  className={cn(
111
- "px-3 py-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
111
+ "px-3 py-2 align-middle whitespace-normal wrap-break-word [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
112
112
  className,
113
113
  )}
114
114
  {...props}
@@ -7,6 +7,7 @@ import {
7
7
  structuralRegionGap,
8
8
  } from "../patterns/surface-anatomy.ts";
9
9
  import { controlHeight, type ControlSize } from "./control.ts";
10
+ import { useFillRegion, fillRegionClassName } from "../patterns/fill-region.ts";
10
11
 
11
12
  // Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
12
13
  // não tinha, e uma fileira densa ficava com as abas 0.25rem mais altas
@@ -18,6 +19,9 @@ const TabsSizeContext = React.createContext<TabsSize>("default");
18
19
  const TabsDistributionContext =
19
20
  React.createContext<TabsDistribution>("content");
20
21
  const TabsFramedContext = React.createContext(false);
22
+
23
+ /** Uso interno: o header estrutural fornece o divisor da faixa de abas. */
24
+ export const TabsHeaderContext = React.createContext(false);
21
25
  type TabsOrientation = "horizontal" | "vertical";
22
26
  const TabsOrientationContext =
23
27
  React.createContext<TabsOrientation>("horizontal");
@@ -70,6 +74,7 @@ function TabsList({
70
74
  style,
71
75
  ...props
72
76
  }: React.ComponentProps<typeof TabsPrimitive.List>) {
77
+ const inHeader = React.useContext(TabsHeaderContext);
73
78
  // O size vem do <Tabs> (contexto) — a altura mora aqui, na list.
74
79
  const size = React.useContext(TabsSizeContext);
75
80
  const orientation = React.useContext(TabsOrientationContext);
@@ -101,7 +106,7 @@ function TabsList({
101
106
  }}
102
107
  {...props}
103
108
  >
104
- {hasHorizontalLine && (
109
+ {hasHorizontalLine && !inHeader && (
105
110
  <span
106
111
  aria-hidden="true"
107
112
  data-slot="tabs-line"
@@ -196,21 +201,27 @@ function TabsListItem({
196
201
 
197
202
  function TabsPanel({
198
203
  className,
204
+ children,
199
205
  ...props
200
206
  }: React.ComponentProps<typeof TabsPrimitive.Content>) {
201
207
  const framed = React.useContext(TabsFramedContext);
202
208
  const orientation = React.useContext(TabsOrientationContext);
209
+ const fill = useFillRegion();
203
210
  return (
204
211
  <TabsPrimitive.Content
205
212
  data-slot="tabs-panel"
213
+ {...fill.attributes}
206
214
  className={cn(
207
215
  "flex-1 outline-none data-[state=inactive]:hidden",
208
216
  structuralBodyRhythm,
217
+ fill.filling && fillRegionClassName,
209
218
  framed && orientation === "horizontal" && "px-4 pb-4",
210
219
  className,
211
220
  )}
212
221
  {...props}
213
- />
222
+ >
223
+ {fill.provide(children)}
224
+ </TabsPrimitive.Content>
214
225
  );
215
226
  }
216
227
 
@@ -122,7 +122,8 @@ referência; itens estáticos permanecem como declarados no contrato.
122
122
 
123
123
  ## Widgets declarativos
124
124
 
125
- `FieldSpec.widget` aceita `textarea`, `code`, `lines`, `refItems`, `icon` e `toggle-group`.
125
+ `FieldSpec.widget` aceita `password`, `textarea`, `code`, `lines`, `refItems`, `icon` e `toggle-group`.
126
+ `password` usa um input de senha com `autocomplete="new-password"`, adequado a cadastro e redefinição.
126
127
  O literal canônico do grupo de escolhas é sempre kebab-case: `widget: 'toggle-group'`. Em enum ele
127
128
  renderiza seleção única; em array de enum, seleção múltipla. As opções seguem a mesma precedência e
128
129
  o mesmo `SelectOption` rico dos selects. Use esse widget quando cada opção funcionar como uma