@softize/opus 26.2.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 +57 -0
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +14 -8
- package/docs/adr/0057-doc-browser-permite-busca-no-chrome-do-consumidor.md +39 -5
- package/docs/adr/0058-mensagem-da-ia-aceita-imagem-e-o-chat-recebe-anexos.md +12 -0
- package/package.json +1 -1
- package/src/ui/components/primitives/composer.tsx +19 -5
- package/src/ui/docs/DocBrowser.tsx +122 -12
- package/src/ui/docs/content/actions.md +5 -3
- package/src/ui/docs/content/chat.md +5 -0
- package/src/ui/docs/content/composer.md +19 -2
- package/src/ui/docs/content/documentation-portal.md +40 -1
- package/src/ui/docs/types.ts +2 -0
- package/src/ui/react.tsx +1 -0
- package/src/vite/design.ts +29 -11
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,63 @@ 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
|
+
|
|
28
|
+
## 27.0.0 — 2026-10-13
|
|
29
|
+
|
|
30
|
+
O aplicativo passa a saber, na recusa de um anexo, o que entrou na mesma ação, e o glob do runtime
|
|
31
|
+
de design passa a seguir a semântica usual de `**`. A seção **Breaking** traz as migrações, que
|
|
32
|
+
atingem só quem chama `onReject` diretamente ou passa um handler com um segundo parâmetro opcional de
|
|
33
|
+
outro tipo.
|
|
34
|
+
|
|
35
|
+
- **Recusa com os aceitos da mesma ação.** `ComposerAttachments.onReject` recebe um segundo
|
|
36
|
+
argumento, `ComposerRejectionContext`, com `accepted`: os arquivos aceitos na mesma escolha,
|
|
37
|
+
colagem ou soltura, ou uma lista vazia quando nada passou. Assim o aplicativo explica uma soltura
|
|
38
|
+
mista, como uma imagem e um PDF arrastados juntos, sem guardar estado entre as duas chamadas. O
|
|
39
|
+
tipo `ComposerRejectionContext` sai de `@softize/opus/ui/react`.
|
|
40
|
+
- **Ordem das chamadas documentada.** Em uma ação com arquivos aceitos e recusados, `onAddFiles`
|
|
41
|
+
roda antes de `onReject`, de forma síncrona e uma vez cada. O mesmo vale para o soltar sobre a
|
|
42
|
+
conversa do `Chat`. A ordem já era essa; agora faz parte do contrato.
|
|
43
|
+
- **`**` casa zero ou mais pastas no glob de `opusDesign`.** Em `entry` e `scenarios`, `**` como
|
|
44
|
+
pasta inteira segue a semântica usual. Antes, `design/scenarios/**/*.ts` exigia pelo menos uma
|
|
45
|
+
subpasta e ignorava, sem aviso, o cenário salvo direto em `design/scenarios/`. Um projeto com
|
|
46
|
+
`**/` no padrão pode passar a carregar arquivos que antes ficavam de fora; se dois cenários da
|
|
47
|
+
mesma action passarem a entrar, o runtime de design deixa de subir e o erro lista a action.
|
|
48
|
+
- **`?` literal no glob.** Antes, o `?` virava quantificador de regex, e `design/*/a?.ts` casava
|
|
49
|
+
`design/x/.ts`. Os demais padrões sem `**` continuam casando os mesmos arquivos.
|
|
50
|
+
|
|
51
|
+
### Breaking
|
|
52
|
+
|
|
53
|
+
**O contexto de `onReject` é obrigatório na assinatura.** Quem implementa o callback com um só
|
|
54
|
+
parâmetro não muda nada. Duas situações deixam de compilar:
|
|
55
|
+
|
|
56
|
+
- quem chama `onReject` diretamente, como ao reaproveitar o tipo `ComposerAttachments` em uma área de
|
|
57
|
+
soltar própria. Migração: passe `{ accepted }`, com uma lista vazia quando nada foi aceito:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// antes: attachments.onReject?.(rejected)
|
|
61
|
+
attachments.onReject?.(rejected, { accepted })
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- quem passa direto um handler que já declarava um segundo parâmetro opcional de outro tipo. Migração:
|
|
65
|
+
envolva o handler, como em `(rejections) => handle(rejections)`.
|
|
66
|
+
|
|
10
67
|
## 26.2.0 — 2026-10-09
|
|
11
68
|
|
|
12
69
|
Portais podem organizar capítulos em subgrupos e abrir ferramentas relacionadas sem substituir
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
- **Status:** aceita.
|
|
4
4
|
- **Data:** 2026-09-15.
|
|
5
|
-
- **
|
|
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
|
|
36
|
-
podem adaptar cabeçalho, espaçamento e
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
71
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
163
|
-
|
|
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
|
|
|
@@ -116,6 +116,18 @@ imagem. Assim, retenção, expiração, governança e dado pessoal ficam com que
|
|
|
116
116
|
imagem, e `ChatMessage` continua só com texto. Se um consumidor precisar, uma nova decisão define
|
|
117
117
|
como.
|
|
118
118
|
|
|
119
|
+
> **Emenda — 2026-10-13:** a ordem entre as duas chamadas da triagem passa a ser garantida. Em uma
|
|
120
|
+
> mesma escolha, colagem ou soltura com arquivos aceitos e recusados, `onAddFiles` roda antes de
|
|
121
|
+
> `onReject`, de forma síncrona e uma vez cada, também no soltar sobre a conversa do `Chat`.
|
|
122
|
+
> `onReject` recebe um segundo argumento, `context: ComposerRejectionContext`, cujo `accepted`
|
|
123
|
+
> traz os arquivos aceitos na mesma ação, ou uma lista vazia quando nada passou. Com ele, o app
|
|
124
|
+
> explica uma soltura mista sem guardar estado entre as duas chamadas; a adoção pela Grand Brasil
|
|
125
|
+
> mostrou a necessidade. O contexto é obrigatório na assinatura: opcional, ele obrigaria quem
|
|
126
|
+
> desestrutura `{ accepted }` a tratar `undefined`, embora o composer sempre o informe. Quem
|
|
127
|
+
> implementa o callback com um só parâmetro não muda nada; quem chama `onReject` diretamente precisa
|
|
128
|
+
> passar o contexto, e um handler com segundo parâmetro opcional de outro tipo precisa ser envolvido.
|
|
129
|
+
> Por essas migrações, a mudança saiu como major, na 27.0.0.
|
|
130
|
+
|
|
119
131
|
### 6. Preparação da imagem no navegador
|
|
120
132
|
|
|
121
133
|
`prepareImage(file, options)` prepara a imagem antes do envio, com os tetos que o app passa:
|
package/package.json
CHANGED
|
@@ -29,14 +29,25 @@ export interface ComposerFileRejection {
|
|
|
29
29
|
reason: "type" | "limit";
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** O que mais aconteceu na mesma ação (escolher, colar ou soltar) que gerou as recusas. */
|
|
33
|
+
export interface ComposerRejectionContext {
|
|
34
|
+
/** Arquivos aceitos na mesma ação, já entregues a `onAddFiles`; vazio quando nenhum passou. */
|
|
35
|
+
accepted: File[];
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
/**
|
|
33
39
|
* Anexos da mensagem, controlados pelo app (ADR 0058). O composer faz a triagem e mostra a faixa;
|
|
34
40
|
* preparar, enviar, guardar e limpar depois do envio são do app, que é o dono do transporte.
|
|
41
|
+
*
|
|
42
|
+
* Em uma mesma ação com arquivos aceitos e recusados, `onAddFiles` é chamado antes de `onReject`,
|
|
43
|
+
* de forma síncrona e uma vez cada. Ação sem aceito não chama `onAddFiles`; sem recusado, não
|
|
44
|
+
* chama `onReject`.
|
|
35
45
|
*/
|
|
36
46
|
export interface ComposerAttachments {
|
|
37
47
|
/** Anexos pendentes, na ordem em que aparecem na faixa. */
|
|
38
48
|
items: ComposerAttachment[];
|
|
39
|
-
/** Recebe os arquivos aceitos pela triagem — do botão, do colar ou do soltar.
|
|
49
|
+
/** Recebe os arquivos aceitos pela triagem — do botão, do colar ou do soltar. Na mesma ação,
|
|
50
|
+
* roda antes de `onReject`. */
|
|
40
51
|
onAddFiles: (files: File[]) => void;
|
|
41
52
|
/** A pessoa removeu um anexo da faixa. */
|
|
42
53
|
onRemove: (id: string) => void;
|
|
@@ -46,8 +57,10 @@ export interface ComposerAttachments {
|
|
|
46
57
|
* Não há curinga (`image/*`): a triagem compara o tipo exato do arquivo. */
|
|
47
58
|
accept?: readonly AiImageMediaType[];
|
|
48
59
|
/** Recebe os arquivos recusados. Presente, a explicação da recusa é do app; ausente, o
|
|
49
|
-
* composer a mostra acima do campo.
|
|
50
|
-
|
|
60
|
+
* composer a mostra acima do campo. Roda depois de `onAddFiles` na mesma ação, e
|
|
61
|
+
* `context.accepted` traz os arquivos aceitos nela (vazio quando nenhum passou) — o app
|
|
62
|
+
* distingue "nada entrou" de "parte entrou" sem guardar estado entre as duas chamadas. */
|
|
63
|
+
onReject?: (rejections: ComposerFileRejection[], context: ComposerRejectionContext) => void;
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
/** Limite padrão de anexos por mensagem. */
|
|
@@ -296,7 +309,8 @@ export function Composer({
|
|
|
296
309
|
const twoRow = actions !== undefined || attachments !== undefined;
|
|
297
310
|
|
|
298
311
|
/** Triagem: tipo aceito e espaço na mensagem. Os aceitos vão ao app; os recusados, ao
|
|
299
|
-
* `onReject` do app ou ao aviso do composer.
|
|
312
|
+
* `onReject` do app ou ao aviso do composer. A ordem (aceitos antes, recusados depois, com os
|
|
313
|
+
* aceitos no contexto) é contrato público de `ComposerAttachments`. */
|
|
300
314
|
const intake = (files: File[]): void => {
|
|
301
315
|
if (attachments === undefined || files.length === 0) return;
|
|
302
316
|
const remaining = Math.max(0, max - items.length);
|
|
@@ -309,7 +323,7 @@ export function Composer({
|
|
|
309
323
|
}
|
|
310
324
|
if (accepted.length > 0) attachments.onAddFiles(accepted);
|
|
311
325
|
if (rejected.length > 0 && attachments.onReject !== undefined) {
|
|
312
|
-
attachments.onReject(rejected);
|
|
326
|
+
attachments.onReject(rejected, { accepted });
|
|
313
327
|
setNotice(null);
|
|
314
328
|
} else {
|
|
315
329
|
setNotice(rejected.length > 0 ? rejectionMessage(rejected, accept, max) : null);
|
|
@@ -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
|
}
|
|
@@ -321,9 +321,11 @@ export const eventDetail = defineDesignScenario(eventGet, (input) => ({
|
|
|
321
321
|
|
|
322
322
|
A resposta é tipada pela entrada e pela saída do contrato; em `list`, é a página inteira. Informe ao
|
|
323
323
|
plugin onde os cenários estão, com caminhos ou globs:
|
|
324
|
-
`opusDesign({ scenarios: 'design/scenarios
|
|
325
|
-
de uma
|
|
326
|
-
|
|
324
|
+
`opusDesign({ scenarios: 'design/scenarios/**/*.ts' })`. Nos globs, `*` casa parte do nome dentro
|
|
325
|
+
de uma pasta e `**` casa qualquer número de pastas, inclusive nenhuma: o padrão do exemplo carrega
|
|
326
|
+
tanto `design/scenarios/pedidos.ts` quanto `design/scenarios/pedidos/lista.ts`. O cenário vence a
|
|
327
|
+
resposta gerada. Um cenário de uma action que não existe, ou dois cenários para a mesma action,
|
|
328
|
+
impedem o runtime de design de subir, e o erro lista os nomes.
|
|
327
329
|
|
|
328
330
|
O Opus não decide onde os cenários ficam nem se entram no repositório. Eles não fazem parte da
|
|
329
331
|
aplicação: podem ficar em uma pasta ignorada pelo git e ser escritos durante a sessão de design,
|
|
@@ -261,6 +261,11 @@ O arraste de arquivos sobre a conversa é consumido pelo Chat e não sobe para o
|
|
|
261
261
|
aplicativo: um contêiner com o próprio soltar, como uma área de envio de arquivos, não recebe a mesma
|
|
262
262
|
imagem duas vezes. Sem `composerAttachments`, o arraste segue para o contêiner, como antes.
|
|
263
263
|
|
|
264
|
+
O soltar sobre a conversa passa pela mesma triagem do composer. Quando ele traz imagens aceitas e
|
|
265
|
+
arquivos recusados juntos, `onAddFiles` recebe as imagens primeiro, e `onReject`, quando presente,
|
|
266
|
+
recebe os recusados logo depois, com as imagens da mesma ação em `accepted`. Veja
|
|
267
|
+
[recusas no Composer](/ui/composer#com-imagens-anexadas).
|
|
268
|
+
|
|
264
269
|
## Propriedades de Chat
|
|
265
270
|
|
|
266
271
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -143,6 +143,23 @@ curinga como `image/*`. O que fica de fora é explicado acima do campo, como “
|
|
|
143
143
|
Para dar outra explicação, passe `onReject`: o composer deixa de mostrar o aviso e entrega os
|
|
144
144
|
arquivos recusados, com o motivo, ao aplicativo.
|
|
145
145
|
|
|
146
|
+
Uma mesma escolha, colagem ou soltura pode trazer arquivos aceitos e recusados, como uma imagem e um
|
|
147
|
+
PDF arrastados juntos. O segundo argumento de `onReject`, do tipo `ComposerRejectionContext`, traz
|
|
148
|
+
em `accepted` os aceitos da mesma ação, e a lista vem vazia quando nada passou. Em uma ação mista,
|
|
149
|
+
`onAddFiles` roda antes de `onReject`. Assim o aplicativo explica a recusa conforme o resultado, sem
|
|
150
|
+
guardar estado entre as duas chamadas:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const onReject: ComposerAttachments['onReject'] = (rejections, { accepted }) => {
|
|
154
|
+
const names = rejections.map((r) => r.file.name).join(', ')
|
|
155
|
+
notify(
|
|
156
|
+
accepted.length > 0
|
|
157
|
+
? `Parte dos arquivos foi anexada. Ficaram de fora: ${names}.`
|
|
158
|
+
: `Nenhum arquivo foi anexado. Ficaram de fora: ${names}.`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
146
163
|
Colar texto continua colando texto, mesmo quando a área de transferência também traz uma imagem, como
|
|
147
164
|
ao copiar células de uma planilha. Uma imagem colada sem texto, como um print, recebe um nome que a
|
|
148
165
|
pessoa reconhece na conversa: “Imagem colada 1.png”. A numeração não volta atrás: remover a primeira e
|
|
@@ -197,11 +214,11 @@ mesmos que o servidor confere.
|
|
|
197
214
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
198
215
|
|---|---|---|---|
|
|
199
216
|
| `items` | `ComposerAttachment[]` | | Imagens anexadas à mensagem que está sendo escrita, na ordem da faixa. |
|
|
200
|
-
| `onAddFiles` | `(files: File[]) => void` | | Recebe os arquivos aceitos pela triagem, vindos do botão, do colar ou do soltar. |
|
|
217
|
+
| `onAddFiles` | `(files: File[]) => void` | | Recebe os arquivos aceitos pela triagem, vindos do botão, do colar ou do soltar. Em uma ação com aceitos e recusados, é chamado antes de `onReject`. |
|
|
201
218
|
| `onRemove` | `(id: string) => void` | | A pessoa removeu uma imagem da faixa; o foco volta ao campo. |
|
|
202
219
|
| `max` | `number` | `4` | Limite de imagens por mensagem. O botão de anexar fica desabilitado quando ele é atingido. |
|
|
203
220
|
| `accept` | `readonly AiImageMediaType[]` | PNG, JPEG e WebP | Formatos aceitos pelo botão e pela triagem, comparados com o tipo exato do arquivo. |
|
|
204
|
-
| `onReject` | `(rejections: ComposerFileRejection[]) => void` | | Recebe os arquivos recusados, com o motivo `type` ou `limit
|
|
221
|
+
| `onReject` | `(rejections: ComposerFileRejection[], context: ComposerRejectionContext) => void` | | Recebe os arquivos recusados, com o motivo `type` ou `limit`, depois de `onAddFiles`; `context.accepted` (`File[]`) traz os arquivos aceitos na mesma ação e vem vazio quando nenhum passou. Sem ele, o composer explica a recusa acima do campo. |
|
|
205
222
|
|
|
206
223
|
`ComposerAttachment` é `{ id, name, status, previewUrl?, error? }`. `status` vale `uploading`,
|
|
207
224
|
`ready` ou `failed`; `previewUrl` é o endereço da miniatura e `error` é o motivo mostrado quando a
|
|
@@ -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.
|
package/src/ui/docs/types.ts
CHANGED
package/src/ui/react.tsx
CHANGED
|
@@ -73,6 +73,7 @@ export type {
|
|
|
73
73
|
ComposerAttachmentStatus,
|
|
74
74
|
ComposerAttachments,
|
|
75
75
|
ComposerFileRejection,
|
|
76
|
+
ComposerRejectionContext,
|
|
76
77
|
} from "./components/primitives/composer.tsx";
|
|
77
78
|
// Preparação da imagem no navegador antes do envio (ADR 0058).
|
|
78
79
|
export { prepareImage } from "./lib/prepare-image.ts";
|
package/src/vite/design.ts
CHANGED
|
@@ -82,7 +82,9 @@ export interface OpusDesignOptions {
|
|
|
82
82
|
* CADA rebuild — domínio novo entra no ar ao salvar o arquivo, sem editar
|
|
83
83
|
* entry nenhum. Dos módulos casados entram os exports (default e named) com
|
|
84
84
|
* shape de registrável; schemas/helpers exportados junto ficam de fora.
|
|
85
|
-
* Pode misturar estáticos e globs no array.
|
|
85
|
+
* Pode misturar estáticos e globs no array. No glob, `*` casa parte de um
|
|
86
|
+
* nome dentro de uma pasta e `**` casa zero ou mais pastas:
|
|
87
|
+
* 'design/scenarios/**\/*.ts' inclui o arquivo solto em `design/scenarios/`.
|
|
86
88
|
*/
|
|
87
89
|
entry?: string | string[]
|
|
88
90
|
|
|
@@ -531,17 +533,33 @@ async function loadGlobEntry(server: ViteDevServer, pattern: string): Promise<un
|
|
|
531
533
|
}
|
|
532
534
|
|
|
533
535
|
/**
|
|
534
|
-
* Matcher próprio de propósito (sem dep nova): `*` casa um segmento
|
|
535
|
-
* `**`
|
|
536
|
-
*
|
|
536
|
+
* Matcher próprio de propósito (sem dep nova): `*` casa parte de um segmento
|
|
537
|
+
* (sem '/'); `**` como segmento inteiro é globstar e casa zero ou mais pastas
|
|
538
|
+
* — `design/**\/*.ts` casa `design/a.ts`, `design/x/a.ts` e `design/x/y/a.ts`.
|
|
539
|
+
* No fim do padrão (`design/**`), casa qualquer arquivo abaixo da pasta. Cobre o
|
|
540
|
+
* entry por pasta de domínio e os cenários; padrão exótico não suportado =
|
|
541
|
+
* aponte entries estáticos.
|
|
542
|
+
*
|
|
543
|
+
* Exportada só para teste: o índice de `@softize/opus/vite` não a reexporta.
|
|
537
544
|
*/
|
|
538
|
-
function globToRegExp(pattern: string): RegExp {
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
+
export function globToRegExp(pattern: string): RegExp {
|
|
546
|
+
const segments = pattern.split('/')
|
|
547
|
+
const body = segments
|
|
548
|
+
.map((segment, index) => {
|
|
549
|
+
const last = index === segments.length - 1
|
|
550
|
+
// Globstar no começo ou no meio: o grupo opcional leva a barra junto, então
|
|
551
|
+
// "nenhuma pasta" casa sem exigir `//` no path.
|
|
552
|
+
if (segment === '**') return last ? '.*' : '(?:.*/)?'
|
|
553
|
+
const escaped = segment.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
554
|
+
// `**` colado a outro texto no segmento (`a**`) mantém o comportamento
|
|
555
|
+
// anterior: qualquer sequência. `\u0000` é placeholder: nunca ocorre num path.
|
|
556
|
+
const converted = escaped
|
|
557
|
+
.replace(/\*\*/g, '\u0000')
|
|
558
|
+
.replace(/\*/g, '[^/]*')
|
|
559
|
+
.replace(/\u0000/g, '.*')
|
|
560
|
+
return last ? converted : `${converted}/`
|
|
561
|
+
})
|
|
562
|
+
.join('')
|
|
545
563
|
return new RegExp(`^${body}$`)
|
|
546
564
|
}
|
|
547
565
|
|