@softize/opus 18.5.0 → 20.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 +275 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +16 -12
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +103 -73
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +81 -54
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +51 -38
- package/src/ui/components/patterns/surface-anatomy.ts +246 -0
- package/src/ui/components/patterns/surface-header.tsx +51 -6
- package/src/ui/components/patterns/surface-media.tsx +69 -0
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +14 -5
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +179 -45
- package/src/ui/components/primitives/drawer.tsx +131 -24
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +134 -52
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +252 -35
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +37 -7
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +219 -28
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +193 -22
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +28 -5
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +68 -21
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +125 -11
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +300 -0
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +25 -45
- package/src/ui/docs/content/skeleton.md +13 -4
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +34 -20
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +14 -2
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +38 -32
- package/src/ui/react.tsx +14 -0
- package/src/vite/design.ts +16 -4
package/src/core/runtime.ts
CHANGED
|
@@ -413,9 +413,35 @@ export class Runtime {
|
|
|
413
413
|
})
|
|
414
414
|
}
|
|
415
415
|
this.actions.set(action.name, action)
|
|
416
|
+
if (this.config.dev) this.warnIgnoredExecutionFields(action)
|
|
416
417
|
}
|
|
417
418
|
}
|
|
418
419
|
|
|
420
|
+
/**
|
|
421
|
+
* `emits` e `background` pertencem ao binding de qualquer kind pelo tipo, mas o runtime e o
|
|
422
|
+
* manifest só os aplicam em `simple` e `form`. Restringir o tipo quebraria bindings
|
|
423
|
+
* existentes; o aviso em desenvolvimento evita que a declaração pareça ter efeito.
|
|
424
|
+
*/
|
|
425
|
+
private warnIgnoredExecutionFields(action: ActionDef): void {
|
|
426
|
+
if (action.kind !== 'list' && action.kind !== 'view') return
|
|
427
|
+
const declared = action as { emits?: unknown; background?: unknown }
|
|
428
|
+
const fields = (['emits', 'background'] as const).filter((field) => declared[field] !== undefined)
|
|
429
|
+
if (fields.length === 0) return
|
|
430
|
+
const list = fields.map((field) => `\`${field}\``).join(' e ')
|
|
431
|
+
const effects = fields.map((field) =>
|
|
432
|
+
field === 'background'
|
|
433
|
+
? 'Sem fila, a action executa na própria requisição.'
|
|
434
|
+
: 'Os eventos declarados não entram no manifest nem na conferência do que o handler emite.',
|
|
435
|
+
)
|
|
436
|
+
this.log.warn(
|
|
437
|
+
`A action "${action.name}" é do kind "${action.kind}" e declara ${list}, que o runtime só ` +
|
|
438
|
+
`aplica em actions simple e form. ${effects.join(' ')} Remova ${list} da declaração da ` +
|
|
439
|
+
'action, no `bindAction` ou no `defineAction`; se a operação precisa de fila ou de ' +
|
|
440
|
+
'eventos declarados, use uma action simple ou form.',
|
|
441
|
+
{ code: 'runtime.action_field_ignored', action: action.name, kind: action.kind, fields },
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
419
445
|
private registerReactions(reactions: Array<ReactionDef<any>>): void {
|
|
420
446
|
for (const reaction of reactions) {
|
|
421
447
|
if (this.reactions.has(reaction.name)) {
|
|
@@ -455,6 +481,12 @@ export class Runtime {
|
|
|
455
481
|
})
|
|
456
482
|
}
|
|
457
483
|
|
|
484
|
+
// Antes de inicializar adapters: é uma checagem do registro, e falhar aqui não deixa
|
|
485
|
+
// conexão aberta para trás. Sem scheduler, a checagem não roda porque nenhum schedule
|
|
486
|
+
// executaria: os adapters inicializam e o start falha depois, com
|
|
487
|
+
// `runtime.eventbus_required` se também faltar event bus, senão `runtime.scheduler_required`.
|
|
488
|
+
if (this.scheduler !== undefined) this.assertScheduledActionsExist()
|
|
489
|
+
|
|
458
490
|
for (const adapter of this.allAdapters()) {
|
|
459
491
|
if (adapter.init !== undefined) await adapter.init(this)
|
|
460
492
|
if (adapter.dispose !== undefined) {
|
|
@@ -1203,6 +1235,35 @@ export class Runtime {
|
|
|
1203
1235
|
}
|
|
1204
1236
|
}
|
|
1205
1237
|
|
|
1238
|
+
/**
|
|
1239
|
+
* Um schedule com nome de action errado dispararia em silêncio para sempre. Schedules com
|
|
1240
|
+
* `enabled: false` ficam de fora: não são registrados e podem apontar para uma action que
|
|
1241
|
+
* ainda não existe neste ambiente.
|
|
1242
|
+
*/
|
|
1243
|
+
private assertScheduledActionsExist(): void {
|
|
1244
|
+
const missing = [...this.schedules.values()].filter(
|
|
1245
|
+
(schedule) => schedule.enabled !== false && !this.actions.has(schedule.action),
|
|
1246
|
+
)
|
|
1247
|
+
if (missing.length === 0) return
|
|
1248
|
+
const list = missing
|
|
1249
|
+
.map((schedule) => `"${schedule.name}" → "${schedule.action}"`)
|
|
1250
|
+
.join(', ')
|
|
1251
|
+
throw error({
|
|
1252
|
+
code: 'runtime.schedule_action_not_found',
|
|
1253
|
+
category: 'internal',
|
|
1254
|
+
message:
|
|
1255
|
+
`O runtime não iniciou porque ${
|
|
1256
|
+
missing.length === 1
|
|
1257
|
+
? 'um schedule aponta para uma action não registrada'
|
|
1258
|
+
: `${missing.length} schedules apontam para actions não registradas`
|
|
1259
|
+
}: ${list}. Corrija o nome em \`action\` ou registre a action no mesmo runtime; para ` +
|
|
1260
|
+
'manter o schedule sem executar, declare `enabled: false`.',
|
|
1261
|
+
meta: {
|
|
1262
|
+
schedules: missing.map((schedule) => ({ schedule: schedule.name, action: schedule.action })),
|
|
1263
|
+
},
|
|
1264
|
+
})
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1206
1267
|
private registerSchedule(schedule: ScheduleDef): void {
|
|
1207
1268
|
if (schedule.enabled === false) {
|
|
1208
1269
|
this.log.info('schedule disabled', { schedule: schedule.name })
|
|
@@ -1221,12 +1282,45 @@ export class Runtime {
|
|
|
1221
1282
|
typeof schedule.input === 'function'
|
|
1222
1283
|
? await (schedule.input as () => unknown | Promise<unknown>)()
|
|
1223
1284
|
: (schedule.input ?? {})
|
|
1224
|
-
await this.execute(schedule.action, input, {
|
|
1285
|
+
const result = await this.execute(schedule.action, input, {
|
|
1225
1286
|
user: null,
|
|
1226
1287
|
tenantId: null,
|
|
1227
1288
|
can: () => false,
|
|
1228
1289
|
provenance: { kind: 'schedule', scheduleName: schedule.name },
|
|
1229
1290
|
})
|
|
1291
|
+
if (result.ok) return
|
|
1292
|
+
// Ninguém espera o resultado de um disparo: sem este registro, a falha só existiria no
|
|
1293
|
+
// audit da action. Action inexistente não chega aqui, porque o start() recusa o schedule.
|
|
1294
|
+
this.log.error(
|
|
1295
|
+
`O schedule "${schedule.name}" não concluiu a action "${schedule.action}" (${result.error.code}). ` +
|
|
1296
|
+
'O agendamento continua ativo e o próximo disparo executa a action de novo. Confira a ' +
|
|
1297
|
+
'mensagem e o audit da action; `auth.unauthenticated` indica que a action agendada precisa ' +
|
|
1298
|
+
'declarar `public: true`.',
|
|
1299
|
+
{
|
|
1300
|
+
schedule: schedule.name,
|
|
1301
|
+
action: schedule.action,
|
|
1302
|
+
code: result.error.code,
|
|
1303
|
+
message: result.error.message,
|
|
1304
|
+
actionId: result.meta.actionId,
|
|
1305
|
+
...(result.error.cause !== undefined ? { cause: result.error.cause } : {}),
|
|
1306
|
+
},
|
|
1307
|
+
)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* `ctx.emit` de reaction não publica: encadear reactions por evento ainda não é suportado.
|
|
1312
|
+
* Fora de produção, o descarte vira aviso para que o evento não pareça ter sido entregue.
|
|
1313
|
+
*/
|
|
1314
|
+
private buildReactionEmit(reaction: ReactionDef<any>, log: Logger): ReactionContext['emit'] {
|
|
1315
|
+
return async (event) => {
|
|
1316
|
+
if (!this.config.dev) return
|
|
1317
|
+
log.warn(
|
|
1318
|
+
`A reaction "${reaction.name}" chamou ctx.emit('${event}'), mas reactions não publicam ` +
|
|
1319
|
+
'eventos: o evento foi descartado e nenhuma reaction ou integração o recebe. Para ' +
|
|
1320
|
+
'anunciar esse fato, emita-o no handler da action que o produz e declare-o em `emits`.',
|
|
1321
|
+
{ code: 'runtime.reaction_emit_ignored', reaction: reaction.name, event },
|
|
1322
|
+
)
|
|
1323
|
+
}
|
|
1230
1324
|
}
|
|
1231
1325
|
|
|
1232
1326
|
private async runReaction(
|
|
@@ -1260,7 +1354,7 @@ export class Runtime {
|
|
|
1260
1354
|
tenantId: event.tenant ?? null,
|
|
1261
1355
|
db: this.data?.contextExtension().db,
|
|
1262
1356
|
log: reactionLog,
|
|
1263
|
-
emit:
|
|
1357
|
+
emit: this.buildReactionEmit(reaction, reactionLog),
|
|
1264
1358
|
storage: this.storage ?? null,
|
|
1265
1359
|
cache: this.cache ?? null,
|
|
1266
1360
|
// Reação = contexto de sistema; o agente só alcança actions públicas (can nega por
|
|
@@ -1411,12 +1505,6 @@ function severityForError(err: any): 'info' | 'warning' | 'error' {
|
|
|
1411
1505
|
return 'error'
|
|
1412
1506
|
}
|
|
1413
1507
|
|
|
1414
|
-
/**
|
|
1415
|
-
* Placeholder pra `ctx.emit` em reactions. Reaction chain (reaction emite
|
|
1416
|
-
* eventos novos que disparam outras reactions) entra em v1+.
|
|
1417
|
-
*/
|
|
1418
|
-
const noopEmit: ActionContext['emit'] = async () => undefined
|
|
1419
|
-
|
|
1420
1508
|
function generateId(): string {
|
|
1421
1509
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
1422
1510
|
return crypto.randomUUID()
|
package/src/core/types.ts
CHANGED
|
@@ -255,8 +255,10 @@ export interface User {
|
|
|
255
255
|
export type CanFn = (permission: string, resource?: unknown) => boolean | Promise<boolean>
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
* Função que emite um `DomainEvent`.
|
|
259
|
-
* `ReactionContext.emit
|
|
258
|
+
* Função que emite um `DomainEvent`. Em `ActionContext.emit`, publica o evento pelo
|
|
259
|
+
* `EventBusAdapter`. Em `ReactionContext.emit`, descarta o evento sem publicá-lo, porque
|
|
260
|
+
* reactions ainda não encadeiam outras por evento; com `config.dev` ativo, cada descarte
|
|
261
|
+
* registra o aviso `runtime.reaction_emit_ignored`.
|
|
260
262
|
*/
|
|
261
263
|
export type EmitFn = <T = unknown>(
|
|
262
264
|
event: string,
|
|
@@ -293,6 +295,7 @@ export interface ReactionContext {
|
|
|
293
295
|
tenantId: string | null
|
|
294
296
|
db: unknown
|
|
295
297
|
log: Logger
|
|
298
|
+
/** Descarta o evento sem publicá-lo: reactions ainda não encadeiam outras. Ver `EmitFn`. */
|
|
296
299
|
emit: EmitFn
|
|
297
300
|
storage: StorageAdapter | null
|
|
298
301
|
cache: CacheAdapter | null
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
CardTitle,
|
|
14
14
|
} from "../primitives/card.tsx";
|
|
15
15
|
import { ActionForm, type ActionFormProps } from "./form.tsx";
|
|
16
|
+
import type { HeadingLevel } from "./heading-level.ts";
|
|
16
17
|
import { ButtonGroup } from "../primitives/button-group.tsx";
|
|
17
18
|
|
|
18
19
|
export interface ActionFormCardProps<
|
|
@@ -21,6 +22,12 @@ export interface ActionFormCardProps<
|
|
|
21
22
|
> extends Omit<ActionFormProps<TInput, TData>, "body" | "footer"> {
|
|
22
23
|
/** Título do header do card. Sem ele, o card começa direto no conteúdo. */
|
|
23
24
|
title?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Nível semântico do título (`h1`–`h6`), repassado ao `CardTitle` como `level`. Sem
|
|
27
|
+
* ele vale o `h3` do Card. Muda só a hierarquia do documento; a escala visual continua
|
|
28
|
+
* a do Card.
|
|
29
|
+
*/
|
|
30
|
+
titleLevel?: HeadingLevel;
|
|
24
31
|
/** Subtítulo opcional, abaixo do título. */
|
|
25
32
|
description?: string;
|
|
26
33
|
/** Classes do Card (a superfície). `className` vai pro <form>. */
|
|
@@ -29,6 +36,7 @@ export interface ActionFormCardProps<
|
|
|
29
36
|
|
|
30
37
|
export function ActionFormCard<TInput extends Record<string, unknown>, TData>({
|
|
31
38
|
title,
|
|
39
|
+
titleLevel,
|
|
32
40
|
description,
|
|
33
41
|
cardClassName,
|
|
34
42
|
...rest
|
|
@@ -37,7 +45,9 @@ export function ActionFormCard<TInput extends Record<string, unknown>, TData>({
|
|
|
37
45
|
<Card className={cardClassName}>
|
|
38
46
|
{(title !== undefined || description !== undefined) && (
|
|
39
47
|
<CardHeader>
|
|
40
|
-
{title !== undefined &&
|
|
48
|
+
{title !== undefined && (
|
|
49
|
+
<CardTitle level={titleLevel}>{title}</CardTitle>
|
|
50
|
+
)}
|
|
41
51
|
{description !== undefined && (
|
|
42
52
|
<CardDescription>{description}</CardDescription>
|
|
43
53
|
)}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { cn } from "../../lib/cn.ts";
|
|
13
|
+
import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
14
|
+
import { headingTags } from "./heading-level.ts";
|
|
13
15
|
import { usePageHeadingPresence } from "./page-heading-context.tsx";
|
|
14
16
|
import {
|
|
15
17
|
SurfaceHeader,
|
|
@@ -121,6 +123,7 @@ export function Content({
|
|
|
121
123
|
...props
|
|
122
124
|
}: ContentProps): ReactElement {
|
|
123
125
|
const titleId = useId();
|
|
126
|
+
const fill = useFillRegion();
|
|
124
127
|
const context = {
|
|
125
128
|
element: as,
|
|
126
129
|
frameVariant,
|
|
@@ -184,6 +187,7 @@ export function Content({
|
|
|
184
187
|
data-as="fieldset"
|
|
185
188
|
data-framed={framed}
|
|
186
189
|
data-frame-variant={frameVariant}
|
|
190
|
+
{...fill.attributes}
|
|
187
191
|
aria-labelledby={ariaLabelledBy}
|
|
188
192
|
className={cn(
|
|
189
193
|
!framed && "space-y-3",
|
|
@@ -193,11 +197,12 @@ export function Content({
|
|
|
193
197
|
framed &&
|
|
194
198
|
frameVariant === "legend" &&
|
|
195
199
|
"space-y-3 rounded-lg border border-border px-4 pb-4",
|
|
200
|
+
fill.filling && fillRegionClassName,
|
|
196
201
|
className,
|
|
197
202
|
)}
|
|
198
203
|
{...(props as ComponentProps<"fieldset">)}
|
|
199
204
|
>
|
|
200
|
-
{anatomy}
|
|
205
|
+
{fill.provide(anatomy)}
|
|
201
206
|
</fieldset>
|
|
202
207
|
) : (
|
|
203
208
|
<section
|
|
@@ -205,15 +210,17 @@ export function Content({
|
|
|
205
210
|
data-as="section"
|
|
206
211
|
data-framed={framed}
|
|
207
212
|
data-frame-variant={frameVariant}
|
|
213
|
+
{...fill.attributes}
|
|
208
214
|
aria-labelledby={ariaLabelledBy}
|
|
209
215
|
className={cn(
|
|
210
216
|
"space-y-3",
|
|
211
217
|
framed && "rounded-lg border border-border p-4",
|
|
218
|
+
fill.filling && fillRegionClassName,
|
|
212
219
|
className,
|
|
213
220
|
)}
|
|
214
221
|
{...(props as ComponentProps<"section">)}
|
|
215
222
|
>
|
|
216
|
-
{anatomy}
|
|
223
|
+
{fill.provide(anatomy)}
|
|
217
224
|
</section>
|
|
218
225
|
)}
|
|
219
226
|
</ContentContext.Provider>
|
|
@@ -291,15 +298,6 @@ export function ContentHeader({
|
|
|
291
298
|
);
|
|
292
299
|
}
|
|
293
300
|
|
|
294
|
-
const headingTags = {
|
|
295
|
-
1: "h1",
|
|
296
|
-
2: "h2",
|
|
297
|
-
3: "h3",
|
|
298
|
-
4: "h4",
|
|
299
|
-
5: "h5",
|
|
300
|
-
6: "h6",
|
|
301
|
-
} as const;
|
|
302
|
-
|
|
303
301
|
export function ContentTitle({
|
|
304
302
|
className,
|
|
305
303
|
id,
|
|
@@ -387,6 +385,7 @@ export function ContentActions({
|
|
|
387
385
|
|
|
388
386
|
export function ContentBody({
|
|
389
387
|
className,
|
|
388
|
+
children,
|
|
390
389
|
...props
|
|
391
390
|
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
392
391
|
const content = requiredContext(
|
|
@@ -394,18 +393,23 @@ export function ContentBody({
|
|
|
394
393
|
"ContentBody",
|
|
395
394
|
"Content",
|
|
396
395
|
);
|
|
396
|
+
const fill = useFillRegion();
|
|
397
397
|
return (
|
|
398
398
|
<div
|
|
399
399
|
data-slot="content-body"
|
|
400
|
+
{...fill.attributes}
|
|
400
401
|
className={cn(
|
|
401
402
|
content.element === "fieldset" && "space-y-3",
|
|
402
403
|
content.element === "fieldset" &&
|
|
403
404
|
content.framed &&
|
|
404
405
|
content.frameVariant === "default" &&
|
|
405
406
|
"clear-left pt-3",
|
|
407
|
+
fill.filling && fillRegionClassName,
|
|
406
408
|
className,
|
|
407
409
|
)}
|
|
408
410
|
{...props}
|
|
409
|
-
|
|
411
|
+
>
|
|
412
|
+
{fill.provide(children)}
|
|
413
|
+
</div>
|
|
410
414
|
);
|
|
411
415
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Propagação da ocupação de altura (interno) — ADR 0024.
|
|
3
|
+
*
|
|
4
|
+
* `Page layout="fill"` limita a página à altura do shell; a `Presentation` em página herda
|
|
5
|
+
* esse limite pela `Page` que ela monta. Para uma região interna ocupar a folga e rolar
|
|
6
|
+
* sozinha, cada elemento entre a raiz limitada e essa região precisa repassar a altura como
|
|
7
|
+
* coluna flexível. Este módulo faz essa cadeia se formar sem classes no consumidor:
|
|
8
|
+
*
|
|
9
|
+
* - a raiz (`FillRoot`) publica o contexto;
|
|
10
|
+
* - os elos canônicos (`PageBody`, `Content`, `ContentBody`, o corpo da `Presentation`)
|
|
11
|
+
* leem o contexto com `useFillRegion` e o republicam para os filhos;
|
|
12
|
+
* - a folha (`ActionList`) usa `useFillLeaf`: assume `fill` por padrão quando o elemento
|
|
13
|
+
* pai é um elo e se registra na cadeia;
|
|
14
|
+
* - um elo só vira coluna flexível quando uma folha se registrou abaixo dele. Irmãos sem
|
|
15
|
+
* folha mantêm a altura do próprio conteúdo, e uma página `fill` sem lista não muda;
|
|
16
|
+
* - superfícies com anatomia própria (Card, Dialog, Drawer, Pane) cortam a cadeia com
|
|
17
|
+
* `FillBoundary`: a altura delas é decisão delas, inclusive quando um portal as renderiza
|
|
18
|
+
* dentro de uma página `fill`.
|
|
19
|
+
*
|
|
20
|
+
* O registro acontece em `useLayoutEffect`, então as classes chegam antes da primeira pintura.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
createContext,
|
|
25
|
+
createElement,
|
|
26
|
+
useContext,
|
|
27
|
+
useLayoutEffect,
|
|
28
|
+
useMemo,
|
|
29
|
+
useRef,
|
|
30
|
+
useState,
|
|
31
|
+
type ReactElement,
|
|
32
|
+
type ReactNode,
|
|
33
|
+
type RefObject,
|
|
34
|
+
} from "react";
|
|
35
|
+
|
|
36
|
+
export type FillLayout = "flow" | "fill";
|
|
37
|
+
|
|
38
|
+
interface FillChain {
|
|
39
|
+
/** Anuncia uma folha abaixo deste elo; devolve a retirada do anúncio. */
|
|
40
|
+
register(): () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const FillChainContext = createContext<FillChain | null>(null);
|
|
44
|
+
|
|
45
|
+
/** A raiz já tem altura limitada: não há o que propagar acima dela. */
|
|
46
|
+
const rootChain: FillChain = { register: () => () => {} };
|
|
47
|
+
|
|
48
|
+
/** Classes de um elo que repassa a altura à folha registrada abaixo dele. */
|
|
49
|
+
export const fillRegionClassName = "flex min-h-0 flex-1 flex-col";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Atributo que marca um elo da cadeia no DOM: `"true"` quando repassa a altura, `"false"`
|
|
53
|
+
* quando está na cadeia sem folha abaixo. A folha só assume `fill` implícito quando o
|
|
54
|
+
* elemento pai o carrega — um contêiner próprio no meio não repassa a altura.
|
|
55
|
+
*/
|
|
56
|
+
export const fillRegionAttribute = "data-fill";
|
|
57
|
+
|
|
58
|
+
/** Publica a cadeia quando `active`; caso contrário a interrompe. */
|
|
59
|
+
export function FillRoot({
|
|
60
|
+
active,
|
|
61
|
+
children,
|
|
62
|
+
}: {
|
|
63
|
+
active: boolean;
|
|
64
|
+
children?: ReactNode;
|
|
65
|
+
}): ReactElement {
|
|
66
|
+
return createElement(
|
|
67
|
+
FillChainContext.Provider,
|
|
68
|
+
{ value: active ? rootChain : null },
|
|
69
|
+
children,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Interrompe a cadeia: o conteúdo abaixo decide a própria altura. */
|
|
74
|
+
export function FillBoundary({
|
|
75
|
+
children,
|
|
76
|
+
}: {
|
|
77
|
+
children?: ReactNode;
|
|
78
|
+
}): ReactElement {
|
|
79
|
+
return createElement(FillChainContext.Provider, { value: null }, children);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FillRegion {
|
|
83
|
+
/** O elo está em uma cadeia `fill`, com ou sem folha abaixo. */
|
|
84
|
+
inChain: boolean;
|
|
85
|
+
/** Uma folha se registrou abaixo: o elo deve repassar a altura. */
|
|
86
|
+
filling: boolean;
|
|
87
|
+
/** Atributos do elemento do elo. */
|
|
88
|
+
attributes: { "data-fill"?: "true" | "false" };
|
|
89
|
+
/** Republica a cadeia para os filhos do elo. */
|
|
90
|
+
provide(children: ReactNode): ReactElement;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Elo da cadeia: lê o contexto, conta as folhas abaixo e o republica. */
|
|
94
|
+
export function useFillRegion(): FillRegion {
|
|
95
|
+
const parent = useContext(FillChainContext);
|
|
96
|
+
const [leaves, setLeaves] = useState(0);
|
|
97
|
+
const chain = useMemo<FillChain | null>(
|
|
98
|
+
() =>
|
|
99
|
+
parent === null
|
|
100
|
+
? null
|
|
101
|
+
: {
|
|
102
|
+
register() {
|
|
103
|
+
setLeaves((count) => count + 1);
|
|
104
|
+
const releaseParent = parent.register();
|
|
105
|
+
let registered = true;
|
|
106
|
+
return () => {
|
|
107
|
+
if (!registered) return;
|
|
108
|
+
registered = false;
|
|
109
|
+
setLeaves((count) => Math.max(0, count - 1));
|
|
110
|
+
releaseParent();
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
[parent],
|
|
115
|
+
);
|
|
116
|
+
const inChain = parent !== null;
|
|
117
|
+
const filling = inChain && leaves > 0;
|
|
118
|
+
return {
|
|
119
|
+
inChain,
|
|
120
|
+
filling,
|
|
121
|
+
attributes: inChain ? { "data-fill": filling ? "true" : "false" } : {},
|
|
122
|
+
provide: (children) =>
|
|
123
|
+
createElement(FillChainContext.Provider, { value: chain }, children),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Folha da cadeia. Sem `layout` explícito, assume `fill` quando o elemento pai é um elo;
|
|
129
|
+
* com `layout="fill"` explícito, registra-se mesmo sob um contêiner próprio, que passa a
|
|
130
|
+
* ser responsabilidade de quem o escreveu. `layout="flow"` nunca se registra.
|
|
131
|
+
*/
|
|
132
|
+
export function useFillLeaf<TElement extends HTMLElement>(
|
|
133
|
+
layout: FillLayout | undefined,
|
|
134
|
+
): { layout: FillLayout; ref: RefObject<TElement | null> } {
|
|
135
|
+
const chain = useContext(FillChainContext);
|
|
136
|
+
const ref = useRef<TElement | null>(null);
|
|
137
|
+
const [linked, setLinked] = useState(false);
|
|
138
|
+
useLayoutEffect(() => {
|
|
139
|
+
setLinked(
|
|
140
|
+
chain !== null &&
|
|
141
|
+
ref.current?.parentElement?.hasAttribute(fillRegionAttribute) === true,
|
|
142
|
+
);
|
|
143
|
+
}, [chain]);
|
|
144
|
+
const resolved = layout ?? (linked ? "fill" : "flow");
|
|
145
|
+
useLayoutEffect(() => {
|
|
146
|
+
if (chain === null || resolved !== "fill") return undefined;
|
|
147
|
+
return chain.register();
|
|
148
|
+
}, [chain, resolved]);
|
|
149
|
+
return { layout: resolved, ref };
|
|
150
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nível de heading (interno): hierarquia do documento, separada do tamanho visual.
|
|
3
|
+
*
|
|
4
|
+
* A ADR 0023 fixa a regra: `h1`–`h6` expressam hierarquia; a escala do título vem da
|
|
5
|
+
* família e do layout. As duas decisões nunca se misturam num mesmo valor.
|
|
6
|
+
*/
|
|
7
|
+
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
|
+
|
|
9
|
+
export const headingTags = {
|
|
10
|
+
1: "h1",
|
|
11
|
+
2: "h2",
|
|
12
|
+
3: "h3",
|
|
13
|
+
4: "h4",
|
|
14
|
+
5: "h5",
|
|
15
|
+
6: "h6",
|
|
16
|
+
} as const satisfies Record<HeadingLevel, `h${HeadingLevel}`>;
|