@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.
- package/CHANGELOG.md +97 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +33 -5
- package/bin/lib/cli-shared.mjs +30 -1
- package/bin/lib/copy.mjs +279 -6
- package/bin/lib/db.mjs +2 -0
- package/docs/adr/0004-page-content-state-is-composed.md +39 -5
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +9 -3
- package/docs/adr/0009-page-title-does-not-carry-a-counter.md +57 -0
- package/docs/adr/0010-page-header-owns-page-chrome.md +73 -0
- package/docs/code-style.md +4 -1
- package/docs/data-layer.md +9 -0
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -3
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
- package/registry/templates/app/src/App.tsx +11 -6
- package/src/core/types.ts +3 -4
- package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
- package/src/ui/components/patterns/confirm.tsx +2 -31
- package/src/ui/components/patterns/content-header.tsx +42 -141
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/form.tsx +15 -15
- package/src/ui/components/patterns/list.tsx +55 -34
- package/src/ui/components/patterns/page-state.tsx +81 -51
- package/src/ui/components/patterns/page.tsx +228 -97
- package/src/ui/components/patterns/state-surface.tsx +262 -0
- package/src/ui/components/patterns/surface-header.tsx +204 -0
- package/src/ui/components/patterns/trigger.tsx +9 -10
- package/src/ui/components/patterns/view.tsx +14 -16
- package/src/ui/components/primitives/alert.tsx +1 -33
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +2 -43
- package/src/ui/components/primitives/button-group.tsx +34 -8
- package/src/ui/components/primitives/button.tsx +31 -35
- package/src/ui/components/primitives/control.ts +69 -0
- package/src/ui/components/primitives/dot.tsx +1 -30
- package/src/ui/components/primitives/input-group.tsx +11 -8
- package/src/ui/components/primitives/item.tsx +3 -1
- package/src/ui/components/primitives/menu.tsx +1 -7
- package/src/ui/components/primitives/pagination.tsx +16 -8
- package/src/ui/components/primitives/select.tsx +2 -2
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +4 -1
- package/src/ui/components/primitives/tabs.tsx +5 -3
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/docs/content/action-form.md +26 -0
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +35 -2
- package/src/ui/docs/content/action-trigger.md +5 -4
- package/src/ui/docs/content/action-view.md +3 -2
- package/src/ui/docs/content/alert.md +16 -4
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +48 -17
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/content.md +5 -4
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/page.md +160 -37
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/semantic-context.md +3 -2
- package/src/ui/docs/content/sidebar.md +2 -42
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/drivers/react.tsx +1 -6
- package/src/ui/meta.ts +8 -8
- package/src/ui/react.tsx +16 -18
- 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 {
|
|
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 {
|
|
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
|
-
}
|