@softize/opus 23.0.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +241 -42
  4. package/bin/lib/copy.mjs +16 -34
  5. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  6. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
  7. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
  8. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  9. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  10. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  11. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  12. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  13. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  14. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  15. package/package.json +5 -1
  16. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  17. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  18. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  19. package/registry/templates/app/src/App.tsx +24 -12
  20. package/src/ui/appearance.ts +149 -0
  21. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  22. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  23. package/src/ui/components/patterns/confirm.tsx +3 -1
  24. package/src/ui/components/patterns/fill-region.ts +8 -7
  25. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  26. package/src/ui/components/patterns/form.tsx +8 -7
  27. package/src/ui/components/patterns/list.tsx +15 -19
  28. package/src/ui/components/patterns/page.tsx +15 -11
  29. package/src/ui/components/patterns/pane.tsx +9 -240
  30. package/src/ui/components/patterns/presentation.tsx +14 -14
  31. package/src/ui/components/patterns/sidebar.tsx +7 -17
  32. package/src/ui/components/patterns/split.tsx +111 -59
  33. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  34. package/src/ui/components/patterns/structural-media.tsx +26 -0
  35. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  36. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  37. package/src/ui/components/patterns/surface.tsx +620 -0
  38. package/src/ui/components/patterns/trigger.tsx +11 -9
  39. package/src/ui/components/primitives/ask.tsx +155 -83
  40. package/src/ui/components/primitives/button.tsx +11 -9
  41. package/src/ui/components/primitives/checkbox.tsx +8 -8
  42. package/src/ui/components/primitives/command.tsx +1 -1
  43. package/src/ui/components/primitives/composer.tsx +47 -45
  44. package/src/ui/components/primitives/detail.tsx +9 -2
  45. package/src/ui/components/primitives/dialog.tsx +60 -26
  46. package/src/ui/components/primitives/drawer.tsx +58 -12
  47. package/src/ui/components/primitives/icon-picker.tsx +90 -73
  48. package/src/ui/components/primitives/input-group.tsx +35 -28
  49. package/src/ui/components/primitives/input-otp.tsx +16 -16
  50. package/src/ui/components/primitives/input.tsx +20 -10
  51. package/src/ui/components/primitives/item.tsx +9 -12
  52. package/src/ui/components/primitives/menu.tsx +44 -65
  53. package/src/ui/components/primitives/metric-card.tsx +75 -59
  54. package/src/ui/components/primitives/radio-group.tsx +9 -9
  55. package/src/ui/components/primitives/select.tsx +344 -209
  56. package/src/ui/components/primitives/switch.tsx +10 -10
  57. package/src/ui/components/primitives/table.tsx +29 -24
  58. package/src/ui/components/primitives/tabs.tsx +4 -2
  59. package/src/ui/components/primitives/textarea.tsx +6 -6
  60. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  61. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  62. package/src/ui/components/primitives/toggle.tsx +82 -16
  63. package/src/ui/docs/DocBrowser.tsx +77 -69
  64. package/src/ui/docs/content/action-form-card.md +9 -9
  65. package/src/ui/docs/content/action-list.md +76 -70
  66. package/src/ui/docs/content/action-view.md +33 -18
  67. package/src/ui/docs/content/button.md +1 -1
  68. package/src/ui/docs/content/communication.md +1 -1
  69. package/src/ui/docs/content/customization.md +64 -11
  70. package/src/ui/docs/content/detail.md +8 -6
  71. package/src/ui/docs/content/dialog.md +100 -58
  72. package/src/ui/docs/content/drawer.md +63 -19
  73. package/src/ui/docs/content/field.md +14 -15
  74. package/src/ui/docs/content/icon-tile.md +14 -13
  75. package/src/ui/docs/content/page.md +34 -26
  76. package/src/ui/docs/content/pane.md +41 -290
  77. package/src/ui/docs/content/presentation.md +3 -3
  78. package/src/ui/docs/content/separator.md +6 -6
  79. package/src/ui/docs/content/sidebar.md +68 -266
  80. package/src/ui/docs/content/skeleton.md +20 -18
  81. package/src/ui/docs/content/split.md +14 -14
  82. package/src/ui/docs/content/surface.md +513 -0
  83. package/src/ui/docs/content/tabs.md +20 -20
  84. package/src/ui/docs/content/toggle.md +117 -65
  85. package/src/ui/docs/content/tokens.md +124 -40
  86. package/src/ui/docs/registry.tsx +4 -6
  87. package/src/ui/index.ts +30 -27
  88. package/src/ui/meta.ts +15 -27
  89. package/src/ui/react.tsx +40 -43
  90. package/src/ui/theme.css +311 -64
  91. package/src/ui/components/patterns/content-header.tsx +0 -528
  92. package/src/ui/components/patterns/surface-media.tsx +0 -69
  93. package/src/ui/components/primitives/card.tsx +0 -191
  94. package/src/ui/docs/content/card.md +0 -292
  95. package/src/ui/docs/content/content.md +0 -275
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Anatomia de superfície (interno): as medidas que Card, Dialog, Drawer e Pane
3
- * compartilham — ADR 0023.
2
+ * Anatomia de superfície (interno): as medidas que Surface, Dialog e Drawer
3
+ * compartilham — ADRs 0023, 0045 e 0047.
4
4
  *
5
5
  * Aqui mora o que antes era escrito seis vezes: recuo de cada região, ritmo vertical,
6
6
  * escala do título, posição das ações e a regra dos divisores. As famílias públicas
@@ -10,13 +10,12 @@
10
10
  *
11
11
  * - `continuous` — sem divisores. O ritmo separa: o header se aproxima do corpo e a
12
12
  * última região — seja ela o header, o body ou o footer — fecha o espaço de baixo com
13
- * o mesmo respiro do topo. É o card de leitura.
13
+ * o mesmo respiro do topo. É a superfície de leitura.
14
14
  * - `sectioned` — regiões simétricas, com divisor entre as que forem adjacentes. É o
15
15
  * modal, o drawer e o painel.
16
16
  *
17
17
  * O recuo lateral é o mesmo nas três regiões da mesma superfície; é isso que a suíte
18
- * transversal verifica. Entre famílias diferentes ele pode divergir: o card de leitura
19
- * é mais folgado que um painel de operação.
18
+ * transversal verifica.
20
19
  */
21
20
 
22
21
  import {
@@ -28,7 +27,7 @@ import {
28
27
  } from "react";
29
28
  import { FillBoundary } from "./fill-region.ts";
30
29
 
31
- export type SurfaceFamily = "card" | "dialog" | "drawer" | "pane";
30
+ export type SurfaceFamily = "surface" | "dialog" | "drawer";
32
31
  export type SurfaceLayout = "continuous" | "sectioned";
33
32
  export type SurfaceRegion = "header" | "body" | "footer";
34
33
 
@@ -70,44 +69,56 @@ interface FamilyAnatomy {
70
69
  }
71
70
 
72
71
  /**
73
- * O recuo seccionado de Card, Dialog e Drawer: 1rem, a densidade das superfícies de
74
- * operação, e o que permite um card seccionado conviver com um drawer sem degrau
75
- * visível. O pane tem a sua (`px-3`, a do chrome), declarada na tabela abaixo.
72
+ * O recuo seccionado de Dialog e Drawer: 1rem, a densidade das superfícies de operação.
73
+ * A Surface usa o mesmo recuo lateral, para conviver com um drawer sem degrau visível, e
74
+ * faixas de header e footer mais baixas.
76
75
  */
77
76
  const sectionedRegions: RegionAnatomy = {
78
77
  header: "p-4",
79
- body: "p-4",
78
+ body: "px-4 py-6",
80
79
  footer: "p-4",
81
80
  };
82
81
 
82
+ /**
83
+ * Ritmo entre blocos diretos do conteúdo; relações internas pertencem a um wrapper filho.
84
+ * Os blocos não encolhem: o body é quem rola quando a família tem altura limitada.
85
+ */
86
+ const structuralBodyRhythm = "flex flex-col gap-6 *:shrink-0";
87
+
83
88
  export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
84
- card: {
89
+ // Medidas da Surface delimitada (com moldura ou elevação). Sem delimitação, as regiões
90
+ // não recebem recuo: a superfície flui alinhada ao hospedeiro (ADR 0045).
91
+ surface: {
85
92
  regions: {
86
- // O `pb-3` do header e do body encurtam a distância até a região seguinte; quando
87
- // a região é a última, ela reassume o respiro de baixo (`[&:last-child]:pb-5`) —
88
- // inclusive o header sozinho, que sem isso fecharia o card mais apertado embaixo
89
- // do que em cima.
93
+ // O espaço entre regiões soma o ritmo estrutural. Quando o header fica sozinho, ele
94
+ // reassume o respiro compacto da faixa; quando o body abre ou fecha a superfície,
95
+ // recebe o respiro inteiro do conteúdo.
90
96
  continuous: {
91
- header: "px-5 pt-5 pb-3 [&:last-child]:pb-5",
92
- body: "px-5 py-3 [&:last-child]:pb-5 [&:first-child]:pt-5",
93
- footer: "px-5 pt-3 pb-5",
97
+ header: "px-4 pt-4 pb-6 [&:last-child]:pb-4",
98
+ body: "px-4 pt-0 pb-6 [&:first-child]:pt-6",
99
+ footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
100
+ },
101
+ sectioned: {
102
+ header: "px-4 py-3",
103
+ body: "px-4 py-6",
104
+ footer: "px-4 py-3",
94
105
  },
95
- sectioned: sectionedRegions,
96
106
  },
107
+ // Uma escala só: o nível 1 troca a apresentação inteira (ADR 0038), nunca o layout.
97
108
  title: {
98
- continuous: "text-base font-semibold leading-none",
99
- sectioned: "text-sm font-semibold leading-none",
109
+ continuous: "text-sm font-semibold",
110
+ sectioned: "text-sm font-semibold",
100
111
  },
101
- description: "text-sm text-muted-foreground",
102
- headerGap: "gap-3",
112
+ description: "mt-0.5 text-xs text-muted-foreground",
113
+ headerGap: "gap-2",
103
114
  footerBand: "",
104
115
  },
105
116
  dialog: {
106
117
  regions: {
107
118
  continuous: {
108
- header: "p-4 pb-2 [&:last-child]:pb-4",
109
- body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
110
- footer: "p-4 pt-2",
119
+ header: "p-4 pb-3 [&:last-child]:pb-4",
120
+ body: "px-4 py-3 [&:last-child]:pb-6 [&:first-child]:pt-6",
121
+ footer: "p-4 pt-3",
111
122
  },
112
123
  sectioned: sectionedRegions,
113
124
  },
@@ -122,9 +133,9 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
122
133
  drawer: {
123
134
  regions: {
124
135
  continuous: {
125
- header: "p-4 pb-2 [&:last-child]:pb-4",
126
- body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
127
- footer: "p-4 pt-2",
136
+ header: "p-4 pb-3 [&:last-child]:pb-4",
137
+ body: "px-4 py-3 [&:last-child]:pb-6 [&:first-child]:pt-6",
138
+ footer: "p-4 pt-3",
128
139
  },
129
140
  sectioned: sectionedRegions,
130
141
  },
@@ -136,29 +147,6 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
136
147
  headerGap: "gap-4",
137
148
  footerBand: "bg-muted/20 dark:bg-input/20",
138
149
  },
139
- pane: {
140
- regions: {
141
- continuous: {
142
- header: "flex shrink-0 items-center px-3 pt-3 pb-2 [&:last-child]:pb-3",
143
- body: "p-3 [&:first-child]:pt-3",
144
- footer: "px-3 pt-2 pb-3",
145
- },
146
- sectioned: {
147
- // A faixa tem a altura da barra do shell; `flex items-center` centraliza a linha
148
- // dentro dela, e `shrink-0` impede que um corpo longo comprima a faixa.
149
- header: "flex h-[3.75rem] shrink-0 items-center px-3",
150
- body: "p-3",
151
- footer: "px-3 py-2",
152
- },
153
- },
154
- title: {
155
- continuous: "text-sm font-semibold leading-none",
156
- sectioned: "text-sm font-semibold leading-none",
157
- },
158
- description: "text-xs text-muted-foreground",
159
- headerGap: "gap-2",
160
- footerBand: "",
161
- },
162
150
  };
163
151
 
164
152
  /** Classes da região, já com o divisor que o layout deriva da vizinhança. */
@@ -166,14 +154,22 @@ export function surfaceRegionClasses(
166
154
  family: SurfaceFamily,
167
155
  layout: SurfaceLayout,
168
156
  region: SurfaceRegion,
157
+ { flush = false }: { flush?: boolean } = {},
169
158
  ): string {
170
159
  const anatomy = surfaceAnatomy[family];
171
- const base = anatomy.regions[layout][region];
172
- if (layout !== "sectioned") return base;
173
- if (region === "header") return `${base} ${headerDivider}`;
160
+ const base =
161
+ region === "body" && flush
162
+ ? structuralBodyRhythm
163
+ : anatomy.regions[layout][region];
164
+ const regionClasses =
165
+ region === "body" && !flush ? `${base} ${structuralBodyRhythm}` : base;
166
+ if (layout !== "sectioned") return regionClasses;
167
+ if (region === "header") return `${regionClasses} ${headerDivider}`;
174
168
  if (region === "footer")
175
- return [base, footerDivider, anatomy.footerBand].filter(Boolean).join(" ");
176
- return base;
169
+ return [regionClasses, footerDivider, anatomy.footerBand]
170
+ .filter(Boolean)
171
+ .join(" ");
172
+ return regionClasses;
177
173
  }
178
174
 
179
175
  /** Escala visual do título — o nível do heading vem da composição, nunca daqui. */
@@ -191,8 +187,7 @@ export function surfaceDescriptionClasses(family: SurfaceFamily): string {
191
187
  /**
192
188
  * A linha do header: mídia, heading que absorve a folga, ações no extremo oposto.
193
189
  *
194
- * `flex-1` não é decoração: quando a própria região é flex — o pane, que tem altura de
195
- * barra —, a linha vira um item que se mede pelo conteúdo e as ações param no meio.
190
+ * `flex-1` mantém o heading como região elástica e as ações no extremo oposto.
196
191
  */
197
192
  export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
198
193
  return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap}`;
@@ -201,6 +196,22 @@ export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
201
196
  /** Ações do header e do footer: fora do fluxo de compressão, na escala densa. */
202
197
  export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
203
198
 
199
+ /**
200
+ * A `legend` de uma Surface `fieldset` delimitada (ADR 0045).
201
+ *
202
+ * - `notch`: moldura contínua. A legend nativa ocupa a borda, afastada do canto pelo
203
+ * mesmo recuo lateral das regiões; a região seguinte recebe o intervalo que a legend
204
+ * não tem como declarar.
205
+ * - `band`: a legend é a faixa do header dentro da superfície. Uma legend só ocupa a
206
+ * largura inteira fora do fluxo de borda, e a região seguinte precisa limpar o float.
207
+ */
208
+ export const surfaceLegendClasses = {
209
+ notch: "ml-2 w-auto px-2",
210
+ afterNotch: "pt-3",
211
+ band: "float-left w-full",
212
+ afterBand: "clear-left",
213
+ } as const;
214
+
204
215
  /**
205
216
  * O layout é do root e chega aos slots por contexto — ADR 0023. Slot não recebe
206
217
  * `layout`: o divisor depende de quem está do outro lado, então a decisão precisa valer
@@ -230,10 +241,9 @@ export function SurfaceLayoutProvider({
230
241
  }
231
242
 
232
243
  const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
233
- card: "continuous",
244
+ surface: "continuous",
234
245
  dialog: "sectioned",
235
246
  drawer: "sectioned",
236
- pane: "sectioned",
237
247
  };
238
248
 
239
249
  /** O layout vigente: o declarado pelo root da superfície, ou o default da família. */
@@ -45,7 +45,6 @@ export function SurfaceAssistant({
45
45
  key="surface-content"
46
46
  id="surface-content"
47
47
  grow
48
- inset="none"
49
48
  className={cn("h-full", contentClassName)}
50
49
  >
51
50
  {children}
@@ -57,13 +56,12 @@ export function SurfaceAssistant({
57
56
  initialSize={initialSize}
58
57
  minSize={minSize}
59
58
  maxSize={maxSize}
60
- inset="none"
61
- className={cn("h-full bg-background", assistantClassName)}
59
+ className={cn("h-full", assistantClassName)}
62
60
  >
63
61
  <aside
64
62
  data-slot="surface-assistant-panel"
65
63
  aria-label={assistantLabel}
66
- className="h-full min-w-0 overflow-hidden"
64
+ className="h-full min-w-0 overflow-hidden bg-background"
67
65
  >
68
66
  {assistant}
69
67
  </aside>