@softize/opus 13.1.0 → 15.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 (72) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +33 -5
  4. package/bin/lib/cli-shared.mjs +30 -1
  5. package/bin/lib/copy.mjs +279 -6
  6. package/bin/lib/db.mjs +2 -0
  7. package/docs/adr/0004-page-content-state-is-composed.md +39 -5
  8. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +9 -3
  9. package/docs/adr/0009-page-title-does-not-carry-a-counter.md +57 -0
  10. package/docs/adr/0010-page-header-owns-page-chrome.md +73 -0
  11. package/docs/code-style.md +4 -1
  12. package/docs/data-layer.md +9 -0
  13. package/package.json +1 -1
  14. package/registry/instructions/opus.md +3 -3
  15. package/registry/skills/build-opus-ui/SKILL.md +3 -2
  16. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
  17. package/registry/templates/app/src/App.tsx +11 -6
  18. package/src/core/types.ts +3 -4
  19. package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
  20. package/src/ui/components/patterns/confirm.tsx +2 -31
  21. package/src/ui/components/patterns/content-header.tsx +42 -141
  22. package/src/ui/components/patterns/data-state.tsx +42 -68
  23. package/src/ui/components/patterns/form.tsx +15 -15
  24. package/src/ui/components/patterns/list.tsx +55 -34
  25. package/src/ui/components/patterns/page-state.tsx +81 -51
  26. package/src/ui/components/patterns/page.tsx +228 -97
  27. package/src/ui/components/patterns/state-surface.tsx +262 -0
  28. package/src/ui/components/patterns/surface-header.tsx +204 -0
  29. package/src/ui/components/patterns/trigger.tsx +9 -10
  30. package/src/ui/components/patterns/view.tsx +14 -16
  31. package/src/ui/components/primitives/alert.tsx +1 -33
  32. package/src/ui/components/primitives/avatar.tsx +15 -5
  33. package/src/ui/components/primitives/badge.tsx +2 -43
  34. package/src/ui/components/primitives/button-group.tsx +34 -8
  35. package/src/ui/components/primitives/button.tsx +31 -35
  36. package/src/ui/components/primitives/control.ts +69 -0
  37. package/src/ui/components/primitives/dot.tsx +1 -30
  38. package/src/ui/components/primitives/input-group.tsx +11 -8
  39. package/src/ui/components/primitives/item.tsx +3 -1
  40. package/src/ui/components/primitives/menu.tsx +1 -7
  41. package/src/ui/components/primitives/pagination.tsx +16 -8
  42. package/src/ui/components/primitives/select.tsx +2 -2
  43. package/src/ui/components/primitives/spinner.tsx +13 -16
  44. package/src/ui/components/primitives/switch.tsx +4 -1
  45. package/src/ui/components/primitives/tabs.tsx +5 -3
  46. package/src/ui/components/primitives/toggle.tsx +9 -4
  47. package/src/ui/docs/content/action-form.md +26 -0
  48. package/src/ui/docs/content/action-list-dialog.md +2 -2
  49. package/src/ui/docs/content/action-list.md +35 -2
  50. package/src/ui/docs/content/action-trigger.md +5 -4
  51. package/src/ui/docs/content/action-view.md +3 -2
  52. package/src/ui/docs/content/alert.md +16 -4
  53. package/src/ui/docs/content/avatar.md +7 -3
  54. package/src/ui/docs/content/button.md +48 -17
  55. package/src/ui/docs/content/communication.md +36 -0
  56. package/src/ui/docs/content/content.md +5 -4
  57. package/src/ui/docs/content/data-state.md +17 -13
  58. package/src/ui/docs/content/dialog.md +1 -4
  59. package/src/ui/docs/content/input.md +1 -1
  60. package/src/ui/docs/content/item.md +1 -1
  61. package/src/ui/docs/content/page.md +160 -37
  62. package/src/ui/docs/content/pagination.md +11 -9
  63. package/src/ui/docs/content/semantic-context.md +3 -2
  64. package/src/ui/docs/content/sidebar.md +2 -42
  65. package/src/ui/docs/content/spinner.md +9 -6
  66. package/src/ui/docs/content/switch.md +1 -1
  67. package/src/ui/docs/content/tabs.md +1 -1
  68. package/src/ui/docs/content/toggle.md +1 -1
  69. package/src/ui/drivers/react.tsx +1 -6
  70. package/src/ui/meta.ts +8 -8
  71. package/src/ui/react.tsx +16 -18
  72. package/src/ui/components/patterns/shell-nav.tsx +0 -154
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,
@@ -422,14 +423,20 @@ export type { DataStateProps } from "./components/patterns/data-state.tsx";
422
423
  export {
423
424
  Page,
424
425
  PageHeader,
426
+ PageNavigation,
427
+ PageBack,
425
428
  PageTitle,
426
429
  PageDescription,
427
- PageMeta,
428
430
  PageActions,
429
431
  PageActionsTarget,
430
432
  PageBody,
431
433
  } from "./components/patterns/page.tsx";
432
- export type { PageProps } from "./components/patterns/page.tsx";
434
+ export type {
435
+ PageProps,
436
+ PageBackProps,
437
+ PageHeaderProps,
438
+ PageHeaderVariant,
439
+ } from "./components/patterns/page.tsx";
433
440
 
434
441
  // Estado integral do conteúdo de Page (loading/error/empty/ready), sem acoplar Page a dados.
435
442
  export { PageState } from "./components/patterns/page-state.tsx";
@@ -499,15 +506,6 @@ export type {
499
506
  SidebarNavItem,
500
507
  } from "./components/patterns/sidebar.tsx";
501
508
 
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
509
  // Roteamento history-based (o pathname É o estado). Escopo pequeno de propósito: leitura
512
510
  // reativa da URL + navigate — sem tabela de rotas. Ver src/ui/router.ts.
513
511
  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
- }