@softize/opus 13.1.0 → 14.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 +34 -0
  2. package/bin/lib/copy.mjs +276 -6
  3. package/docs/code-style.md +4 -1
  4. package/package.json +1 -1
  5. package/registry/instructions/opus.md +3 -3
  6. package/registry/templates/app/src/App.tsx +11 -6
  7. package/src/core/types.ts +3 -4
  8. package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
  9. package/src/ui/components/patterns/confirm.tsx +2 -31
  10. package/src/ui/components/patterns/content-header.tsx +42 -141
  11. package/src/ui/components/patterns/data-state.tsx +42 -68
  12. package/src/ui/components/patterns/form.tsx +15 -15
  13. package/src/ui/components/patterns/list.tsx +14 -8
  14. package/src/ui/components/patterns/page-state.tsx +39 -51
  15. package/src/ui/components/patterns/page.tsx +18 -53
  16. package/src/ui/components/patterns/state-surface.tsx +148 -0
  17. package/src/ui/components/patterns/surface-header.tsx +119 -0
  18. package/src/ui/components/patterns/trigger.tsx +7 -9
  19. package/src/ui/components/patterns/view.tsx +14 -16
  20. package/src/ui/components/primitives/alert.tsx +1 -33
  21. package/src/ui/components/primitives/avatar.tsx +15 -5
  22. package/src/ui/components/primitives/badge.tsx +2 -43
  23. package/src/ui/components/primitives/button.tsx +31 -35
  24. package/src/ui/components/primitives/control.ts +60 -0
  25. package/src/ui/components/primitives/dot.tsx +1 -30
  26. package/src/ui/components/primitives/input-group.tsx +11 -8
  27. package/src/ui/components/primitives/item.tsx +3 -1
  28. package/src/ui/components/primitives/menu.tsx +1 -7
  29. package/src/ui/components/primitives/pagination.tsx +16 -8
  30. package/src/ui/components/primitives/select.tsx +2 -2
  31. package/src/ui/components/primitives/spinner.tsx +13 -16
  32. package/src/ui/components/primitives/switch.tsx +4 -1
  33. package/src/ui/components/primitives/tabs.tsx +5 -3
  34. package/src/ui/components/primitives/toggle.tsx +9 -4
  35. package/src/ui/docs/content/action-form.md +26 -0
  36. package/src/ui/docs/content/action-list-dialog.md +2 -2
  37. package/src/ui/docs/content/action-list.md +3 -1
  38. package/src/ui/docs/content/action-trigger.md +4 -4
  39. package/src/ui/docs/content/action-view.md +3 -2
  40. package/src/ui/docs/content/avatar.md +7 -3
  41. package/src/ui/docs/content/button.md +30 -14
  42. package/src/ui/docs/content/communication.md +36 -0
  43. package/src/ui/docs/content/content.md +5 -4
  44. package/src/ui/docs/content/data-state.md +17 -13
  45. package/src/ui/docs/content/dialog.md +1 -4
  46. package/src/ui/docs/content/input.md +1 -1
  47. package/src/ui/docs/content/item.md +1 -1
  48. package/src/ui/docs/content/page.md +12 -4
  49. package/src/ui/docs/content/pagination.md +11 -9
  50. package/src/ui/docs/content/semantic-context.md +3 -2
  51. package/src/ui/docs/content/sidebar.md +2 -42
  52. package/src/ui/docs/content/spinner.md +9 -6
  53. package/src/ui/docs/content/switch.md +1 -1
  54. package/src/ui/docs/content/tabs.md +1 -1
  55. package/src/ui/docs/content/toggle.md +1 -1
  56. package/src/ui/drivers/react.tsx +1 -6
  57. package/src/ui/meta.ts +5 -5
  58. package/src/ui/react.tsx +8 -16
  59. package/src/ui/components/patterns/shell-nav.tsx +0 -154
@@ -1,9 +1,12 @@
1
1
  ## Carregamento sem progresso conhecido
2
2
 
3
- Use `Spinner` quando a duração ou o progresso da espera não forem conhecidos. `sm` atende ações
4
- compactas; `lg`, estados mais amplos. O componente possui o nome acessível “Carregando”.
3
+ Use `Spinner` quando a duração ou o progresso da espera não forem conhecidos. `size` usa os nomes da
4
+ escala dos controles e mede o glifo do controle homônimo: `sm` é o ícone de um `Button` `sm`, `lg` o
5
+ de um `lg`. O ícone é decorativo; quem nomeia a espera é o contêiner (`role="status"`, como fazem
6
+ `DataState` e `PageState`) ou o texto ao lado.
5
7
 
6
8
  ```tsx preview
9
+ <Spinner size="xs" />
7
10
  <Spinner size="sm" />
8
11
  <Spinner />
9
12
  <Spinner size="lg" />
@@ -20,11 +23,11 @@ andamento.
20
23
 
21
24
  ## Em carga de conteúdo
22
25
 
23
- Espera curta sem forma definida. Quando o conteúdo tem forma conhecida (lista, card), prefira
24
- Skeleton.
26
+ Espera curta sem forma definida; o contêiner nomeia o estado. Para uma consulta inteira, `DataState`
27
+ já monta esse contêiner. Quando o conteúdo tem forma conhecida (lista, card), prefira Skeleton.
25
28
 
26
29
  ```tsx preview
27
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
30
+ <div role="status" className="flex items-center gap-2 text-sm text-muted-foreground">
28
31
  <Spinner />
29
32
  <span>Carregando sessões…</span>
30
33
  </div>
@@ -34,4 +37,4 @@ Skeleton.
34
37
 
35
38
  | Propriedade | Tipo | Padrão | Descrição |
36
39
  |---|---|---|---|
37
- | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | O tamanho sm para dentro de botão, lg para estados de página. |
40
+ | `size` | `'xs' \| 'sm' \| 'default' \| 'lg'` | `'default'` | O glifo do controle homônimo na escala única (0.875 · 0.875 · 1 · 1.25rem). |
@@ -66,5 +66,5 @@ disabled esmaece e bloqueia a chave — ligada ou desligada — e o rótulo em p
66
66
  | `checked` | `boolean` | | O estado, no modo controlado — parear com onCheckedChange. |
67
67
  | `onCheckedChange` | `(checked: boolean) => void` | | Chamado a cada alternância, com o novo estado. |
68
68
  | `defaultChecked` | `boolean` | `false` | Estado inicial no modo não controlado. |
69
- | `size` | `'sm' \| 'default'` | `'default'` | Tamanho da chave — sm para densidade em linha de lista. |
69
+ | `size` | `'sm' \| 'default'` | `'default'` | Tamanho da chave, com os nomes da escala única — sm para densidade em linha de lista. |
70
70
  | `disabled` | `boolean` | `false` | Esmaece e bloqueia — o Label em par esmaece junto (peer-disabled). |
@@ -92,7 +92,7 @@ lateral do gatilho.
92
92
  | `defaultValue` | `string` | | Aba inicial no modo não controlado. |
93
93
  | `value` | `string` | | Aba ativa no modo controlado. Use com `onValueChange`. |
94
94
  | `onValueChange` | `(value: string) => void` | | Chamado quando a pessoa seleciona outra aba. |
95
- | `size` | `'default' \| 'sm'` | `'default'` | Escala de altura compartilhada com `TabsList`. |
95
+ | `size` | `'default' \| 'sm'` | `'default'` | Altura da lista na escala única dos controles (2.25 e 2rem), aplicada em `TabsList`. |
96
96
  | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção da lista de abas. |
97
97
 
98
98
  ## Propriedades de TabsList
@@ -70,7 +70,7 @@ disabled esmaece e bloqueia o clique — o estado pressed permanece visível.
70
70
  | `onPressedChange` | `(pressed: boolean) => void` | | Chamado a cada alternância, com o novo estado. |
71
71
  | `defaultPressed` | `boolean` | `false` | Estado inicial no modo não controlado. |
72
72
  | `variant` | `'default' \| 'outline'` | `'default'` | default não tem borda (fundo só quando ativo); outline carrega a borda. |
73
- | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Altura do botão — sm para toolbar densa, lg para alvo mais confortável. |
73
+ | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Altura na escala única dos controles (2 · 2.25 · 2.5rem) — sm para toolbar densa, lg para alvo mais confortável. |
74
74
  | `disabled` | `boolean` | `false` | Esmaece e bloqueia o clique, preservando o estado visual. |
75
75
 
76
76
  ## ToggleGroup
@@ -2,7 +2,7 @@
2
2
  * @softize/opus/ui/react — React driver
3
3
  *
4
4
  * Hooks + Provider pra invocar actions client-side.
5
- * - `OpusProvider` — injeta ClientAdapter no contexto (`TbdlibProvider` é alias em retirada)
5
+ * - `OpusProvider` — injeta ClientAdapter no contexto
6
6
  * - `useAction(action)` — invoca actions simple/form/view
7
7
  * - `useLookupAction(action)` — variante pra search (output Paginated)
8
8
  *
@@ -83,11 +83,6 @@ export function OpusProvider({
83
83
  return <OpusContext.Provider value={value}>{children}</OpusContext.Provider>
84
84
  }
85
85
 
86
- /** @deprecated Renomeado para `OpusProvider`; o alias segue até a próxima série. */
87
- export const TbdlibProvider = OpusProvider
88
- /** @deprecated Use `OpusProviderProps`. */
89
- export type TbdlibProviderProps = OpusProviderProps
90
-
91
86
  function useClient(): ClientAdapter {
92
87
  const ctx = useContext(OpusContext)
93
88
  if (ctx === null) {
package/src/ui/meta.ts CHANGED
@@ -69,7 +69,7 @@ export const componentMeta = {
69
69
  name: "button",
70
70
  ancestry: "shadcn",
71
71
  whenToUse:
72
- "Inicie uma ação com um controle clicável. Use `context` para o significado, `variant` para o tratamento visual e `size` para a escala; `busy` comunica o andamento e impede um novo acionamento. Para ações relacionadas, use ButtonGroup.",
72
+ "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.",
73
73
  },
74
74
  card: {
75
75
  name: "card",
@@ -117,7 +117,7 @@ export const componentMeta = {
117
117
  name: "content-header",
118
118
  ancestry: "opus",
119
119
  whenToUse:
120
- "Estruture o cabeçalho de um Content com título, descrição, metadados e ações. No caso comum, declare esses valores diretamente em Content; componha ContentHeader apenas quando precisar controlar a anatomia.",
120
+ "Estruture o cabeçalho de um Content com título, contador, descrição e ações — a mesma anatomia de PageHeader. No caso comum, declare esses valores diretamente em Content; componha ContentHeader apenas quando precisar controlar a anatomia.",
121
121
  },
122
122
  copyable: {
123
123
  name: "copyable",
@@ -183,7 +183,7 @@ export const componentMeta = {
183
183
  name: "spinner",
184
184
  ancestry: "shadcn",
185
185
  whenToUse:
186
- "Indique uma espera sem progresso determinado, como uma ação ou consulta em andamento. Para reservar a forma do conteúdo, use Skeleton.",
186
+ "Indique uma espera sem progresso determinado, como uma ação ou consulta em andamento. O ícone é decorativo: o contêiner (`role=\"status\"`) ou o texto ao lado nomeia a espera. Para reservar a forma do conteúdo, use Skeleton.",
187
187
  },
188
188
  table: {
189
189
  name: "table",
@@ -327,7 +327,7 @@ export const componentMeta = {
327
327
  name: "sidebar",
328
328
  ancestry: "opus",
329
329
  whenToUse:
330
- "Organize navegação global ou contextual em uma coluna lateral. Split e Pane definem posição e largura; Sidebar fornece a superfície e o colapso, enquanto PaneHeader, PaneBody e PaneFooter estruturam as regiões fixa e rolável. Use SidebarNav para grupos planos, componha árvores com SidebarItem e SidebarTreeGroup e use ShellNav quando a navegação precisar de cabeçalho e rodapé próprios.",
330
+ "Organize navegação global ou contextual em uma coluna lateral. Split e Pane definem posição e largura; Sidebar fornece a superfície e o colapso, enquanto PaneHeader, PaneBody e PaneFooter estruturam as regiões fixa e rolável. Use SidebarNav para grupos planos e componha árvores com SidebarItem e SidebarTreeGroup; cabeçalho e rodapé próprios ficam em PaneHeader e PaneFooter.",
331
331
  },
332
332
  "scroll-area": {
333
333
  name: "scroll-area",
@@ -412,7 +412,7 @@ export const componentMeta = {
412
412
  name: "data-state",
413
413
  ancestry: "opus",
414
414
  whenToUse:
415
- "Coordene carregamento, erro, vazio e conteúdo de uma consulta assíncrona. Use DataState dentro da estrutura que receberá os dados; para uma região disponível à criação, use Empty. Ações em andamento pertencem ao estado `busy` do controle que as iniciou.",
415
+ "Coordene carregamento, erro, vazio e conteúdo de uma consulta assíncrona com as mesmas superfícies de PageState: `emptyMessage` compõe Empty com moldura sólida, o erro é um Alert com `onRetry` e `retryLabel`. Use DataState dentro da estrutura que receberá os dados; para uma região disponível à criação, use Empty. Ações em andamento pertencem ao estado `busy` do controle que as iniciou.",
416
416
  },
417
417
  "action-list-dialog": {
418
418
  name: "action-list-dialog",
package/src/ui/react.tsx CHANGED
@@ -18,7 +18,12 @@ export type {
18
18
  ButtonVariant,
19
19
  ButtonSize,
20
20
  } from "./components/primitives/button.tsx";
21
- export type { ControlShape } from "./components/primitives/control.ts";
21
+ export type {
22
+ ControlShape,
23
+ ControlSize,
24
+ ControlIconSize,
25
+ ControlScale,
26
+ } from "./components/primitives/control.ts";
22
27
 
23
28
  export { Input } from "./components/primitives/input.tsx";
24
29
 
@@ -358,6 +363,7 @@ export { useOverflowing } from "./lib/overflow.ts";
358
363
  export {
359
364
  ActionForm,
360
365
  ActionFormField,
366
+ LabelHelp,
361
367
  useActionFormContext,
362
368
  } from "./components/patterns/form.tsx";
363
369
  export type {
@@ -395,12 +401,7 @@ export { ActionView } from "./components/patterns/view.tsx";
395
401
  export type { ActionViewProps } from "./components/patterns/view.tsx";
396
402
 
397
403
  export { ActionTrigger } from "./components/patterns/trigger.tsx";
398
- export {
399
- dialog,
400
- DialogHost,
401
- confirm,
402
- ConfirmHost,
403
- } from "./components/patterns/confirm.tsx";
404
+ export { dialog, DialogHost } from "./components/patterns/confirm.tsx";
404
405
  export type {
405
406
  AlertOptions,
406
407
  ChooseOptions,
@@ -499,15 +500,6 @@ export type {
499
500
  SidebarNavItem,
500
501
  } from "./components/patterns/sidebar.tsx";
501
502
 
502
- // O menu como primitivo pivotável: recolhe quando composto dentro de Sidebar.
503
- export { ShellNav, ShellNavHeading } from "./components/patterns/shell-nav.tsx";
504
- export type {
505
- ShellNavProps,
506
- ShellNavGroup,
507
- ShellNavItem,
508
- ShellNavHeadingProps,
509
- } from "./components/patterns/shell-nav.tsx";
510
-
511
503
  // Roteamento history-based (o pathname É o estado). Escopo pequeno de propósito: leitura
512
504
  // reativa da URL + navigate — sem tabela de rotas. Ver src/ui/router.ts.
513
505
  export {
@@ -1,154 +0,0 @@
1
- /**
2
- * <ShellNav /> — o menu como primitivo PIVOTÁVEL. Um nav (grupos → itens, com heading e
3
- * âncora inferior) que serve os DOIS lares do chrome sem flag:
4
- *
5
- * - dentro de <Sidebar> → recolhe pra ícone-só (com tooltip) junto com a coluna;
6
- * - fora de <Sidebar> → permanece expandido.
7
- *
8
- * "Tanto faz onde": o componente se adapta ao lugar, sem "modo" configurado.
9
- *
10
- * Presentacional e CONTROLADO — o app é dono do `activeId`, do clique e do roteamento.
11
- * v1 PLANA (grupos → itens); aninhamento (accordion/pasta) e DnD COMPOSTO entram quando o
12
- * primeiro consumidor de árvore migrar — ver `docs/shellnav.md`.
13
- */
14
-
15
- import type { ReactNode } from 'react'
16
- import { cn } from '../../lib/cn.ts'
17
- import { focusRing } from '../primitives/control.ts'
18
- import { useSidebarCollapsed } from './sidebar.tsx'
19
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip.tsx'
20
-
21
- /** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
22
- export interface ShellNavItem {
23
- /** Coordenada estável — volta em `onSelect` e casa com `activeId`. Único em toda a nav. */
24
- id: string
25
- label: string
26
- /** Ícone à esquerda (é o que sobra no modo recolhido — vira o alvo, com o label no tooltip). */
27
- icon?: ReactNode
28
- /** Selo à direita (contagem, `beta`). Some no recolhido. */
29
- badge?: ReactNode
30
- /** Visível mas inerte. */
31
- disabled?: boolean
32
- }
33
-
34
- /** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
35
- export interface ShellNavGroup {
36
- /** Cabeçalho do grupo. Ausente/vazio = itens soltos, sem rótulo. Some no recolhido. */
37
- label?: string
38
- items: ShellNavItem[]
39
- }
40
-
41
- /** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
42
- export interface ShellNavProps {
43
- groups: ShellNavGroup[]
44
- /** `id` do item ativo. Ausente = nenhum destacado. */
45
- activeId?: string
46
- onSelect: (id: string) => void
47
- /** Topo do nav — título, busca, botão (ex.: o "+" do Relatórios). Some no recolhido. Ver <ShellNavHeading>. */
48
- heading?: ReactNode
49
- /** Grupos ANCORADOS embaixo (ex.: Configurações no app), mesmo renderer, separados por filete. */
50
- footer?: ShellNavGroup[]
51
- /** Rótulo da landmark `<nav>`. */
52
- navLabel?: string
53
- /** Classes da raiz (a coluna). A largura vem do pane/sidebar que a contém. */
54
- className?: string
55
- }
56
-
57
- /** Rótulo só existe se tiver texto — vazio não vira cabeçalho fantasma. */
58
- const titled = (label?: string): boolean => (label ?? '').trim() !== ''
59
-
60
- /** Selo vazio não vira pílula sem texto. `0` conta (contagem legítima). */
61
- const shown = (badge: ReactNode): boolean => {
62
- if (badge === undefined || badge === null || badge === false || badge === '') return false
63
- if (typeof badge === 'number' && Number.isNaN(badge)) return false
64
- return !(Array.isArray(badge) && badge.length === 0)
65
- }
66
-
67
- /** Grupo sem item não abre buraco no `space-y` nem deixa rótulo pairando sobre nada. */
68
- const filled = (group: ShellNavGroup): boolean => group.items.length > 0
69
-
70
- function renderItem(item: ShellNavItem, activeId: string | undefined, onSelect: (id: string) => void, railed: boolean): React.ReactElement {
71
- const active = item.id === activeId
72
- const button = (
73
- <button
74
- type="button"
75
- disabled={item.disabled}
76
- aria-current={active ? 'page' : undefined}
77
- onClick={() => onSelect(item.id)}
78
- className={cn(
79
- 'flex w-full items-center gap-2.5 rounded-md py-1.5 text-left text-sm transition-colors',
80
- 'disabled:pointer-events-none disabled:opacity-40',
81
- 'outline-none',
82
- focusRing,
83
- railed ? 'justify-center px-0' : 'px-2.5',
84
- active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60',
85
- )}
86
- >
87
- {item.icon !== undefined && <span className="flex shrink-0">{item.icon}</span>}
88
- {!railed && <span className="min-w-0 flex-1 truncate">{item.label}</span>}
89
- {!railed && shown(item.badge) && <span className="shrink-0">{item.badge}</span>}
90
- </button>
91
- )
92
- if (!railed) return <div key={item.id}>{button}</div>
93
- // Recolhido: o label migra pro tooltip (à direita) — sem ele, a coluna vira ícones adivinháveis.
94
- return (
95
- <Tooltip key={item.id}>
96
- <TooltipTrigger asChild>{button}</TooltipTrigger>
97
- <TooltipContent side="right">{item.label}</TooltipContent>
98
- </Tooltip>
99
- )
100
- }
101
-
102
- function renderGroup(group: ShellNavGroup, gi: number, activeId: string | undefined, onSelect: (id: string) => void, railed: boolean): React.ReactElement | null {
103
- if (!filled(group)) return null
104
- return (
105
- <div key={gi} className="space-y-0.5">
106
- {!railed && titled(group.label) && (
107
- <div className="px-2.5 pb-1 text-xs font-medium text-muted-foreground/70">{group.label}</div>
108
- )}
109
- {group.items.map((item) => renderItem(item, activeId, onSelect, railed))}
110
- </div>
111
- )
112
- }
113
-
114
- /** @deprecated Use `SidebarNav`: mesma navegação, integrada à `Sidebar`. Mantido por compatibilidade. */
115
- export function ShellNav({ groups, activeId, onSelect, heading, footer, navLabel = 'Navegação', className }: ShellNavProps): React.ReactElement {
116
- const railed = useSidebarCollapsed()
117
-
118
- const body = (
119
- <div data-slot="shell-nav" className={cn('flex h-full min-h-0 flex-col', className)}>
120
- {!railed && heading}
121
- <nav aria-label={navLabel} className={cn('min-h-0 flex-1 space-y-4 overflow-y-auto p-2', railed && 'px-1.5')}>
122
- {groups.map((group, gi) => renderGroup(group, gi, activeId, onSelect, railed))}
123
- </nav>
124
- {footer !== undefined && footer.some(filled) && (
125
- <div className={cn('space-y-4 border-t border-border p-2', railed && 'px-1.5')}>
126
- {footer.map((group, gi) => renderGroup(group, gi, activeId, onSelect, railed))}
127
- </div>
128
- )}
129
- </div>
130
- )
131
-
132
- // Recolhido usa tooltips — garante um provider (nested é ok se o app já tem um).
133
- return railed ? <TooltipProvider delayDuration={200}>{body}</TooltipProvider> : body
134
- }
135
-
136
- /** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
137
- export interface ShellNavHeadingProps {
138
- /** Título da seção. */
139
- title: ReactNode
140
- /** Ação à direita — em geral um botão (o "+" de criar). */
141
- action?: ReactNode
142
- className?: string
143
- }
144
-
145
- /** @deprecated Use `SidebarHeader` com `SidebarNav`. Cabeçalho do ShellNav: título + espaço
146
- * pra um botão. Mantido por compatibilidade. */
147
- export function ShellNavHeading({ title, action, className }: ShellNavHeadingProps): React.ReactElement {
148
- return (
149
- <div className={cn('flex h-12 shrink-0 items-center gap-1.5 border-b border-border px-4', className)}>
150
- <span className="min-w-0 flex-1 truncate text-sm font-semibold">{title}</span>
151
- {action !== undefined && <span className="shrink-0">{action}</span>}
152
- </div>
153
- )
154
- }