@softize/opus 27.0.0 → 28.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 +73 -0
- package/bin/lib/check.mjs +139 -30
- package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +14 -8
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
- package/docs/adr/0057-doc-browser-permite-busca-no-chrome-do-consumidor.md +39 -5
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
- package/docs/adr/README.md +2 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
- package/src/ui/components/patterns/list.tsx +62 -53
- package/src/ui/components/patterns/page.tsx +146 -38
- package/src/ui/components/patterns/presentation.tsx +44 -59
- package/src/ui/components/patterns/structural-header.tsx +42 -6
- package/src/ui/components/patterns/surface-anatomy.ts +52 -22
- package/src/ui/components/patterns/surface.tsx +2 -3
- package/src/ui/components/primitives/control.ts +2 -2
- package/src/ui/components/primitives/dialog.tsx +7 -2
- package/src/ui/components/primitives/drawer.tsx +7 -2
- package/src/ui/components/primitives/icon-picker.tsx +271 -34
- package/src/ui/components/primitives/pagination.tsx +5 -4
- package/src/ui/components/primitives/tabs.tsx +15 -2
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/docs/DocBrowser.tsx +122 -12
- package/src/ui/docs/content/action-list.md +11 -2
- package/src/ui/docs/content/button.md +7 -7
- package/src/ui/docs/content/compositions.md +35 -23
- package/src/ui/docs/content/dialog.md +18 -6
- package/src/ui/docs/content/documentation-portal.md +40 -1
- package/src/ui/docs/content/drawer.md +17 -5
- package/src/ui/docs/content/icon-picker.md +37 -19
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/page.md +169 -69
- package/src/ui/docs/content/pagination.md +7 -8
- package/src/ui/docs/content/presentation.md +4 -3
- package/src/ui/docs/content/surface.md +8 -5
- package/src/ui/docs/content/tabs.md +12 -4
- package/src/ui/docs/types.ts +2 -0
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +9 -1
|
@@ -15,15 +15,25 @@
|
|
|
15
15
|
import {
|
|
16
16
|
Fragment,
|
|
17
17
|
useEffect,
|
|
18
|
+
useId,
|
|
18
19
|
useMemo,
|
|
19
20
|
useRef,
|
|
20
21
|
useState,
|
|
22
|
+
useSyncExternalStore,
|
|
21
23
|
type ReactNode,
|
|
22
24
|
} from "react";
|
|
23
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
ArrowLeft,
|
|
27
|
+
ArrowRight,
|
|
28
|
+
ExternalLink,
|
|
29
|
+
Menu,
|
|
30
|
+
Search,
|
|
31
|
+
X,
|
|
32
|
+
} from "lucide-react";
|
|
24
33
|
import type { DocEntry, DocGroup, DocSection } from "./types.ts";
|
|
25
34
|
import { Pane, Split, type PaneSize } from "../components/patterns/split.tsx";
|
|
26
35
|
import { Input } from "../components/primitives/input.tsx";
|
|
36
|
+
import { Button } from "../components/primitives/button.tsx";
|
|
27
37
|
import {
|
|
28
38
|
Breadcrumb,
|
|
29
39
|
BreadcrumbList,
|
|
@@ -109,6 +119,8 @@ export interface DocBrowserProps {
|
|
|
109
119
|
}>;
|
|
110
120
|
/** Seções e grupos começam recolhidos; a busca revela resultados sem mudar a escolha manual. */
|
|
111
121
|
sidebarInitiallyCollapsed?: boolean;
|
|
122
|
+
/** Em telas de até 48rem, oferece navegação recolhível no lugar da lateral fixa. Default false. */
|
|
123
|
+
collapsibleNavigation?: boolean;
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
function documentNavigation(
|
|
@@ -142,11 +154,15 @@ function EditorialDocument({
|
|
|
142
154
|
const [headings, setHeadings] = useState<
|
|
143
155
|
Array<{ id: string; title: string }>
|
|
144
156
|
>([]);
|
|
145
|
-
const entries = documentEntries(
|
|
157
|
+
const entries = documentEntries(
|
|
158
|
+
sections.filter((section) => section.readingSequence !== false),
|
|
159
|
+
);
|
|
146
160
|
const index = entries.findIndex(({ entry }) => entry.slug === page.slug);
|
|
147
161
|
const previous = index > 0 ? entries[index - 1]?.entry : undefined;
|
|
148
162
|
const next = index >= 0 ? entries[index + 1]?.entry : undefined;
|
|
149
|
-
const context =
|
|
163
|
+
const context = documentEntries(sections).find(
|
|
164
|
+
({ entry }) => entry.slug === page.slug,
|
|
165
|
+
)?.context;
|
|
150
166
|
|
|
151
167
|
useEffect(() => {
|
|
152
168
|
const article = articleRef.current;
|
|
@@ -303,8 +319,10 @@ type SidebarSection = Omit<DocSection, "groups"> & {
|
|
|
303
319
|
function filterSections(
|
|
304
320
|
sections: DocSection[],
|
|
305
321
|
query: string,
|
|
322
|
+
basePath: string,
|
|
306
323
|
): SidebarSection[] {
|
|
307
324
|
const terms = searchable(query).split(/\s+/).filter(Boolean);
|
|
325
|
+
const namespace = JSON.stringify(basePath);
|
|
308
326
|
// Identidade vem do manifesto completo, antes de remover ramos pela busca.
|
|
309
327
|
const indexGroups = (groups: DocGroup[], parent: string): SidebarGroup[] =>
|
|
310
328
|
groups.map((group, index) => {
|
|
@@ -317,8 +335,8 @@ function filterSections(
|
|
|
317
335
|
});
|
|
318
336
|
const indexedSections = sections.map((section, sectionIndex) => ({
|
|
319
337
|
...section,
|
|
320
|
-
navigationKey:
|
|
321
|
-
groups: indexGroups(section.groups,
|
|
338
|
+
navigationKey: `${namespace}:section:${sectionIndex}`,
|
|
339
|
+
groups: indexGroups(section.groups, `${namespace}:group:${sectionIndex}`),
|
|
322
340
|
}));
|
|
323
341
|
const matchingGroups = (groups: SidebarGroup[]): SidebarGroup[] =>
|
|
324
342
|
groups.flatMap((group) => {
|
|
@@ -343,6 +361,7 @@ function DocSidebarTree({
|
|
|
343
361
|
sections,
|
|
344
362
|
activeSlug,
|
|
345
363
|
onSelect,
|
|
364
|
+
onNavigate = navigate,
|
|
346
365
|
forceExpanded = false,
|
|
347
366
|
initiallyCollapsed = false,
|
|
348
367
|
links = [],
|
|
@@ -350,6 +369,7 @@ function DocSidebarTree({
|
|
|
350
369
|
sections: SidebarSection[];
|
|
351
370
|
activeSlug?: string;
|
|
352
371
|
onSelect: (slug: string) => void;
|
|
372
|
+
onNavigate?: (href: string) => void;
|
|
353
373
|
forceExpanded?: boolean;
|
|
354
374
|
initiallyCollapsed?: boolean;
|
|
355
375
|
links?: DocBrowserProps["sidebarLinks"];
|
|
@@ -496,7 +516,7 @@ function DocSidebarTree({
|
|
|
496
516
|
onClick={
|
|
497
517
|
link.newTab || /^https?:\/\//i.test(link.href)
|
|
498
518
|
? undefined
|
|
499
|
-
: () =>
|
|
519
|
+
: () => onNavigate(link.href)
|
|
500
520
|
}
|
|
501
521
|
label={link.label}
|
|
502
522
|
icon={
|
|
@@ -510,6 +530,17 @@ function DocSidebarTree({
|
|
|
510
530
|
);
|
|
511
531
|
}
|
|
512
532
|
|
|
533
|
+
const compactNavigationQuery = "(max-width: 48rem)";
|
|
534
|
+
const subscribeCompactNavigation = (notify: () => void): (() => void) => {
|
|
535
|
+
const media = window.matchMedia(compactNavigationQuery);
|
|
536
|
+
media.addEventListener("change", notify);
|
|
537
|
+
return () => media.removeEventListener("change", notify);
|
|
538
|
+
};
|
|
539
|
+
const compactNavigationSnapshot = (): boolean =>
|
|
540
|
+
window.matchMedia(compactNavigationQuery).matches;
|
|
541
|
+
const serverNavigationSnapshot = (): boolean => false;
|
|
542
|
+
const skipNavigationSubscription = (): (() => void) => () => {};
|
|
543
|
+
|
|
513
544
|
export function DocBrowser({
|
|
514
545
|
basePath = "/docs",
|
|
515
546
|
path: controlledPath,
|
|
@@ -525,8 +556,19 @@ export function DocBrowser({
|
|
|
525
556
|
content,
|
|
526
557
|
sidebarLinks = [],
|
|
527
558
|
sidebarInitiallyCollapsed = false,
|
|
559
|
+
collapsibleNavigation = false,
|
|
528
560
|
}: DocBrowserProps): React.ReactElement {
|
|
529
561
|
const [sidebarQuery, setSidebarQuery] = useState("");
|
|
562
|
+
const watchViewport = sidebar && collapsibleNavigation;
|
|
563
|
+
const narrowViewport = useSyncExternalStore(
|
|
564
|
+
watchViewport ? subscribeCompactNavigation : skipNavigationSubscription,
|
|
565
|
+
watchViewport ? compactNavigationSnapshot : serverNavigationSnapshot,
|
|
566
|
+
serverNavigationSnapshot,
|
|
567
|
+
);
|
|
568
|
+
const compactNavigation = collapsibleNavigation && narrowViewport;
|
|
569
|
+
const [navigationOpen, setNavigationOpen] = useState(false);
|
|
570
|
+
const navigationId = useId();
|
|
571
|
+
const navigationButtonRef = useRef<HTMLButtonElement>(null);
|
|
530
572
|
// Standalone: o path vem do router da casa (`usePathname` — pathname É o estado).
|
|
531
573
|
// Controlado: o host manda, e este valor é ignorado. O hook roda sempre (regra dos
|
|
532
574
|
// hooks); o que muda é quem vence no `??`.
|
|
@@ -542,9 +584,10 @@ export function DocBrowser({
|
|
|
542
584
|
const searchOptions = typeof search === "object" ? search : {};
|
|
543
585
|
const query =
|
|
544
586
|
searchOptions.placement === "external" ? searchOptions.value : sidebarQuery;
|
|
587
|
+
const previousQuery = useRef(query);
|
|
545
588
|
const visibleSections = useMemo(
|
|
546
|
-
() => filterSections(sections, query),
|
|
547
|
-
[query, sections],
|
|
589
|
+
() => filterSections(sections, query, basePath),
|
|
590
|
+
[query, sections, basePath],
|
|
548
591
|
);
|
|
549
592
|
const hasCustomContent = content !== undefined;
|
|
550
593
|
const page = hasCustomContent
|
|
@@ -555,7 +598,40 @@ export function DocBrowser({
|
|
|
555
598
|
|
|
556
599
|
// `navigate` emite `popstate`, então o host é notificado nos dois modos — inclusive o
|
|
557
600
|
// que ainda escuta o evento na unha, que era como este componente avisava antes.
|
|
558
|
-
const go = (to: string): void =>
|
|
601
|
+
const go = (to: string): void => {
|
|
602
|
+
if (compactNavigation) {
|
|
603
|
+
setNavigationOpen(false);
|
|
604
|
+
navigationButtonRef.current?.focus();
|
|
605
|
+
}
|
|
606
|
+
navigate(to);
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
useEffect(() => {
|
|
610
|
+
setNavigationOpen(false);
|
|
611
|
+
if (
|
|
612
|
+
collapsibleNavigation &&
|
|
613
|
+
document.getElementById(navigationId)?.contains(document.activeElement)
|
|
614
|
+
)
|
|
615
|
+
navigationButtonRef.current?.focus();
|
|
616
|
+
}, [path, collapsibleNavigation, navigationId]);
|
|
617
|
+
|
|
618
|
+
useEffect(() => {
|
|
619
|
+
if (previousQuery.current === query) return;
|
|
620
|
+
previousQuery.current = query;
|
|
621
|
+
if (collapsibleNavigation && query.trim()) setNavigationOpen(true);
|
|
622
|
+
}, [collapsibleNavigation, query]);
|
|
623
|
+
|
|
624
|
+
useEffect(() => {
|
|
625
|
+
if (!sidebar || !compactNavigation || !navigationOpen) return;
|
|
626
|
+
const closeOnEscape = (event: KeyboardEvent): void => {
|
|
627
|
+
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
setNavigationOpen(false);
|
|
630
|
+
navigationButtonRef.current?.focus();
|
|
631
|
+
};
|
|
632
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
633
|
+
return () => document.removeEventListener("keydown", closeOnEscape);
|
|
634
|
+
}, [sidebar, compactNavigation, navigationOpen]);
|
|
559
635
|
|
|
560
636
|
useEffect(() => {
|
|
561
637
|
if (!hasCustomContent && requestedSlug !== slug)
|
|
@@ -592,9 +668,14 @@ export function DocBrowser({
|
|
|
592
668
|
<div className="mx-auto max-w-3xl px-8 py-8">{page?.render()}</div>
|
|
593
669
|
);
|
|
594
670
|
|
|
595
|
-
|
|
671
|
+
const browser = (
|
|
596
672
|
<Split className="h-full">
|
|
597
|
-
<Pane
|
|
673
|
+
<Pane
|
|
674
|
+
id={collapsibleNavigation ? navigationId : undefined}
|
|
675
|
+
initialSize={compactNavigation ? undefined : sidebarSize}
|
|
676
|
+
grow={compactNavigation}
|
|
677
|
+
className={compactNavigation && !navigationOpen ? "hidden" : undefined}
|
|
678
|
+
>
|
|
598
679
|
<Sidebar className="w-full">
|
|
599
680
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
600
681
|
{search !== false && searchOptions.placement !== "external" && (
|
|
@@ -632,6 +713,7 @@ export function DocBrowser({
|
|
|
632
713
|
forceExpanded={query.trim() !== ""}
|
|
633
714
|
initiallyCollapsed={sidebarInitiallyCollapsed}
|
|
634
715
|
links={sidebarLinks}
|
|
716
|
+
onNavigate={go}
|
|
635
717
|
onSelect={(nextSlug) => go(`${basePath}/${nextSlug}`)}
|
|
636
718
|
/>
|
|
637
719
|
</div>
|
|
@@ -641,10 +723,38 @@ export function DocBrowser({
|
|
|
641
723
|
<Pane
|
|
642
724
|
key={hasCustomContent ? path : page?.slug}
|
|
643
725
|
grow
|
|
644
|
-
className=
|
|
726
|
+
className={
|
|
727
|
+
compactNavigation && navigationOpen ? "hidden" : "overflow-y-auto"
|
|
728
|
+
}
|
|
645
729
|
>
|
|
646
730
|
{readingContent}
|
|
647
731
|
</Pane>
|
|
648
732
|
</Split>
|
|
649
733
|
);
|
|
734
|
+
|
|
735
|
+
if (!collapsibleNavigation) return browser;
|
|
736
|
+
return (
|
|
737
|
+
<div data-slot="doc-browser" className="flex h-full min-h-0 flex-col">
|
|
738
|
+
{compactNavigation && (
|
|
739
|
+
<div className="shrink-0 border-b border-border p-2">
|
|
740
|
+
<Button
|
|
741
|
+
ref={navigationButtonRef}
|
|
742
|
+
context="neutral"
|
|
743
|
+
variant="ghost"
|
|
744
|
+
aria-controls={navigationId}
|
|
745
|
+
aria-expanded={navigationOpen}
|
|
746
|
+
onClick={() => setNavigationOpen((open) => !open)}
|
|
747
|
+
>
|
|
748
|
+
{navigationOpen ? (
|
|
749
|
+
<X aria-hidden="true" />
|
|
750
|
+
) : (
|
|
751
|
+
<Menu aria-hidden="true" />
|
|
752
|
+
)}
|
|
753
|
+
Navegação
|
|
754
|
+
</Button>
|
|
755
|
+
</div>
|
|
756
|
+
)}
|
|
757
|
+
{browser}
|
|
758
|
+
</div>
|
|
759
|
+
);
|
|
650
760
|
}
|
|
@@ -536,7 +536,7 @@ render(
|
|
|
536
536
|
| `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Ela aparece depois dos filtros inline e imediatamente antes do acesso aos filtros avançados. A busca participa da contagem de controles inline: com mais de um controle, a barra exibe as labels; quando ela é o único recorte, sua label é omitida. |
|
|
537
537
|
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
538
538
|
| `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
|
|
539
|
-
| `grouping` | `{ fields }` — dimensões enumeráveis disponíveis em **Agrupar por**. Cada nome referencia um filtro `select` ou `lookup` com até 12 opções declaradas ou resolvidas por `filterOptions`. Não define agrupamento inicial.
|
|
539
|
+
| `grouping` | `{ fields }` — dimensões enumeráveis disponíveis em **Agrupar por**. Cada nome referencia um filtro `select` ou `lookup` com até 12 opções declaradas ou resolvidas por `filterOptions`. Não define agrupamento inicial. |
|
|
540
540
|
|
|
541
541
|
## useListAction
|
|
542
542
|
|
|
@@ -547,6 +547,14 @@ declarativo (`items`, `total`, `error`, `isLoading`, `isFetching`, `refetch`) so
|
|
|
547
547
|
chamar a action. `options.enabled` suspende a consulta enquanto for `false`; sem a opção, vale
|
|
548
548
|
`true`.
|
|
549
549
|
|
|
550
|
+
## Controles da visualização
|
|
551
|
+
|
|
552
|
+
Controles específicos da visualização ativa, como a orientação de um comparativo, usam
|
|
553
|
+
`viewControls`. O slot fica imediatamente após o seletor de visualização. As configurações da lista
|
|
554
|
+
aparecem antes desse conjunto. O consumidor controla seu estado e quando exibi-lo. Isso mantém a
|
|
555
|
+
configuração visual junto à escolha da visualização, sem deslocar ações sobre o recorte
|
|
556
|
+
para fora de `toolbarActions`.
|
|
557
|
+
|
|
550
558
|
## Propriedades de ActionList
|
|
551
559
|
|
|
552
560
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -555,7 +563,7 @@ chamar a action. `options.enabled` suspende a consulta enquanto for `false`; sem
|
|
|
555
563
|
| `input` | `TInput` | | O escopo base, como `{ workspaceId }`. A toolbar acrescenta busca, filtros e paginação sem sobrescrever essas chaves. |
|
|
556
564
|
| `layout` | `'flow' \| 'fill'` | `'fill'` como filha direta de `PageBody`, `SurfaceBody` ou do corpo da `Presentation` em uma página `fill`; `'flow'` nos demais casos | Em `fill`, ocupa a altura de um ancestral limitado e rola somente a região de resultados; na tabela, o cabeçalho fica fixo. Sem ancestral limitado, equivale a `flow`. |
|
|
557
565
|
| `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
|
|
558
|
-
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para filtros select/lookup. Em uma dimensão de agrupamento, a coleção precisa ser completa e ter de 1 a 12 itens.
|
|
566
|
+
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para filtros select/lookup. Em uma dimensão de agrupamento, a coleção precisa ser completa e ter de 1 a 12 itens. |
|
|
559
567
|
| `filterDefaults` | `Record<string, string>` | | Valores de repouso aplicados à consulta; o `X` só aparece quando o filtro diverge deles e retorna a eles. |
|
|
560
568
|
| `filterIcons` | `Record<string, ReactNode>` | | Ícones leading dos filtros, indexados pelo nome declarado no contrato. |
|
|
561
569
|
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3; sectionIcons?: Record<string, ReactNode>; sectionVariant?: 'plain' \| 'framed' }` | `{ columns: 1, sectionVariant: 'plain' }` | Define as colunas, os ícones e o tratamento dos grupos; o Opus deriva a largura correspondente do diálogo. |
|
|
@@ -575,5 +583,6 @@ chamar a action. `options.enabled` suspende a consulta enquanto for `false`; sem
|
|
|
575
583
|
| `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
|
|
576
584
|
| `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
|
|
577
585
|
| `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
|
|
586
|
+
| `viewControls` | `ReactNode` | | Controles da visualização ativa, imediatamente após o seletor de visualização. |
|
|
578
587
|
| `toolbarActions` | `ReactNode` | | Operações ligadas ao recorte atual, no fim da barra e separadas do grupo de controles. Os nós são renderizados como recebidos: nome acessível e tooltip de uma ação somente com ícone ficam com o consumidor. |
|
|
579
588
|
| `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita). A partir de duas, entram em um grupo com intervalo compacto; uma só fica solta, sem grupo. Cliques ali não disparam o `onRowClick`. |
|
|
@@ -32,22 +32,22 @@ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em
|
|
|
32
32
|
| Nome | Medida | Uso |
|
|
33
33
|
| --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
34
|
| `xs` | 1.5rem | Ação dentro de um campo (`InputGroupButton`). |
|
|
35
|
-
| `sm` | 2rem |
|
|
36
|
-
| `default` | 2.25rem |
|
|
35
|
+
| `sm` | 2rem | Headers, criação de coleções e operações densas de toolbar ou seção. |
|
|
36
|
+
| `default` | 2.25rem | Footers de Dialog e Drawer e controles comuns; é também a linha de `Input`. |
|
|
37
37
|
| `lg` | 2.5rem | Chamada principal com mais área de toque. |
|
|
38
38
|
| `icon-xs` | 1.5rem | Ação só de ícone dentro de um campo ou de uma linha densa; é o quadrado de `ActionTrigger` com `icon`. |
|
|
39
|
-
| `icon-sm` |
|
|
39
|
+
| `icon-sm` | 2rem | Ação só de ícone em composições compactas que precisam de mais presença; também é o quadrado das setas do pager de `ActionList`. |
|
|
40
40
|
| `icon` | 2.25rem | Ação só de ícone na linha padrão. |
|
|
41
41
|
| `icon-lg` | 2.5rem | Ação só de ícone ao lado de um `lg`. |
|
|
42
42
|
|
|
43
43
|
Escolha o tamanho pela região, não pela importância visual: `variant` e `context` resolvem a
|
|
44
|
-
hierarquia da ação. Headers de Page,
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
hierarquia da ação. Headers de Page, Dialog, Drawer e Surface usam `sm`/`icon-sm`. A criação
|
|
45
|
+
no cabeçalho de uma coleção usa `sm` textual, sem ícone. Footers de
|
|
46
|
+
[`Dialog`](/ui/dialog) e [`Drawer`](/ui/drawer) conservam `default`. Operações densas de toolbar ou de seção usam `sm`;
|
|
47
47
|
ações dentro de linha ou célula usam `xs` ou `icon-xs`. Assim a mesma decisão mantém a mesma altura
|
|
48
48
|
mesmo quando uma superfície troca uma ação secundária por uma primária.
|
|
49
49
|
|
|
50
|
-
O retorno de `PageBack` usa `icon`. O fechamento de Dialog e Drawer é um `CloseButton` circular
|
|
50
|
+
O retorno de `PageBack` usa `icon-sm`. O fechamento de Dialog e Drawer é um `CloseButton` circular
|
|
51
51
|
(`shape="pill"`), `neutral` e `subtle`, no quadrado `icon-sm` por padrão, ou `icon-xs` com
|
|
52
52
|
`closeSize="xs"`.
|
|
53
53
|
|
|
@@ -24,8 +24,8 @@ Cada família dá um nome próprio a cada região. A tabela mostra a correspond
|
|
|
24
24
|
| Navegação | `PageNavigation` ou `PageBack` | `DialogNavigation` | `DrawerNavigation` | Não existe |
|
|
25
25
|
| Título no cabeçalho | `PageTitle` | `DialogTitle` | `DrawerTitle` | `SurfaceTitle` |
|
|
26
26
|
| Ações do cabeçalho | `PageActions` | `DialogActions` | `DrawerActions` | `SurfaceActions` |
|
|
27
|
-
| Intro | `
|
|
28
|
-
| Ações da intro |
|
|
27
|
+
| Intro | `PageIntro` no início do `PageBody` | `DialogIntro` | `DrawerIntro` | Não existe |
|
|
28
|
+
| Ações da intro | `PageIntroActions` | `DialogIntroActions` | `DrawerIntroActions` | Não existe |
|
|
29
29
|
| Corpo | `PageBody` | `DialogBody` | `DrawerBody` | `SurfaceBody` |
|
|
30
30
|
| Rodapé | `PageFooter` | `DialogFooter` | `DrawerFooter` | `SurfaceFooter` |
|
|
31
31
|
| Divisores | Antes do `PageFooter` | `sectioned` (padrão) | `sectioned` (padrão) | `sectioned` |
|
|
@@ -97,7 +97,7 @@ function DialogWithIntro({ layout, label }) {
|
|
|
97
97
|
<DialogContent layout={layout}>
|
|
98
98
|
<DialogHeader>
|
|
99
99
|
<DialogActions>
|
|
100
|
-
<Button variant="ghost" size="icon" aria-label="Abrir conversa">
|
|
100
|
+
<Button variant="ghost" size="icon-sm" aria-label="Abrir conversa">
|
|
101
101
|
<MessageCircle />
|
|
102
102
|
</Button>
|
|
103
103
|
</DialogActions>
|
|
@@ -142,7 +142,7 @@ function CompactDialog({ layout, label }) {
|
|
|
142
142
|
<Button variant="outline" size="sm">
|
|
143
143
|
Ver histórico
|
|
144
144
|
</Button>
|
|
145
|
-
<Button variant="ghost" size="icon" aria-label="Abrir conversa">
|
|
145
|
+
<Button variant="ghost" size="icon-sm" aria-label="Abrir conversa">
|
|
146
146
|
<MessageCircle />
|
|
147
147
|
</Button>
|
|
148
148
|
</DialogActions>
|
|
@@ -197,7 +197,7 @@ function OrderField() {
|
|
|
197
197
|
function CollapseButton() {
|
|
198
198
|
return (
|
|
199
199
|
<DrawerClose asChild>
|
|
200
|
-
<Button variant="ghost" size="icon" aria-label="Recolher painel">
|
|
200
|
+
<Button variant="ghost" size="icon-sm" aria-label="Recolher painel">
|
|
201
201
|
<ChevronsRight />
|
|
202
202
|
</Button>
|
|
203
203
|
</DrawerClose>
|
|
@@ -216,7 +216,7 @@ function DrawerWithIntro({ layout, label }) {
|
|
|
216
216
|
<CollapseButton />
|
|
217
217
|
</DrawerNavigation>
|
|
218
218
|
<DrawerActions>
|
|
219
|
-
<Button variant="ghost" size="icon" aria-label="Abrir conversa">
|
|
219
|
+
<Button variant="ghost" size="icon-sm" aria-label="Abrir conversa">
|
|
220
220
|
<MessageCircle />
|
|
221
221
|
</Button>
|
|
222
222
|
</DrawerActions>
|
|
@@ -261,7 +261,7 @@ function CompactDrawer({ layout, label }) {
|
|
|
261
261
|
<Button variant="outline" size="sm">
|
|
262
262
|
Ver histórico
|
|
263
263
|
</Button>
|
|
264
|
-
<Button variant="ghost" size="icon" aria-label="Abrir conversa">
|
|
264
|
+
<Button variant="ghost" size="icon-sm" aria-label="Abrir conversa">
|
|
265
265
|
<MessageCircle />
|
|
266
266
|
</Button>
|
|
267
267
|
</DrawerActions>
|
|
@@ -300,8 +300,8 @@ painel.
|
|
|
300
300
|
|
|
301
301
|
## Page
|
|
302
302
|
|
|
303
|
-
|
|
304
|
-
página, e uma `
|
|
303
|
+
Na composição com intro, o `PageHeader` guarda a navegação e as ações da
|
|
304
|
+
página, e uma `PageIntro` abre o `PageBody` com o título e as ações do conteúdo. A página
|
|
305
305
|
precisa de exatamente um heading principal, por isso o `PageHeader` dessa composição não leva
|
|
306
306
|
`PageTitle`.
|
|
307
307
|
|
|
@@ -322,27 +322,20 @@ render(
|
|
|
322
322
|
</Breadcrumb>
|
|
323
323
|
</PageNavigation>
|
|
324
324
|
<PageActions>
|
|
325
|
-
<Button variant="ghost" size="icon" aria-label="Abrir conversa">
|
|
325
|
+
<Button variant="ghost" size="icon-sm" aria-label="Abrir conversa">
|
|
326
326
|
<MessageCircle />
|
|
327
327
|
</Button>
|
|
328
328
|
</PageActions>
|
|
329
329
|
</PageHeader>
|
|
330
330
|
<PageBody>
|
|
331
|
-
<
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
actions={
|
|
336
|
-
<Button variant="outline" size="sm">
|
|
337
|
-
Ver histórico
|
|
338
|
-
</Button>
|
|
339
|
-
}
|
|
340
|
-
>
|
|
331
|
+
<PageIntro>
|
|
332
|
+
<PageTitle>Editar ordem de serviço</PageTitle>
|
|
333
|
+
<PageIntroActions><Button variant="outline" size="sm">Ver histórico</Button></PageIntroActions>
|
|
334
|
+
</PageIntro>
|
|
341
335
|
<div className="space-y-2">
|
|
342
336
|
<Label htmlFor="page-intro-owner">Responsável</Label>
|
|
343
337
|
<Input id="page-intro-owner" defaultValue="Marina Alves" />
|
|
344
338
|
</div>
|
|
345
|
-
</Surface>
|
|
346
339
|
</PageBody>
|
|
347
340
|
<PageFooter>
|
|
348
341
|
<ButtonGroup mode="spaced">
|
|
@@ -380,7 +373,7 @@ render(
|
|
|
380
373
|
<Button variant="outline" size="sm">
|
|
381
374
|
Ver histórico
|
|
382
375
|
</Button>
|
|
383
|
-
<Button variant="ghost" size="icon" aria-label="Abrir conversa">
|
|
376
|
+
<Button variant="ghost" size="icon-sm" aria-label="Abrir conversa">
|
|
384
377
|
<MessageCircle />
|
|
385
378
|
</Button>
|
|
386
379
|
</PageActions>
|
|
@@ -449,7 +442,7 @@ render(
|
|
|
449
442
|
<Button variant="outline" size="sm">
|
|
450
443
|
Ver histórico
|
|
451
444
|
</Button>
|
|
452
|
-
<Button variant="ghost" size="icon" aria-label="Abrir conversa">
|
|
445
|
+
<Button variant="ghost" size="icon-sm" aria-label="Abrir conversa">
|
|
453
446
|
<MessageCircle />
|
|
454
447
|
</Button>
|
|
455
448
|
</SurfaceActions>
|
|
@@ -477,3 +470,22 @@ Os dois panes recebem conteúdos diferentes sem mudar de componente: o primeiro
|
|
|
477
470
|
[`Sidebar`](/ui/sidebar), que já tem anatomia própria, e o segundo, uma `Surface` completa. A
|
|
478
471
|
rolagem também pertence ao conteúdo: a `Surface` ocupa a altura do pane com `flex h-full flex-col`,
|
|
479
472
|
e o `SurfaceBody` declara `min-h-0 flex-1 overflow-y-auto` para rolar entre o cabeçalho e o rodapé.
|
|
473
|
+
|
|
474
|
+
## Ritmo entre regiões e blocos
|
|
475
|
+
|
|
476
|
+
Surface (incluindo cards e seções), Page, Dialog, Drawer e Tabs organizam o conteúdo
|
|
477
|
+
com intervalos de `1rem`. Os bodies separam seus blocos diretos; grupos internos podem
|
|
478
|
+
ter densidade própria. DetailGroup e Separator não criam margens externas.
|
|
479
|
+
|
|
480
|
+
Sem delimitação, Surface e Tabs não adicionam padding inferior. Uma Surface com moldura
|
|
481
|
+
ou elevação, assim como Dialog e Drawer, reserva `1rem` até a borda. No layout contínuo,
|
|
482
|
+
o intervalo vem da região anterior. No seccionado, há `1rem` entre cada divisória e o
|
|
483
|
+
conteúdo adjacente. Não some padding e gap para expressar o mesmo intervalo.
|
|
484
|
+
|
|
485
|
+
Page conserva os recuos laterais e a largura de leitura. Seu body não acrescenta padding
|
|
486
|
+
inferior quando termina a página; antes de um footer seccionado, reserva `1rem` até a linha.
|
|
487
|
+
O modo `flush` remove recuos do body para uma lista ou tabela controlar sua própria borda,
|
|
488
|
+
mas mantém o intervalo entre blocos.
|
|
489
|
+
|
|
490
|
+
Ao migrar, remova `space-y`, `gap`, `pt` e `pb` locais que apenas reconstruíam esses defaults
|
|
491
|
+
nos slots. Preserve classes de layout internas, como uma grade ou um grupo de campos compacto.
|
|
@@ -48,7 +48,7 @@ distinta da página.
|
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
O corpo cresce até o limite da janela e passa a rolar; cabeçalho e rodapé permanecem visíveis.
|
|
51
|
-
Seus filhos diretos recebem o ritmo estrutural de `
|
|
51
|
+
Seus filhos diretos recebem o ritmo estrutural de `1rem`. Um grupo compacto, uma grade ou uma
|
|
52
52
|
linha declara a própria relação em um wrapper filho; `flush` fica reservado ao conteúdo que
|
|
53
53
|
controla todo o inset.
|
|
54
54
|
O backdrop escurece a página sem aplicar desfoque.
|
|
@@ -126,13 +126,13 @@ fluxo modal encadeado, em que a superfície permanece aberta e troca de etapa.
|
|
|
126
126
|
<DialogContent>
|
|
127
127
|
<DialogHeader>
|
|
128
128
|
<DialogNavigation>
|
|
129
|
-
<Button variant="ghost" size="icon" aria-label="Voltar para a lista">
|
|
129
|
+
<Button variant="ghost" size="icon-sm" aria-label="Voltar para a lista">
|
|
130
130
|
<ArrowLeft />
|
|
131
131
|
</Button>
|
|
132
132
|
</DialogNavigation>
|
|
133
133
|
<DialogTitle>Evento 4821</DialogTitle>
|
|
134
134
|
<DialogActions>
|
|
135
|
-
<Button variant="ghost" size="icon" aria-label="Mais ações">
|
|
135
|
+
<Button variant="ghost" size="icon-sm" aria-label="Mais ações">
|
|
136
136
|
<Ellipsis />
|
|
137
137
|
</Button>
|
|
138
138
|
</DialogActions>
|
|
@@ -146,9 +146,10 @@ fluxo modal encadeado, em que a superfície permanece aberta e troca de etapa.
|
|
|
146
146
|
</Dialog>
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
O header compartilha a geometria de Page e Surface delimitada: padding de `1rem` e
|
|
150
|
+
linha mínima de `2rem`, nos dois layouts. Declare `size="sm"` nos botões textuais e
|
|
151
|
+
`size="icon-sm"` nos de ícone (`2rem`), incluindo o retorno. `CloseButton` já usa
|
|
152
|
+
`icon-sm` por padrão. Conteúdo maior pode expandir a linha; rodapés preservam sua escala.
|
|
152
153
|
|
|
153
154
|
O cabeçalho aceita `DialogDescription` como subheading opcional para orientar sobre a finalidade.
|
|
154
155
|
Omita quando repetir o título. Condições, regras de negócio e consequências necessárias à decisão
|
|
@@ -656,3 +657,14 @@ comparar as duas formas em cada `layout`, abra [Composições](/ui/compositions)
|
|
|
656
657
|
| `id` | `string` | | Identificador para associação acessível explícita. |
|
|
657
658
|
| `className` | `string` | | Ajusta a apresentação do parágrafo. |
|
|
658
659
|
| `…props` | `ComponentProps<"p">` | | Atributos nativos. |
|
|
660
|
+
|
|
661
|
+
## Espaçamento do corpo
|
|
662
|
+
|
|
663
|
+
O body separa os blocos diretos por `1rem`. Dialog e Drawer são superfícies delimitadas:
|
|
664
|
+
o conteúdo mantém `1rem` até a borda inferior, mesmo sem uma propriedade `framed`.
|
|
665
|
+
Em `sectioned`, o corpo tem `1rem` entre o conteúdo e cada divisória. Em `continuous`,
|
|
666
|
+
o cabeçalho fornece o intervalo superior; sem cabeçalho, o próprio corpo abre esse recuo.
|
|
667
|
+
Use `flush` quando o conteúdo controlar seus próprios recuos.
|
|
668
|
+
|
|
669
|
+
Na intro, `DialogTitle` usa a mesma escala de `PageTitle` (`1.875rem`). No header
|
|
670
|
+
compacto, o título conserva a escala de `1.125rem`. O nível semântico não altera essas medidas.
|
|
@@ -88,7 +88,45 @@ que a pessoa possa consultar, inclusive aquelas disponíveis por URL direta.
|
|
|
88
88
|
A adaptação do cabeçalho, da leitura editorial e dos catálogos à largura da tela é uma exceção
|
|
89
89
|
restrita ao portal documental. A UI das aplicações e os componentes dos exemplos vivos preservam
|
|
90
90
|
o contrato desktop a partir de `64rem`. O aplicativo organiza a moldura do portal em telas estreitas;
|
|
91
|
-
`DocBrowser` não
|
|
91
|
+
`DocBrowser` não transforma os exemplos em layouts móveis.
|
|
92
|
+
|
|
93
|
+
### Navegação em telas estreitas
|
|
94
|
+
|
|
95
|
+
Em portais autônomos, use `collapsibleNavigation` para oferecer um menu recolhível em telas
|
|
96
|
+
de até `48rem`. O botão Navegação abre a árvore na área disponível, no lugar da leitura.
|
|
97
|
+
Selecionar um destino interno ou pressionar Escape recolhe o menu. A busca global abre
|
|
98
|
+
os resultados quando a consulta muda; uma consulta já preenchida na montagem não abre
|
|
99
|
+
o menu sobre a leitura. Ramos abertos manualmente são preservados. Acima desse limite, a lateral
|
|
100
|
+
permanece visível. Sem essa opção, a composição existente não muda.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<DocBrowser sections={sections} editorial collapsibleNavigation />
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Ao adotar a opção, retire do host as regras móveis que limitam a altura ou a dimensão
|
|
107
|
+
inicial de `Split` e `Pane`, como a antiga faixa de `14rem`. O navegador passa a organizar
|
|
108
|
+
essa área; a moldura externa continua sendo do aplicativo. Preserve a mesma instância
|
|
109
|
+
de `DocBrowser` ao trocar de área: uma `key` baseada na área descarta o botão que
|
|
110
|
+
recebeu o foco e os ramos abertos. Áreas com mapas diferentes usam `basePath` distintos:
|
|
111
|
+
cada uma preserva seus próprios ramos, sem expandir capítulos da outra. Catálogos que
|
|
112
|
+
compartilham o mapa mantêm o mesmo `basePath`. Dentro de cada mapa, a ordem dos ramos
|
|
113
|
+
deve permanecer estável durante a navegação. Escape retorna ao botão Navegação e mantém a consulta.
|
|
114
|
+
|
|
115
|
+
### Histórico fora da sequência de leitura
|
|
116
|
+
|
|
117
|
+
Um arquivo histórico pode continuar disponível no menu e na busca sem aparecer como próxima
|
|
118
|
+
leitura de um guia vigente. Declare `readingSequence: false` na sua `DocSection`:
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
const archive: DocSection = {
|
|
122
|
+
label: 'Registros históricos',
|
|
123
|
+
readingSequence: false,
|
|
124
|
+
groups: [{ pages: historicalPages }],
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A opção afeta somente os links anterior/próxima. URLs diretas e contexto continuam disponíveis;
|
|
129
|
+
ela não é uma restrição de acesso. Seções sem essa declaração participam da sequência.
|
|
92
130
|
|
|
93
131
|
## Consulte decisões e entregas
|
|
94
132
|
|
|
@@ -205,6 +243,7 @@ não define as rotas nem a autorização do aplicativo.
|
|
|
205
243
|
| `content` | `ReactNode` | — | Conteúdo do host no lugar da página documental, mantendo a lateral. O host resolve rota e título; `null` deixa a leitura vazia. |
|
|
206
244
|
| `sidebarLinks` | `{ label: string; href: string; active?: boolean; newTab?: boolean }[]` | `[]` | Destinos globais fora dos capítulos; `active` marca o catálogo atual e `newTab` abre em outra aba com ícone e descrição acessível. |
|
|
207
245
|
| `sidebarInitiallyCollapsed` | `boolean` | `false` | Seções e grupos nomeados começam recolhidos, inclusive em links diretos. A busca revela resultados temporariamente sem alterar os ramos abertos pela pessoa. |
|
|
246
|
+
| `collapsibleNavigation` | `boolean` | `false` | Em telas de até `48rem`, oferece um botão para alternar entre navegação e leitura, preservando a árvore. |
|
|
208
247
|
|
|
209
248
|
A entrada histórica `@softize/opus/ui/docs` inclui o catálogo e oferece suas páginas quando
|
|
210
249
|
`sections` é omitida. Para conteúdo próprio, prefira a entrada leve do exemplo.
|
|
@@ -119,7 +119,7 @@ retorno de um fluxo encadeado, em que o painel permanece aberto e troca de etapa
|
|
|
119
119
|
<DrawerContent side="right">
|
|
120
120
|
<DrawerHeader>
|
|
121
121
|
<DrawerNavigation>
|
|
122
|
-
<Button variant="ghost" size="icon" aria-label="Voltar para a lista">
|
|
122
|
+
<Button variant="ghost" size="icon-sm" aria-label="Voltar para a lista">
|
|
123
123
|
<ArrowLeft />
|
|
124
124
|
</Button>
|
|
125
125
|
</DrawerNavigation>
|
|
@@ -130,7 +130,7 @@ retorno de um fluxo encadeado, em que o painel permanece aberto e troca de etapa
|
|
|
130
130
|
</DrawerMedia>
|
|
131
131
|
<DrawerTitle>ABC-1D23</DrawerTitle>
|
|
132
132
|
<DrawerActions>
|
|
133
|
-
<Button variant="ghost" size="icon" aria-label="Mais ações">
|
|
133
|
+
<Button variant="ghost" size="icon-sm" aria-label="Mais ações">
|
|
134
134
|
<Ellipsis />
|
|
135
135
|
</Button>
|
|
136
136
|
</DrawerActions>
|
|
@@ -144,9 +144,10 @@ retorno de um fluxo encadeado, em que o painel permanece aberto e troca de etapa
|
|
|
144
144
|
</Drawer>
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
O header compartilha a geometria de Page e Surface delimitada: padding de `1rem` e
|
|
148
|
+
linha mínima de `2rem`, nos dois layouts. Declare `size="sm"` nos botões textuais e
|
|
149
|
+
`size="icon-sm"` nos de ícone (`2rem`), incluindo o retorno. `CloseButton` já usa
|
|
150
|
+
`icon-sm` por padrão. Conteúdo maior pode expandir a linha; rodapés preservam sua escala.
|
|
150
151
|
|
|
151
152
|
O cabeçalho aceita `DrawerDescription` como subheading opcional para orientar sobre a finalidade.
|
|
152
153
|
Omita quando repetir o título. Condições, regras de negócio e consequências necessárias à decisão
|
|
@@ -323,3 +324,14 @@ comparar as duas formas em cada `layout`, abra [Composições](/ui/compositions)
|
|
|
323
324
|
| `id` | `string` | | Identificador para associação acessível explícita. |
|
|
324
325
|
| `className` | `string` | | Ajusta a apresentação do parágrafo. |
|
|
325
326
|
| `…props` | `ComponentProps<"p">` | | Atributos nativos. |
|
|
327
|
+
|
|
328
|
+
## Espaçamento do corpo
|
|
329
|
+
|
|
330
|
+
O body separa os blocos diretos por `1rem`. Dialog e Drawer são superfícies delimitadas:
|
|
331
|
+
o conteúdo mantém `1rem` até a borda inferior, mesmo sem uma propriedade `framed`.
|
|
332
|
+
Em `sectioned`, o corpo tem `1rem` entre o conteúdo e cada divisória. Em `continuous`,
|
|
333
|
+
o cabeçalho fornece o intervalo superior; sem cabeçalho, o próprio corpo abre esse recuo.
|
|
334
|
+
Use `flush` quando o conteúdo controlar seus próprios recuos.
|
|
335
|
+
|
|
336
|
+
Na intro, `DrawerTitle` usa a mesma escala de `PageTitle` (`1.875rem`). No header
|
|
337
|
+
compacto, o título conserva a escala de `1.125rem`. O nível semântico não altera essas medidas.
|