@softize/opus 27.0.0 → 27.1.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 CHANGED
@@ -7,6 +7,24 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
7
7
  `opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
8
8
  mudança cobra do seu código.
9
9
 
10
+ ## 27.1.0 — 2026-10-13
11
+
12
+ Portais documentais podem oferecer navegação recolhível em telas estreitas com
13
+ `DocBrowser.collapsibleNavigation`. O menu ocupa a área disponível, preserva os ramos
14
+ abertos e mostra os resultados da busca; selecionar um destino interno ou pressionar
15
+ Escape retorna à leitura. A composição desktop e consumidores sem a opção não mudam.
16
+
17
+ **Ao adotar:** remova as regras CSS móveis do host que limitavam `Split` e `Pane` a
18
+ uma faixa curta. Preserve a instância de `DocBrowser` entre áreas, sem uma `key`
19
+ que remonte o navegador a cada troca; isso mantém a busca, os ramos abertos e o foco.
20
+ Mapas distintos usam `basePath` distintos para preservar a abertura dos ramos de cada
21
+ área. Uma consulta já preenchida na montagem não abre o menu; editar a busca revela
22
+ os resultados.
23
+
24
+ `DocSection.readingSequence: false` mantém uma seção na navegação e na busca, mas a retira
25
+ dos links de leitura anterior/próxima. Isso permite consultar registros históricos sem
26
+ apresentá-los como continuidade de guias vigentes.
27
+
10
28
  ## 27.0.0 — 2026-10-13
11
29
 
12
30
  O aplicativo passa a saber, na recusa de um anexo, o que entrou na mesma ação, e o glob do runtime
@@ -2,7 +2,7 @@
2
2
 
3
3
  - **Status:** aceita.
4
4
  - **Data:** 2026-09-15.
5
- - **Emenda:** 2026-10-07; portal documental delimitado pela ADR 0057.
5
+ - **Emendas:** 2026-10-07 e 2026-10-13; portal documental delimitado pela ADR 0057.
6
6
 
7
7
  ## Contexto
8
8
 
@@ -32,8 +32,9 @@ nesse caso, o cancelamento usa `outline`.
32
32
  ### Exceção: portal de documentação
33
33
 
34
34
  O portal documental tem uma jornada de leitura distinta da UI operacional das aplicações. Conforme
35
- a ADR 0057, `DocHeader`, a leitura editorial de `DocBrowser` e `DocCatalog`/`DocRecordDetail`
36
- podem adaptar cabeçalho, espaçamento e sumário à largura da tela. A exceção foi aprovada para
35
+ a ADR 0057, `DocHeader`, a leitura editorial e a navegação móvel opcional de `DocBrowser`,
36
+ além de `DocCatalog`/`DocRecordDetail`, podem adaptar cabeçalho, navegação, espaçamento e
37
+ sumário à largura da tela. A exceção foi aprovada para
37
38
  preservar a composição validada nos portais do Opus e da Grand Brasil.
38
39
 
39
40
  A exceção não torna responsivos `Split`, `Sidebar`, formulários, tabelas ou outros componentes da
@@ -41,9 +42,13 @@ UI das aplicações. O host documental continua responsável pela composição d
41
42
  estreita. Exemplos vivos usam o mesmo contrato desktop dos componentes demonstrados; o portal não
42
43
  promete que todos os exemplos sejam adequados a telas menores que `64rem`.
43
44
 
44
- O gate permite variantes de viewport somente nos três arquivos documentais identificados na ADR
45
- 0057. Arquivos novos, mesmo dentro de `docs/`, continuam sujeitos à regra até uma decisão explícita.
46
- Container queries não entram na exceção.
45
+ O gate permite variantes de viewport somente nas funções editoriais identificadas pela ADR 0057.
46
+ A consulta direta de largura via `matchMedia` tem uma exceção separada: somente os helpers
47
+ `subscribeCompactNavigation` e `compactNavigationSnapshot` de `DocBrowser.tsx` podem consultar
48
+ a constante `compactNavigationQuery`, fixada em `(max-width: 48rem)`. Isso não libera variantes
49
+ na função `DocBrowser`. Chamadas diretas com consultas dinâmicas não identificáveis são recusadas;
50
+ preferências explícitas continuam permitidas. Arquivos novos, mesmo dentro de `docs/`, seguem
51
+ a regra. Container queries não entram na exceção.
47
52
 
48
53
  ## Consequências
49
54
 
@@ -67,7 +72,8 @@ Container queries não entram na exceção.
67
72
  ## Verificação
68
73
 
69
74
  - Um teste de inventário reprova variantes de largura e container queries na UI viva.
70
- - O mesmo teste delimita a exceção documental por arquivo e comprova que ela não alcança componentes
71
- da aplicação, outros arquivos documentais ou container queries.
75
+ - O mesmo teste delimita a exceção documental por função e verifica chamadas diretas de `matchMedia`,
76
+ inclusive constantes e consultas dinâmicas. Mutações fora dos helpers, do arquivo ou do breakpoint
77
+ documentados reprovam; a exceção não alcança componentes da aplicação nem container queries.
72
78
  - Testes estruturais fixam layouts, larguras de modal e distribuição dos footers.
73
79
  - Documentação pública declara a largura mínima, o default por conteúdo e a exceção `equal`.
@@ -1,8 +1,37 @@
1
1
  # ADR 0057 — Portal documental compartilha navegação, busca e leitura
2
2
 
3
- - **Status:** aceita para implementação local; publicação pendente.
3
+ - **Status:** aceita; composição original em 25.7.0, subgrupos e abertura em nova aba em 26.2.0,
4
+ navegação recolhível e sequência de leitura em 27.1.0.
4
5
  - **Data:** 2026-10-07.
5
6
 
7
+ > **Emenda — 2026-10-13, versão 27.1.0:** a composição anterior reúne as entregas
8
+ > das versões 25.7.0 e 26.2.0. Esta extensão separa presença no mapa de participação na leitura:
9
+ > `DocSection.readingSequence: false` retira a seção dos links anterior/próxima, mas
10
+ > preserva busca, contexto e URLs. A omissão mantém a sequência existente. Excluir o
11
+ > arquivo histórico da árvore impediria sua descoberta; duplicar o mapa para calcular
12
+ > continuidade criaria duas ordens concorrentes.
13
+ >
14
+ > `DocBrowser.collapsibleNavigation` é opcional e começa desligado. Quando ligado, em
15
+ > telas de até `48rem`, oferece um botão de navegação e começa recolhido. Ao abrir,
16
+ > a árvore ocupa a área disponível no lugar da leitura, sem a faixa fixa de `14rem`.
17
+ > A seleção de um destino interno e a tecla Escape recolhem o menu; editar a busca revela
18
+ > seus resultados. A consulta preenchida na montagem não abre o menu sobre a leitura.
19
+ > A árvore permanece montada para preservar ramos abertos. Acima
20
+ > desse limite, a lateral continua visível. `Split` e `Sidebar` não mudam de contrato.
21
+ > O host preserva a instância também entre Documentação e UI: remontá-la descartaria
22
+ > o botão que recebeu o foco e o estado dos ramos. Mapas distintos usam `basePath`
23
+ > distintos; as chaves dos ramos incluem essa identidade antes da posição no mapa.
24
+ > Assim, abrir um capítulo em Documentação não abre outro na mesma posição em UI,
25
+ > e voltar restaura a abertura manual. A ordem interna de cada mapa permanece estável.
26
+ > Escape retorna
27
+ > o foco ao botão Navegação, inclusive quando acionado pela busca externa; a consulta
28
+ > permanece para permitir retomar os mesmos resultados.
29
+ > Esta composição substitui os ajustes CSS duplicados nos dois portais, estendendo
30
+ > a exceção documental à montagem do navegador, não aos exemplos ou demais telas.
31
+ > A alternativa de manter uma lateral curta preservaria a rolagem que esconde destinos
32
+ > globais. Testes de teclado, redimensionamento, busca, rotas e sequência de leitura,
33
+ > além de inspeção visual nos dois consumidores, verificam a extensão antes de publicar.
34
+
6
35
  ## Contexto
7
36
 
8
37
  Portais de documentação precisam apresentar guias, decisões e entregas sem trocar o mapa de
@@ -114,10 +143,14 @@ A composição documental é uma exceção explícita à ADR 0017: `DocHeader.ts
114
143
  (na leitura editorial) e `DocCatalog.tsx` (incluindo `DocRecordDetail`) podem usar variantes de
115
144
  viewport para organizar cabeçalho, sumário e espaçamento. A exceção fica restrita a esses arquivos;
116
145
  não libera toda a pasta `docs/` nem container queries. O gate verifica as funções de composição
117
- permitidas, sem liberar outras funções ou a montagem de `Split` no mesmo arquivo.
146
+ permitidas. A emenda de 13/10 admite a consulta de largura via `matchMedia` apenas nos helpers
147
+ `subscribeCompactNavigation` e `compactNavigationSnapshot`, em `DocBrowser.tsx`, usando a
148
+ constante `compactNavigationQuery` fixada em `(max-width: 48rem)`. Variantes de viewport
149
+ continuam proibidas na função `DocBrowser`; consultas diretas dinâmicas não identificáveis
150
+ também reprovam. Preferências explícitas de acessibilidade e tema permanecem permitidas.
118
151
 
119
152
  Essa adaptação foi aprovada para o portal de leitura, não para a UI das aplicações. O host organiza
120
- sua moldura em telas estreitas; `Split`, `Sidebar` e os exemplos vivos não ganham um contrato
153
+ sua moldura; o navegador pode oferecer sua navegação móvel opcional. `Split`, `Sidebar` e os exemplos vivos não ganham um contrato
121
154
  responsivo por estarem dentro da documentação. Manter o portal inteiramente desktop descartaria
122
155
  a composição já validada; ampliar a exceção para toda a UI alteraria uma decisão fora deste escopo.
123
156
 
@@ -159,8 +192,9 @@ e separa a consulta documental da consulta local.
159
192
  - Os dois portais compartilham cabeçalho e catálogos sem compartilhar sua taxonomia específica.
160
193
  - Guias, índices e fontes acessíveis por URL direta precisam ser filtrados antes de chegar ao
161
194
  navegador quando houver restrição de acesso. Esta composição não é uma fronteira de segurança.
162
- - A consolidação é um candidato local. A publicação e a adoção pela Grand Brasil são etapas
163
- distintas, posteriores à revisão independente.
195
+ - A composição original integra a versão 25.7.0; subgrupos e abertura em nova aba, a 26.2.0;
196
+ a navegação recolhível e a sequência de leitura, a 27.1.0. A adoção por cada portal é uma
197
+ etapa própria.
164
198
 
165
199
  ## Verificação
166
200
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "27.0.0",
3
+ "version": "27.1.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 { ArrowLeft, ArrowRight, ExternalLink, Search } from "lucide-react";
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(sections);
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 = entries[index]?.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: `section:${sectionIndex}`,
321
- groups: indexGroups(section.groups, `group:${sectionIndex}`),
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
- : () => navigate(link.href)
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 => navigate(to);
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
- return (
671
+ const browser = (
596
672
  <Split className="h-full">
597
- <Pane initialSize={sidebarSize}>
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="overflow-y-auto"
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
  }
@@ -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 muda sozinho a orientação de `Split` nem transforma os exemplos em layouts móveis.
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.
@@ -25,4 +25,6 @@ export interface DocGroup {
25
25
  export interface DocSection {
26
26
  label: string
27
27
  groups: DocGroup[]
28
+ /** False exclui a seção da leitura anterior/próxima, sem retirá-la da navegação ou busca. */
29
+ readingSequence?: boolean
28
30
  }