@softize/opus 16.0.0 → 16.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,15 @@ 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
|
+
## 16.1.0 — 2026-09-11
|
|
11
|
+
|
|
12
|
+
`PresentationDevtoolsProvider`, `usePresentationRegistration` e `PresentationDevtools` permitem
|
|
13
|
+
que o shell reúna as Presentations ativas em um único launcher de desenvolvimento. Cada recurso
|
|
14
|
+
registra sua definição e o estado vivo da invocação; a pessoa escolhe qual snapshot inspecionar no
|
|
15
|
+
menu. O JSON continua mascarando dados sensíveis e só é materializado depois da escolha, evitando
|
|
16
|
+
serialização desnecessária durante a renderização. O consumidor deve montar o provider, os registros
|
|
17
|
+
e o launcher apenas em ambiente de desenvolvimento.
|
|
18
|
+
|
|
10
19
|
## 16.0.0 — 2026-09-11
|
|
11
20
|
|
|
12
21
|
Opus 16 introduz `Presentation` como artefato público declarativo, disponível também pelo subpath
|
package/package.json
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type ReactElement,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
2
13
|
import { Braces, X } from "lucide-react";
|
|
3
14
|
import type {
|
|
4
15
|
PresentationDiagnostic,
|
|
@@ -20,6 +31,13 @@ import {
|
|
|
20
31
|
DialogHeader,
|
|
21
32
|
DialogTitle,
|
|
22
33
|
} from "../primitives/dialog.tsx";
|
|
34
|
+
import {
|
|
35
|
+
Menu,
|
|
36
|
+
MenuContent,
|
|
37
|
+
MenuItem,
|
|
38
|
+
MenuLabel,
|
|
39
|
+
MenuTrigger,
|
|
40
|
+
} from "../primitives/menu.tsx";
|
|
23
41
|
import {
|
|
24
42
|
Drawer,
|
|
25
43
|
DrawerBody,
|
|
@@ -248,6 +266,211 @@ export interface PresentationInspectorProps {
|
|
|
248
266
|
className?: string;
|
|
249
267
|
}
|
|
250
268
|
|
|
269
|
+
export interface PresentationRegistration
|
|
270
|
+
extends Pick<
|
|
271
|
+
PresentationInspectorProps,
|
|
272
|
+
"invocation" | "resolved" | "diagnostics"
|
|
273
|
+
> {
|
|
274
|
+
definition: PresentationDefinition;
|
|
275
|
+
/** Retira temporariamente a Presentation do inventário ativo sem desmontar o consumidor. */
|
|
276
|
+
enabled?: boolean;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
interface ActivePresentationRegistration
|
|
280
|
+
extends Omit<PresentationRegistration, "enabled"> {
|
|
281
|
+
key: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface PresentationRegistrationContextValue {
|
|
285
|
+
upsert: (key: string, registration: ActivePresentationRegistration) => void;
|
|
286
|
+
remove: (key: string) => void;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface PresentationDevtoolsContextValue {
|
|
290
|
+
keys: readonly string[];
|
|
291
|
+
read: (key: string) => ActivePresentationRegistration | undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const PresentationRegistrationContext =
|
|
295
|
+
createContext<PresentationRegistrationContextValue | null>(null);
|
|
296
|
+
const PresentationDevtoolsContext =
|
|
297
|
+
createContext<PresentationDevtoolsContextValue | null>(null);
|
|
298
|
+
|
|
299
|
+
export interface PresentationDevtoolsProviderProps {
|
|
300
|
+
children: ReactNode;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Mantém o inventário das Presentations montadas para uma única ferramenta do shell. */
|
|
304
|
+
export function PresentationDevtoolsProvider({
|
|
305
|
+
children,
|
|
306
|
+
}: PresentationDevtoolsProviderProps): ReactElement {
|
|
307
|
+
const registrations = useRef(
|
|
308
|
+
new Map<string, ActivePresentationRegistration>(),
|
|
309
|
+
);
|
|
310
|
+
const [keys, setKeys] = useState<readonly string[]>([]);
|
|
311
|
+
const upsert = useCallback(
|
|
312
|
+
(key: string, registration: ActivePresentationRegistration): void => {
|
|
313
|
+
const exists = registrations.current.has(key);
|
|
314
|
+
registrations.current.set(key, registration);
|
|
315
|
+
if (!exists) setKeys((current) => [...current, key]);
|
|
316
|
+
},
|
|
317
|
+
[],
|
|
318
|
+
);
|
|
319
|
+
const remove = useCallback((key: string): void => {
|
|
320
|
+
if (!registrations.current.delete(key)) return;
|
|
321
|
+
setKeys((current) => current.filter((candidate) => candidate !== key));
|
|
322
|
+
}, []);
|
|
323
|
+
const read = useCallback(
|
|
324
|
+
(key: string) => registrations.current.get(key),
|
|
325
|
+
[],
|
|
326
|
+
);
|
|
327
|
+
const registration = useMemo(() => ({ upsert, remove }), [upsert, remove]);
|
|
328
|
+
const devtools = useMemo(() => ({ keys, read }), [keys, read]);
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<PresentationRegistrationContext.Provider value={registration}>
|
|
332
|
+
<PresentationDevtoolsContext.Provider value={devtools}>
|
|
333
|
+
{children}
|
|
334
|
+
</PresentationDevtoolsContext.Provider>
|
|
335
|
+
</PresentationRegistrationContext.Provider>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Anuncia um snapshot ao provider mais próximo; fora dele, permanece inofensivo. */
|
|
340
|
+
export function usePresentationRegistration({
|
|
341
|
+
definition,
|
|
342
|
+
invocation,
|
|
343
|
+
resolved,
|
|
344
|
+
diagnostics,
|
|
345
|
+
enabled = true,
|
|
346
|
+
}: PresentationRegistration): void {
|
|
347
|
+
const context = useContext(PresentationRegistrationContext);
|
|
348
|
+
const upsert = context?.upsert;
|
|
349
|
+
const remove = context?.remove;
|
|
350
|
+
const key = useId();
|
|
351
|
+
|
|
352
|
+
// Sem dependências de propósito: os dados podem mudar mantendo a mesma Presentation.
|
|
353
|
+
// `upsert` substitui o snapshot em memória sem provocar um ciclo de render.
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
if (upsert === undefined || !enabled) {
|
|
356
|
+
remove?.(key);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
upsert(key, { key, definition, invocation, resolved, diagnostics });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
useEffect(
|
|
363
|
+
() => () => {
|
|
364
|
+
remove?.(key);
|
|
365
|
+
},
|
|
366
|
+
[key, remove],
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
interface SelectedPresentation {
|
|
371
|
+
key: string;
|
|
372
|
+
title: string;
|
|
373
|
+
json: string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface PresentationDevtoolsProps {
|
|
377
|
+
/** Nome acessível e tooltip do launcher. */
|
|
378
|
+
triggerLabel?: string;
|
|
379
|
+
className?: string;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Launcher único do shell para escolher e inspecionar qualquer Presentation ativa. */
|
|
383
|
+
export function PresentationDevtools({
|
|
384
|
+
triggerLabel = "Inspecionar Presentations ativas",
|
|
385
|
+
className,
|
|
386
|
+
}: PresentationDevtoolsProps): ReactElement | null {
|
|
387
|
+
const registry = useContext(PresentationDevtoolsContext);
|
|
388
|
+
const [selected, setSelected] = useState<SelectedPresentation | null>(null);
|
|
389
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
390
|
+
const registrations =
|
|
391
|
+
registry?.keys.flatMap((key) => {
|
|
392
|
+
const registration = registry.read(key);
|
|
393
|
+
return registration === undefined ? [] : [registration];
|
|
394
|
+
}) ?? [];
|
|
395
|
+
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
if (selected !== null && !registry?.keys.includes(selected.key)) {
|
|
398
|
+
setSelected(null);
|
|
399
|
+
}
|
|
400
|
+
}, [registry?.keys, selected]);
|
|
401
|
+
|
|
402
|
+
if (registrations.length === 0) return null;
|
|
403
|
+
|
|
404
|
+
function inspect(key: string): void {
|
|
405
|
+
const registration = registry?.read(key);
|
|
406
|
+
if (registration === undefined) return;
|
|
407
|
+
const json =
|
|
408
|
+
JSON.stringify(createPresentationInspectionSnapshot(registration), null, 2) ??
|
|
409
|
+
"null";
|
|
410
|
+
setSelected({ key, title: registration.definition.title, json });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<>
|
|
415
|
+
<Menu open={menuOpen} onOpenChange={setMenuOpen}>
|
|
416
|
+
<MenuTrigger asChild>
|
|
417
|
+
<Button
|
|
418
|
+
size="icon"
|
|
419
|
+
variant="outline"
|
|
420
|
+
data-slot="presentation-devtools"
|
|
421
|
+
data-appearance="fab"
|
|
422
|
+
className={cn("rounded-full shadow-md", className)}
|
|
423
|
+
aria-label={triggerLabel}
|
|
424
|
+
title={triggerLabel}
|
|
425
|
+
>
|
|
426
|
+
<Braces aria-hidden />
|
|
427
|
+
</Button>
|
|
428
|
+
</MenuTrigger>
|
|
429
|
+
<MenuContent side="left" align="center" className="min-w-64">
|
|
430
|
+
<MenuLabel>Presentations ativas</MenuLabel>
|
|
431
|
+
{[...registrations].reverse().map((registration) => (
|
|
432
|
+
<MenuItem key={registration.key} onSelect={() => inspect(registration.key)}>
|
|
433
|
+
<span className="min-w-0 flex-1 truncate">
|
|
434
|
+
{registration.definition.title}
|
|
435
|
+
</span>
|
|
436
|
+
<span className="text-xs text-muted-foreground">
|
|
437
|
+
{registration.invocation === undefined ||
|
|
438
|
+
registration.invocation === null ||
|
|
439
|
+
typeof registration.invocation !== "object" ||
|
|
440
|
+
!("surface" in registration.invocation)
|
|
441
|
+
? "Presentation"
|
|
442
|
+
: String(registration.invocation.surface)}
|
|
443
|
+
</span>
|
|
444
|
+
</MenuItem>
|
|
445
|
+
))}
|
|
446
|
+
</MenuContent>
|
|
447
|
+
</Menu>
|
|
448
|
+
{selected === null ? null : (
|
|
449
|
+
<Dialog open onOpenChange={(open) => !open && setSelected(null)}>
|
|
450
|
+
<DialogContent className="sm:max-w-3xl" aria-describedby={undefined}>
|
|
451
|
+
<DialogHeader>
|
|
452
|
+
<DialogTitle>{selected.title}</DialogTitle>
|
|
453
|
+
</DialogHeader>
|
|
454
|
+
<DialogBody>
|
|
455
|
+
<pre className="max-h-[65vh] overflow-auto rounded-lg bg-muted p-4 text-xs leading-relaxed">
|
|
456
|
+
{selected.json}
|
|
457
|
+
</pre>
|
|
458
|
+
</DialogBody>
|
|
459
|
+
<DialogFooter>
|
|
460
|
+
<Copyable
|
|
461
|
+
value={selected.json}
|
|
462
|
+
className={buttonVariants({ variant: "outline" })}
|
|
463
|
+
>
|
|
464
|
+
Copiar JSON
|
|
465
|
+
</Copyable>
|
|
466
|
+
</DialogFooter>
|
|
467
|
+
</DialogContent>
|
|
468
|
+
</Dialog>
|
|
469
|
+
)}
|
|
470
|
+
</>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
251
474
|
/** Ferramenta de desenvolvimento para conferir e copiar o artefato consumido pelo renderer. */
|
|
252
475
|
export function PresentationInspector({
|
|
253
476
|
definition,
|
|
@@ -129,6 +129,13 @@ Use `floating` quando o inspetor precisar ficar disponível sem ocupar o cabeça
|
|
|
129
129
|
aplica tamanho, forma, borda e elevação de FAB; `className` define a âncora na superfície consumidora.
|
|
130
130
|
Reserve espaço para outros controles flutuantes do shell em vez de sobrepô-los.
|
|
131
131
|
|
|
132
|
+
Quando várias Presentations puderem estar montadas ao mesmo tempo, envolva o shell com
|
|
133
|
+
`PresentationDevtoolsProvider`, anuncie cada recurso com `usePresentationRegistration` e renderize
|
|
134
|
+
um único `PresentationDevtools`. O launcher abre a lista ativa e só cria o snapshot mascarado depois
|
|
135
|
+
que uma Presentation é escolhida. Monte os três exclusivamente em desenvolvimento: o inventário
|
|
136
|
+
inclui definição, invocação, bindings resolvidos e diagnósticos que não pertencem à experiência de
|
|
137
|
+
produção.
|
|
138
|
+
|
|
132
139
|
## Propriedades de Presentation
|
|
133
140
|
|
|
134
141
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -156,3 +163,26 @@ Reserve espaço para outros controles flutuantes do shell em vez de sobrepô-los
|
|
|
156
163
|
| `title` | `string` | `JSON da Presentation` | Título do dialog de inspeção. |
|
|
157
164
|
| `floating` | `boolean` | `false` | Apresenta o gatilho com aparência de FAB. |
|
|
158
165
|
| `className` | `string` | | Classes adicionais do gatilho. |
|
|
166
|
+
|
|
167
|
+
## Propriedades de PresentationDevtoolsProvider
|
|
168
|
+
|
|
169
|
+
| Propriedade | Tipo | Descrição |
|
|
170
|
+
| ----------- | ----------- | ------------------------------------------ |
|
|
171
|
+
| `children` | `ReactNode` | Árvore que registra Presentations ativas. |
|
|
172
|
+
|
|
173
|
+
## Parâmetros de usePresentationRegistration
|
|
174
|
+
|
|
175
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
176
|
+
| ------------- | ---------------------------- | ------ | ------------------------------------------------------ |
|
|
177
|
+
| `definition` | `PresentationDefinition` | | Definição estática da Presentation. |
|
|
178
|
+
| `invocation` | `unknown` | | Estado vivo serializável, quando existir. |
|
|
179
|
+
| `resolved` | `unknown` | | Bindings resolvidos pelo renderer. |
|
|
180
|
+
| `diagnostics` | `PresentationDiagnostic[]` | | Diagnósticos da definição ou execução. |
|
|
181
|
+
| `enabled` | `boolean` | `true` | Mantém ou retira o recurso do inventário ativo. |
|
|
182
|
+
|
|
183
|
+
## Propriedades de PresentationDevtools
|
|
184
|
+
|
|
185
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
186
|
+
| -------------- | -------- | -------------------------------- | ---------------------------------------------- |
|
|
187
|
+
| `triggerLabel` | `string` | `Inspecionar Presentations ativas` | Nome acessível e tooltip do launcher. |
|
|
188
|
+
| `className` | `string` | | Classes adicionais; o shell define a posição. |
|
package/src/ui/meta.ts
CHANGED
|
@@ -406,7 +406,7 @@ export const componentMeta = {
|
|
|
406
406
|
name: "presentation",
|
|
407
407
|
ancestry: "opus",
|
|
408
408
|
whenToUse:
|
|
409
|
-
"Descreva um recurso que precisa manter a mesma anatomia ao aparecer como Page, Dialog ou Drawer. Presentation organiza navegação, título, ações de cabeçalho, body e ações de rodapé sem redefinir o conteúdo para cada superfície. A definição persistente liga esses slots a actions Opus; a invocação carrega o estado JSON da execução. PresentationInspector reúne
|
|
409
|
+
"Descreva um recurso que precisa manter a mesma anatomia ao aparecer como Page, Dialog ou Drawer. Presentation organiza navegação, título, ações de cabeçalho, body e ações de rodapé sem redefinir o conteúdo para cada superfície. A definição persistente liga esses slots a actions Opus; a invocação carrega o estado JSON da execução. PresentationInspector inspeciona um recurso; PresentationDevtools reúne as Presentations ativas do shell em desenvolvimento. Ambos materializam snapshots mascarados somente sob demanda.",
|
|
410
410
|
},
|
|
411
411
|
router: {
|
|
412
412
|
name: "router",
|
package/src/ui/react.tsx
CHANGED
|
@@ -420,11 +420,17 @@ export type { DataStateProps } from "./components/patterns/data-state.tsx";
|
|
|
420
420
|
|
|
421
421
|
export {
|
|
422
422
|
Presentation,
|
|
423
|
+
PresentationDevtools,
|
|
424
|
+
PresentationDevtoolsProvider,
|
|
423
425
|
PresentationInspector,
|
|
426
|
+
usePresentationRegistration,
|
|
424
427
|
} from "./components/patterns/presentation.tsx";
|
|
425
428
|
export type {
|
|
426
429
|
PresentationProps,
|
|
430
|
+
PresentationDevtoolsProps,
|
|
431
|
+
PresentationDevtoolsProviderProps,
|
|
427
432
|
PresentationInspectorProps,
|
|
433
|
+
PresentationRegistration,
|
|
428
434
|
} from "./components/patterns/presentation.tsx";
|
|
429
435
|
|
|
430
436
|
// Esqueleto de página do back-office (main + container + header título/descrição/ação).
|