@aurea-uds/react 0.2.0 → 0.3.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/README.md +34 -2
- package/dist/actions.d.ts +10 -3
- package/dist/actions.js +55 -6
- package/dist/code-client.d.ts +6 -0
- package/dist/code-client.js +24 -0
- package/dist/code.d.ts +2 -13
- package/dist/code.js +5 -34
- package/dist/data-display-client.d.ts +14 -0
- package/dist/data-display-client.js +22 -0
- package/dist/data-display.d.ts +2 -23
- package/dist/data-display.js +5 -14
- package/dist/feedback-client.d.ts +64 -0
- package/dist/feedback-client.js +110 -0
- package/dist/feedback.d.ts +2 -63
- package/dist/feedback.js +5 -101
- package/dist/file-input.js +57 -9
- package/dist/identity-client.d.ts +9 -0
- package/dist/identity-client.js +12 -0
- package/dist/identity.d.ts +2 -17
- package/dist/identity.js +5 -25
- package/dist/index.d.ts +2 -1
- package/dist/index.js +19 -3
- package/dist/inputs-client.d.ts +105 -0
- package/dist/inputs-client.js +264 -0
- package/dist/inputs.d.ts +2 -91
- package/dist/inputs.js +9 -126
- package/dist/internal.d.ts +20 -5
- package/dist/internal.js +91 -10
- package/dist/layout-client.d.ts +9 -0
- package/dist/layout-client.js +72 -0
- package/dist/layout.d.ts +2 -15
- package/dist/layout.js +5 -76
- package/dist/markup.d.ts +72 -0
- package/dist/markup.js +87 -0
- package/dist/media-client.d.ts +36 -0
- package/dist/media-client.js +239 -0
- package/dist/media.d.ts +2 -9
- package/dist/media.js +5 -99
- package/dist/navigation-client.d.ts +94 -0
- package/dist/navigation-client.js +154 -0
- package/dist/navigation.d.ts +2 -85
- package/dist/navigation.js +5 -138
- package/dist/overlays.d.ts +23 -0
- package/dist/overlays.js +45 -1
- package/dist/pure.d.ts +30 -0
- package/dist/pure.js +26 -2
- package/dist/system.d.ts +9 -0
- package/dist/system.js +46 -0
- package/package.json +180 -176
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// Fase 9 (achado A5): este arquivo saiu do index.tsx de 970 linhas. Um módulo por categoria
|
|
4
|
+
// do registry — a taxonomia já existia e é gateada. A ordem de import entre eles é um DAG:
|
|
5
|
+
// internal → system → actions → feedback → inputs → navigation → layout → data-display → resto.
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
8
|
+
import { cx, useAureaStrings } from "./internal.js";
|
|
9
|
+
import { Icon } from "./system.js";
|
|
10
|
+
import { IconButton } from "./actions.js";
|
|
11
|
+
// A galeria AMPLIA num Dialog, e o Dialog já existe (trava do item L2). O import é para `overlays`,
|
|
12
|
+
// que como este módulo mora no "resto" do DAG e não importa `media` — não há ciclo.
|
|
13
|
+
import { Dialog } from "./overlays.js";
|
|
14
|
+
// MediaPlayer (Fase 5): headless sobre <video>/<audio> nativos — o MOTOR de mídia é
|
|
15
|
+
// o browser; o componente só liga estado (play/tempo/buffer/volume/legenda) às
|
|
16
|
+
// classes .media-* já existentes. Sem CSS estrutural novo: o vídeo preenche o
|
|
17
|
+
// .media-viewport por style inline no elemento (não por regra no stylesheet), então
|
|
18
|
+
// o core e os baselines dos docs não mudam.
|
|
19
|
+
// A11y (o APG não fecha player; prática corrente pesquisada 07/2026): os controles
|
|
20
|
+
// são <button> nativos com aria-label que troca de estado (Reproduzir/Pausar,
|
|
21
|
+
// Silenciar/Ativar som) e a barra é <input type="range"> nativo — um slider de
|
|
22
|
+
// verdade, com setas/Home/End vindos do browser — cujo aria-valuetext lê o tempo por
|
|
23
|
+
// extenso (o número cru "243" não se entende; "4 minutes and 3 seconds" sim). Nada de
|
|
24
|
+
// role="slider" à mão. SEM atalho global de teclado: cada controle é focável e opera
|
|
25
|
+
// pelo próprio elemento nativo, então play/pause/seek/volume por teclado saem sem
|
|
26
|
+
// interceptar — interceptar quebraria digitação e não é padrão APG. O tempo por
|
|
27
|
+
// extenso é helper embutido em inglês (não i18n — como o formatSize do FileInput);
|
|
28
|
+
// os rótulos de botão passam pela i18n como todo controle.
|
|
29
|
+
function clockTime(sec) { if (!Number.isFinite(sec) || sec < 0)
|
|
30
|
+
sec = 0; const m = Math.floor(sec / 60), s = Math.floor(sec % 60); return `${m}:${String(s).padStart(2, "0")}`; }
|
|
31
|
+
export function spokenTime(sec) { if (!Number.isFinite(sec) || sec < 0)
|
|
32
|
+
sec = 0; const m = Math.floor(sec / 60), s = Math.floor(sec % 60); const mp = m === 1 ? "minute" : "minutes", sp = s === 1 ? "second" : "seconds"; return m && s ? `${m} ${mp} and ${s} ${sp}` : m ? `${m} ${mp}` : `${s} ${sp}`; }
|
|
33
|
+
// Os handlers de mídia do consumidor são COMPOSTOS com os internos (não podem
|
|
34
|
+
// sobrescrevê-los: um onPlay externo silenciaria o estado playing e o botão
|
|
35
|
+
// ficaria em "Reproduzir" — auditoria 18/07/2026, MÉDIO 2).
|
|
36
|
+
export function MediaPlayer({ kind = "video", src, poster, title, subtitle, className, children, onClick, onPlay, onPause, onEnded, onTimeUpdate, onLoadedMetadata, onDurationChange, onProgress, onVolumeChange, ...rest }) {
|
|
37
|
+
const s = useAureaStrings();
|
|
38
|
+
const boxRef = React.useRef(null);
|
|
39
|
+
const mediaRef = React.useRef(null);
|
|
40
|
+
const [playing, setPlaying] = React.useState(false);
|
|
41
|
+
const [current, setCurrent] = React.useState(0);
|
|
42
|
+
const [duration, setDuration] = React.useState(0);
|
|
43
|
+
const [buffered, setBuffered] = React.useState(0);
|
|
44
|
+
const [volume, setVolume] = React.useState(1);
|
|
45
|
+
const [muted, setMuted] = React.useState(false);
|
|
46
|
+
const [hasCaptions, setHasCaptions] = React.useState(false);
|
|
47
|
+
const [captionsOn, setCaptionsOn] = React.useState(false);
|
|
48
|
+
const [fullscreen, setFullscreen] = React.useState(false);
|
|
49
|
+
// fullscreenchange vem do document (não do elemento); é o único estado que exige
|
|
50
|
+
// listener — o resto sincroniza pelos eventos de mídia do próprio <video>.
|
|
51
|
+
React.useEffect(() => { const on = () => setFullscreen(document.fullscreenElement === boxRef.current); document.addEventListener("fullscreenchange", on); return () => document.removeEventListener("fullscreenchange", on); }, []);
|
|
52
|
+
const m = () => mediaRef.current;
|
|
53
|
+
// ramifica pelo estado sincronizado (playing), não por el.paused: playing vem dos
|
|
54
|
+
// eventos play/pause e não fica atrás do elemento (nem preso, como no jsdom).
|
|
55
|
+
const togglePlay = () => { const el = m(); if (!el)
|
|
56
|
+
return; if (playing)
|
|
57
|
+
el.pause();
|
|
58
|
+
else {
|
|
59
|
+
const p = el.play();
|
|
60
|
+
if (p)
|
|
61
|
+
p.catch(() => { });
|
|
62
|
+
} };
|
|
63
|
+
const skip = (d) => { const el = m(); if (el)
|
|
64
|
+
el.currentTime = Math.max(0, Math.min(el.currentTime + d, el.duration || Infinity)); };
|
|
65
|
+
// seek/setVol/mute atualizam o estado na hora (input controlado responsivo) e o
|
|
66
|
+
// elemento; os eventos de mídia reconfirmam depois (idempotente) e cobrem mudanças
|
|
67
|
+
// externas (controles nativos, outra aba).
|
|
68
|
+
const seek = (v) => { const el = m(); if (el)
|
|
69
|
+
el.currentTime = v; setCurrent(v); };
|
|
70
|
+
const setVol = (v) => { const el = m(); if (el) {
|
|
71
|
+
el.volume = v;
|
|
72
|
+
if (v > 0)
|
|
73
|
+
el.muted = false;
|
|
74
|
+
} setVolume(v); if (v > 0)
|
|
75
|
+
setMuted(false); };
|
|
76
|
+
const toggleMute = () => { const el = m(); const next = el ? !el.muted : !muted; if (el)
|
|
77
|
+
el.muted = next; setMuted(next); };
|
|
78
|
+
const toggleCaptions = () => { const el = m(); if (!el || !el.textTracks.length)
|
|
79
|
+
return; const show = el.textTracks[0].mode !== "showing"; el.textTracks[0].mode = show ? "showing" : "hidden"; setCaptionsOn(show); };
|
|
80
|
+
const toggleFullscreen = () => { if (document.fullscreenElement)
|
|
81
|
+
document.exitFullscreen?.();
|
|
82
|
+
else
|
|
83
|
+
boxRef.current?.requestFullscreen?.(); };
|
|
84
|
+
const syncVol = () => { const el = m(); if (el) {
|
|
85
|
+
setVolume(el.volume);
|
|
86
|
+
setMuted(el.muted);
|
|
87
|
+
} };
|
|
88
|
+
const syncBuf = () => { const el = m(); if (el && el.buffered.length)
|
|
89
|
+
setBuffered(el.buffered.end(el.buffered.length - 1)); };
|
|
90
|
+
const onMeta = () => { const el = m(); if (el) {
|
|
91
|
+
setDuration(el.duration);
|
|
92
|
+
setHasCaptions(el.textTracks.length > 0);
|
|
93
|
+
syncVol();
|
|
94
|
+
} };
|
|
95
|
+
const playedPct = duration > 0 ? Math.min(100, (current / duration) * 100) : 0;
|
|
96
|
+
const bufferedPct = duration > 0 ? Math.min(100, (buffered / duration) * 100) : 0;
|
|
97
|
+
const volPct = muted ? 0 : Math.round(volume * 100);
|
|
98
|
+
const valueText = duration > 0 ? `${spokenTime(current)} of ${spokenTime(duration)}` : spokenTime(current);
|
|
99
|
+
const MediaTag = kind === "audio" ? "audio" : "video";
|
|
100
|
+
return _jsxs("div", { ref: boxRef, className: cx("media-player", className), role: "group", "aria-label": typeof title === "string" ? title : s.mediaPlayer, children: [_jsxs("div", { className: "media-viewport", children: [kind === "audio" && _jsxs("div", { className: "media-placeholder", children: [_jsx(Icon, { name: "volume--up" }), title && _jsx("strong", { children: title }), subtitle && _jsx("span", { children: subtitle })] }), _jsx(MediaTag, { ref: mediaRef, src: src, ...rest, ...(kind === "video" ? { poster, playsInline: true, className: "media-fill", onClick: (e) => { onClick?.(e); togglePlay(); } } : { onClick }), onPlay: (e) => { setPlaying(true); onPlay?.(e); }, onPause: (e) => { setPlaying(false); onPause?.(e); }, onEnded: (e) => { setPlaying(false); onEnded?.(e); }, onTimeUpdate: (e) => { const el = m(); if (el)
|
|
101
|
+
setCurrent(el.currentTime); onTimeUpdate?.(e); }, onLoadedMetadata: (e) => { onMeta(); onLoadedMetadata?.(e); }, onDurationChange: (e) => { const el = m(); if (el)
|
|
102
|
+
setDuration(el.duration); onDurationChange?.(e); }, onProgress: (e) => { syncBuf(); onProgress?.(e); }, onVolumeChange: (e) => { syncVol(); onVolumeChange?.(e); }, children: children })] }), kind === "video" && title && _jsx("div", { className: "media-overlay-title", children: _jsxs("div", { children: [_jsx("strong", { children: title }), subtitle && _jsx("span", { children: subtitle })] }) }), _jsxs("div", { className: "media-controls", children: [_jsxs("div", { className: "media-seek", style: { "--media-played": `${playedPct}%` }, children: [_jsxs("div", { className: "media-seek-track", children: [_jsx("span", { className: "media-seek-buffered", style: { width: `${bufferedPct}%` } }), _jsx("span", { className: "media-seek-played" })] }), _jsx("input", { type: "range", min: 0, max: duration > 0 ? duration : 0, step: "any", value: Math.min(current, duration || 0), "aria-label": s.mediaSeek, "aria-valuetext": valueText, onChange: e => seek(Number(e.target.value)) })] }), _jsxs("div", { className: "media-control-row", children: [_jsxs("div", { className: "media-control-group", children: [_jsx("button", { className: "media-control", type: "button", "aria-label": playing ? s.mediaPause : s.mediaPlay, onClick: togglePlay, children: _jsx(Icon, { name: playing ? "pause" : "play" }) }), _jsx("button", { className: "media-control", type: "button", "aria-label": s.mediaSkipBack, onClick: () => skip(-10), children: _jsx(Icon, { name: "rewind--10" }) }), _jsx("button", { className: "media-control", type: "button", "aria-label": s.mediaSkipForward, onClick: () => skip(10), children: _jsx(Icon, { name: "forward--10" }) }), _jsxs("span", { className: "media-time", children: [clockTime(current), " / ", clockTime(duration)] })] }), _jsxs("div", { className: "media-control-group", children: [_jsx("button", { className: "media-control", type: "button", "aria-label": muted ? s.mediaUnmute : s.mediaMute, onClick: toggleMute, children: _jsx(Icon, { name: muted || volume === 0 ? "volume--mute" : "volume--up" }) }), _jsx("input", { className: "media-volume", type: "range", min: 0, max: 100, value: volPct, "aria-label": s.mediaVolume, "aria-valuetext": `${volPct}%`, onChange: e => setVol(Number(e.target.value) / 100) }), hasCaptions && _jsx("button", { className: cx("media-control", captionsOn && "active"), type: "button", "aria-label": captionsOn ? s.mediaCaptionsHide : s.mediaCaptionsShow, "aria-pressed": captionsOn, onClick: toggleCaptions, children: _jsx(Icon, { name: "closed-caption" }) }), kind === "video" && _jsx("button", { className: "media-control", type: "button", "aria-label": fullscreen ? s.mediaFullscreenExit : s.mediaFullscreenEnter, onClick: toggleFullscreen, children: _jsx(Icon, { name: fullscreen ? "minimize" : "maximize" }) })] })] })] })] });
|
|
103
|
+
}
|
|
104
|
+
export function Image({ ratio, fit, alt, className, style, render, onError, ...props }) {
|
|
105
|
+
const [quebrou, setQuebrou] = React.useState(false);
|
|
106
|
+
// A proporção vai INLINE e só quando existe. Declará-la no core com fallback `auto` custou uma
|
|
107
|
+
// medição em 15/08/2026: `auto` vence o `aspect-ratio: auto <width>/<height>` que o navegador
|
|
108
|
+
// deriva dos atributos, e uma imagem com `width`/`height` e sem `ratio` saía com altura ZERO —
|
|
109
|
+
// a regra escrita contra o salto de layout produzindo o salto. Sem prop, o navegador decide.
|
|
110
|
+
const estilo = { ...(ratio ? { aspectRatio: ratio } : null), ...style };
|
|
111
|
+
// O hook roda SEMPRE, antes de qualquer saída antecipada — trocar a ordem dos hooks entre
|
|
112
|
+
// renders é o que o React proíbe, e o ramo do erro é uma saída antecipada.
|
|
113
|
+
const elemento = useRender({ defaultTagName: "img", render,
|
|
114
|
+
// Os defaults vêm ANTES do spread do consumidor, e a ordem foi corrigida por um teste que
|
|
115
|
+
// reprovou: escritos depois, `loading="eager"` numa imagem de topo de página era engolido pelo
|
|
116
|
+
// nosso `lazy` — o componente prometia default e entregava imposição.
|
|
117
|
+
props: { loading: "lazy", decoding: "async", ...props, alt,
|
|
118
|
+
className: cx("image", fit === "contain" && "image-contain", className), style: estilo,
|
|
119
|
+
onError: (e) => { setQuebrou(true); onError?.(e); } } });
|
|
120
|
+
// `role="img"` com `aria-label={alt}`: a caixa de erro continua sendo a imagem para quem usa
|
|
121
|
+
// leitor de tela, com o mesmo texto alternativo. Sem isso o `alt` some junto com o `<img>`.
|
|
122
|
+
if (quebrou)
|
|
123
|
+
return _jsx("span", { className: cx("image", "image-broken", fit === "contain" && "image-contain", className), style: estilo, role: "img", "aria-label": alt, children: _jsx(Icon, { name: "image" }) });
|
|
124
|
+
return elemento;
|
|
125
|
+
}
|
|
126
|
+
export function Gallery({ items, label, selected, onSelect, zoom, ratio = "1/1", className, ...props }) {
|
|
127
|
+
const s = useAureaStrings();
|
|
128
|
+
const [ampliado, setAmpliado] = React.useState(null);
|
|
129
|
+
const interativo = !!onSelect || !!zoom;
|
|
130
|
+
const aberto = items.find(i => i.id === ampliado);
|
|
131
|
+
return _jsxs(_Fragment, { children: [_jsx("ul", { className: cx("gallery", className), "aria-label": label ?? s.galleryLabel, ...props, children: items.map(i => {
|
|
132
|
+
// LEGENDA VISÍVEL TORNA A MINIATURA DECORATIVA, e quem exigiu isso foi o axe, não a
|
|
133
|
+
// teoria: com `alt` e legenda dizendo a mesma coisa, ele reprova `image-redundant-alt` e o
|
|
134
|
+
// leitor de tela anuncia o texto DUAS vezes seguidas. É a regra de figura com legenda do
|
|
135
|
+
// WAI — quando o texto ao lado já diz, a imagem entra com `alt=""`. O `alt` de verdade não
|
|
136
|
+
// se perde: ele continua nomeando a foto AMPLIADA, que é onde não há legenda ao lado.
|
|
137
|
+
const miolo = _jsxs(_Fragment, { children: [_jsx(Image, { src: i.src, alt: i.caption != null ? "" : i.alt, ratio: ratio }), i.caption != null && _jsx("span", { className: "gallery-caption", children: i.caption })] });
|
|
138
|
+
return _jsx("li", { className: "gallery-item", children: interativo
|
|
139
|
+
? _jsx("button", { type: "button", className: cx("gallery-tile", i.id === selected && "is-selected"), "aria-current": i.id === selected ? "true" : undefined, onClick: () => { onSelect?.(i.id); if (zoom)
|
|
140
|
+
setAmpliado(i.id); }, children: miolo })
|
|
141
|
+
: miolo }, i.id);
|
|
142
|
+
}) }), zoom && _jsx(Dialog, { open: !!aberto, title: aberto ? (aberto.caption ?? aberto.alt) : "", onClose: () => setAmpliado(null), children: aberto && _jsx(Image, { src: aberto.src, alt: aberto.alt, fit: "contain" }) })] });
|
|
143
|
+
}
|
|
144
|
+
// ── Carousel (PLANO-1.0, item L1) ────────────────────────────────────────────────────────────
|
|
145
|
+
// MARCAÇÃO, NÃO MOTOR — e a pergunta que o item mandava decidir primeiro ("se `scroll-snap` do
|
|
146
|
+
// CSS cobre, o componente é marcação") foi respondida MEDINDO, não preferindo:
|
|
147
|
+
//
|
|
148
|
+
// • as QUATRO referências embrulham o MESMO motor de terceiro, o `embla-carousel` — medido em
|
|
149
|
+
// 15/08/2026 no `kibo-main/packages/shadcn-ui/.../carousel.tsx`, nas quatro bases do
|
|
150
|
+
// `ui-main` (`aria`, `base`, `radix`, `new-york-v4`), no `carousel-base.tsx` do `react-main`
|
|
151
|
+
// e no `activepieces-main`. Nenhuma escreve um motor; todas pagam o mesmo;
|
|
152
|
+
// • o `@base-ui/react` NÃO tem carrossel (medido: 46 pastas em `packages/react/src`, nenhuma
|
|
153
|
+
// é carousel — a mesma medição que abriu a decisão de motor do Calendar);
|
|
154
|
+
// • trazer o `embla` seria DEPENDÊNCIA NOVA, que pelo `BUILDING.md` §3.3 interrompe o lote e
|
|
155
|
+
// exige o Victor — para um componente que o navegador já sabe fazer;
|
|
156
|
+
// • e o caminho SEM JavaScript nenhum ainda não serve: `::scroll-button()`/`::scroll-marker()`
|
|
157
|
+
// (CSS Overflow 5) não são Baseline — pesquisado em 15/08/2026: Chrome/Edge 135+ têm,
|
|
158
|
+
// o Safari 26.6 estava previsto para o fim deste mês e o Firefox segue "em desenvolvimento".
|
|
159
|
+
// Um design system não pode entregar controle que só funciona num navegador.
|
|
160
|
+
//
|
|
161
|
+
// Então o motor é o CONTÊINER DE ROLAGEM nativo com `scroll-snap`, que é Baseline há anos e dá
|
|
162
|
+
// de graça o que o `embla` reimplementa: arrasto por toque com inércia, rolagem por roda e
|
|
163
|
+
// teclado, e o encaixe no slide. O que sobra de JavaScript é o que o CSS ainda não tem — saber
|
|
164
|
+
// em QUAL slide se está, para desenhar o ponto aceso e desabilitar a seta do fim.
|
|
165
|
+
//
|
|
166
|
+
// A ANIMAÇÃO é do CSS, de propósito: `scrollBy` sem `behavior` resolve para o `scroll-behavior`
|
|
167
|
+
// computado do elemento, então `.carousel-track{scroll-behavior:smooth}` decide — e a regra de
|
|
168
|
+
// `prefers-reduced-motion` que o core já tem (`scroll-behavior:auto!important`) alcança este
|
|
169
|
+
// componente sem uma linha nova. Passar `behavior:"smooth"` daqui passaria POR CIMA dela.
|
|
170
|
+
//
|
|
171
|
+
// UM componente, não sete peças: `Carousel.Root/Content/Item/PrevTrigger/NextTrigger/
|
|
172
|
+
// IndicatorGroup/Indicator` é a decomposição da referência, e aqui ela custaria sete fichas para
|
|
173
|
+
// desenhar uma lista que rola. É o mesmo argumento que o `Stepper`, o `TreeView` e a `Sidebar`
|
|
174
|
+
// já resolveram: quem embrulha cada slide é o componente, e por isso o rótulo "Slide 3 de 8"
|
|
175
|
+
// nunca fica com o consumidor — que é onde ele seria esquecido.
|
|
176
|
+
//
|
|
177
|
+
// LIMITES DECLARADOS (todos são escopo menor que o da referência, `BUILDING.md` §Passo 5):
|
|
178
|
+
// • sem laço infinito, sem autoplay e sem arrasto com o MOUSE — os três são do `embla` e
|
|
179
|
+
// nenhum deles apareceu na medição dos consumidores; laço, ainda por cima, não existe em
|
|
180
|
+
// contêiner de rolagem nativo e voltaria a exigir motor;
|
|
181
|
+
// • sem eixo vertical: não há uso medido, e o sprite não tem chevron para cima (a allowlist
|
|
182
|
+
// do contrato tem `chevron--left/right/down`), então o eixo custaria glifo novo por nada;
|
|
183
|
+
// • quantos slides aparecem por vez é CSS, não prop: `--carousel-slide` (default `100%`) é a
|
|
184
|
+
// válvula no idioma do `--qr-size` e do `--datagrid-max-h`. É o caso `multiple` da
|
|
185
|
+
// referência, sem API nenhuma;
|
|
186
|
+
// • em RTL a rolagem vai para o lado certo (o deslocamento é medido pela borda inicial), mas
|
|
187
|
+
// o GLIFO da seta não espelha — é uma linha de CSS que ninguém pediu e que nenhum teste
|
|
188
|
+
// daqui mediria.
|
|
189
|
+
const desloc = (caixa, item, rtl) => rtl ? item.right - caixa.right : item.left - caixa.left;
|
|
190
|
+
export function Carousel({ children, label, controls = true, indicators = true, className, ...props }) {
|
|
191
|
+
const s = useAureaStrings();
|
|
192
|
+
const trilho = React.useRef(null);
|
|
193
|
+
const slides = React.Children.toArray(children);
|
|
194
|
+
// Nasce com `fim:false` e não com `true`: no HTML estático do catálogo — e no primeiro quadro
|
|
195
|
+
// de qualquer consumidor — ninguém mediu nada ainda, e um carrossel existe porque há mais
|
|
196
|
+
// conteúdo do que cabe. Começar com as duas setas apagadas mostraria um controle morto numa
|
|
197
|
+
// página sem JavaScript; a medição da montagem corrige o caso de um slide só.
|
|
198
|
+
const [pos, setPos] = React.useState({ indice: 0, inicio: true, fim: false });
|
|
199
|
+
const medir = React.useCallback(() => {
|
|
200
|
+
const el = trilho.current;
|
|
201
|
+
if (!el)
|
|
202
|
+
return;
|
|
203
|
+
const rtl = getComputedStyle(el).direction === "rtl";
|
|
204
|
+
const caixa = el.getBoundingClientRect();
|
|
205
|
+
let indice = 0, perto = Infinity;
|
|
206
|
+
[...el.children].forEach((filho, i) => {
|
|
207
|
+
const d = Math.abs(desloc(caixa, filho.getBoundingClientRect(), rtl));
|
|
208
|
+
if (d < perto) {
|
|
209
|
+
perto = d;
|
|
210
|
+
indice = i;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// `Math.abs` no scrollLeft porque em RTL ele é NEGATIVO (de -max a 0) nos navegadores
|
|
214
|
+
// atuais; sem isso, um carrossel em árabe nasceria com as duas setas no estado errado.
|
|
215
|
+
const rolagem = Math.abs(el.scrollLeft), max = el.scrollWidth - el.clientWidth;
|
|
216
|
+
const inicio = rolagem <= 1, fim = rolagem >= max - 1;
|
|
217
|
+
// Devolver o MESMO objeto quando nada mudou faz o React não re-renderizar: `onScroll` dispara
|
|
218
|
+
// dezenas de vezes por arrasto, e sem isto cada uma delas remontaria a fila de pontos.
|
|
219
|
+
setPos(p => p.indice === indice && p.inicio === inicio && p.fim === fim ? p : { indice, inicio, fim });
|
|
220
|
+
}, []);
|
|
221
|
+
// `slides.length` na dependência: trocar a lista muda quem é o último, e a seta do fim
|
|
222
|
+
// continuaria desabilitada sobre um carrossel que voltou a ter para onde ir.
|
|
223
|
+
React.useEffect(() => { medir(); window.addEventListener("resize", medir); return () => window.removeEventListener("resize", medir); }, [medir, slides.length]);
|
|
224
|
+
const irPara = (i) => {
|
|
225
|
+
const el = trilho.current, alvo = el?.children[i];
|
|
226
|
+
if (!el || !alvo)
|
|
227
|
+
return;
|
|
228
|
+
el.scrollBy({ left: desloc(el.getBoundingClientRect(), alvo.getBoundingClientRect(), getComputedStyle(el).direction === "rtl") });
|
|
229
|
+
};
|
|
230
|
+
// A11y pelo padrão APG de carrossel: a região se anuncia com `aria-roledescription="carousel"`
|
|
231
|
+
// e nome próprio, cada slide é um `group` com "Slide N de M". O trilho é `tabIndex={0}` porque
|
|
232
|
+
// conteúdo que rola tem de ser alcançável por teclado — é a regra `scrollable-region-focusable`
|
|
233
|
+
// do axe, e é o que dá as setas de rolagem nativas sem interceptar tecla nenhuma.
|
|
234
|
+
//
|
|
235
|
+
// Os slides fora da vista NÃO ficam `inert`: aqui todos existem no DOM e na árvore de
|
|
236
|
+
// acessibilidade, como em qualquer lista que rola. Escondê-los seria esconder as fotos 2 a 8 de
|
|
237
|
+
// quem usa leitor de tela para ganhar uma ordem de Tab mais curta.
|
|
238
|
+
return _jsxs("div", { className: cx("carousel", className), role: "region", "aria-roledescription": "carousel", "aria-label": label ?? s.carouselLabel, ...props, children: [_jsx("div", { ref: trilho, className: "carousel-track", tabIndex: 0, onScroll: medir, children: slides.map((slide, i) => _jsx("div", { className: "carousel-slide", role: "group", "aria-roledescription": "slide", "aria-label": `${s.carouselSlide} ${i + 1} ${s.positionOf} ${slides.length}`, children: slide }, i)) }), (controls || indicators) && _jsxs("div", { className: "carousel-controls", children: [controls && _jsx(IconButton, { icon: "chevron--left", label: s.carouselPrev, size: "sm", disabled: pos.inicio, onClick: () => irPara(pos.indice - 1) }), indicators && _jsx("div", { className: "carousel-dots", children: slides.map((_, i) => _jsx("button", { type: "button", className: "carousel-dot", "aria-current": i === pos.indice ? "true" : undefined, "aria-label": `${s.carouselSlide} ${i + 1}`, onClick: () => irPara(i) }, i)) }), controls && _jsx(IconButton, { icon: "chevron--right", label: s.carouselNext, size: "sm", disabled: pos.fim, onClick: () => irPara(pos.indice + 1) })] })] });
|
|
239
|
+
}
|
package/dist/media.d.ts
CHANGED
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export declare function spokenTime(sec: number): string;
|
|
4
|
-
export interface MediaPlayerProps extends Omit<React.VideoHTMLAttributes<HTMLVideoElement> & RefAttributes<HTMLVideoElement>, "title"> {
|
|
5
|
-
kind?: "video" | "audio";
|
|
6
|
-
title?: ReactNode;
|
|
7
|
-
subtitle?: ReactNode;
|
|
8
|
-
}
|
|
9
|
-
export declare function MediaPlayer({ kind, src, poster, title, subtitle, className, children, onClick, onPlay, onPause, onEnded, onTimeUpdate, onLoadedMetadata, onDurationChange, onProgress, onVolumeChange, ...rest }: MediaPlayerProps): React.JSX.Element;
|
|
1
|
+
export * from "./media-client.js";
|
|
2
|
+
export { MediaPlayerShell } from "./markup.js";
|
package/dist/media.js
CHANGED
|
@@ -1,99 +1,5 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { cx, useAureaStrings } from "./internal.js";
|
|
8
|
-
import { Icon } from "./system.js";
|
|
9
|
-
export function MediaPlayerShell({ children, className, ...props }) { return _jsx("div", { className: cx("media-player", className), ...props, children: children }); }
|
|
10
|
-
// MediaPlayer (Fase 5): headless sobre <video>/<audio> nativos — o MOTOR de mídia é
|
|
11
|
-
// o browser; o componente só liga estado (play/tempo/buffer/volume/legenda) às
|
|
12
|
-
// classes .media-* já existentes. Sem CSS estrutural novo: o vídeo preenche o
|
|
13
|
-
// .media-viewport por style inline no elemento (não por regra no stylesheet), então
|
|
14
|
-
// o core e os baselines dos docs não mudam.
|
|
15
|
-
// A11y (o APG não fecha player; prática corrente pesquisada 07/2026): os controles
|
|
16
|
-
// são <button> nativos com aria-label que troca de estado (Reproduzir/Pausar,
|
|
17
|
-
// Silenciar/Ativar som) e a barra é <input type="range"> nativo — um slider de
|
|
18
|
-
// verdade, com setas/Home/End vindos do browser — cujo aria-valuetext lê o tempo por
|
|
19
|
-
// extenso (o número cru "243" não se entende; "4 minutes and 3 seconds" sim). Nada de
|
|
20
|
-
// role="slider" à mão. SEM atalho global de teclado: cada controle é focável e opera
|
|
21
|
-
// pelo próprio elemento nativo, então play/pause/seek/volume por teclado saem sem
|
|
22
|
-
// interceptar — interceptar quebraria digitação e não é padrão APG. O tempo por
|
|
23
|
-
// extenso é helper embutido em inglês (não i18n — como o formatSize do FileInput);
|
|
24
|
-
// os rótulos de botão passam pela i18n como todo controle.
|
|
25
|
-
function clockTime(sec) { if (!Number.isFinite(sec) || sec < 0)
|
|
26
|
-
sec = 0; const m = Math.floor(sec / 60), s = Math.floor(sec % 60); return `${m}:${String(s).padStart(2, "0")}`; }
|
|
27
|
-
export function spokenTime(sec) { if (!Number.isFinite(sec) || sec < 0)
|
|
28
|
-
sec = 0; const m = Math.floor(sec / 60), s = Math.floor(sec % 60); const mp = m === 1 ? "minute" : "minutes", sp = s === 1 ? "second" : "seconds"; return m && s ? `${m} ${mp} and ${s} ${sp}` : m ? `${m} ${mp}` : `${s} ${sp}`; }
|
|
29
|
-
// Os handlers de mídia do consumidor são COMPOSTOS com os internos (não podem
|
|
30
|
-
// sobrescrevê-los: um onPlay externo silenciaria o estado playing e o botão
|
|
31
|
-
// ficaria em "Reproduzir" — auditoria 18/07/2026, MÉDIO 2).
|
|
32
|
-
export function MediaPlayer({ kind = "video", src, poster, title, subtitle, className, children, onClick, onPlay, onPause, onEnded, onTimeUpdate, onLoadedMetadata, onDurationChange, onProgress, onVolumeChange, ...rest }) {
|
|
33
|
-
const s = useAureaStrings();
|
|
34
|
-
const boxRef = React.useRef(null);
|
|
35
|
-
const mediaRef = React.useRef(null);
|
|
36
|
-
const [playing, setPlaying] = React.useState(false);
|
|
37
|
-
const [current, setCurrent] = React.useState(0);
|
|
38
|
-
const [duration, setDuration] = React.useState(0);
|
|
39
|
-
const [buffered, setBuffered] = React.useState(0);
|
|
40
|
-
const [volume, setVolume] = React.useState(1);
|
|
41
|
-
const [muted, setMuted] = React.useState(false);
|
|
42
|
-
const [hasCaptions, setHasCaptions] = React.useState(false);
|
|
43
|
-
const [captionsOn, setCaptionsOn] = React.useState(false);
|
|
44
|
-
const [fullscreen, setFullscreen] = React.useState(false);
|
|
45
|
-
// fullscreenchange vem do document (não do elemento); é o único estado que exige
|
|
46
|
-
// listener — o resto sincroniza pelos eventos de mídia do próprio <video>.
|
|
47
|
-
React.useEffect(() => { const on = () => setFullscreen(document.fullscreenElement === boxRef.current); document.addEventListener("fullscreenchange", on); return () => document.removeEventListener("fullscreenchange", on); }, []);
|
|
48
|
-
const m = () => mediaRef.current;
|
|
49
|
-
// ramifica pelo estado sincronizado (playing), não por el.paused: playing vem dos
|
|
50
|
-
// eventos play/pause e não fica atrás do elemento (nem preso, como no jsdom).
|
|
51
|
-
const togglePlay = () => { const el = m(); if (!el)
|
|
52
|
-
return; if (playing)
|
|
53
|
-
el.pause();
|
|
54
|
-
else {
|
|
55
|
-
const p = el.play();
|
|
56
|
-
if (p)
|
|
57
|
-
p.catch(() => { });
|
|
58
|
-
} };
|
|
59
|
-
const skip = (d) => { const el = m(); if (el)
|
|
60
|
-
el.currentTime = Math.max(0, Math.min(el.currentTime + d, el.duration || Infinity)); };
|
|
61
|
-
// seek/setVol/mute atualizam o estado na hora (input controlado responsivo) e o
|
|
62
|
-
// elemento; os eventos de mídia reconfirmam depois (idempotente) e cobrem mudanças
|
|
63
|
-
// externas (controles nativos, outra aba).
|
|
64
|
-
const seek = (v) => { const el = m(); if (el)
|
|
65
|
-
el.currentTime = v; setCurrent(v); };
|
|
66
|
-
const setVol = (v) => { const el = m(); if (el) {
|
|
67
|
-
el.volume = v;
|
|
68
|
-
if (v > 0)
|
|
69
|
-
el.muted = false;
|
|
70
|
-
} setVolume(v); if (v > 0)
|
|
71
|
-
setMuted(false); };
|
|
72
|
-
const toggleMute = () => { const el = m(); const next = el ? !el.muted : !muted; if (el)
|
|
73
|
-
el.muted = next; setMuted(next); };
|
|
74
|
-
const toggleCaptions = () => { const el = m(); if (!el || !el.textTracks.length)
|
|
75
|
-
return; const show = el.textTracks[0].mode !== "showing"; el.textTracks[0].mode = show ? "showing" : "hidden"; setCaptionsOn(show); };
|
|
76
|
-
const toggleFullscreen = () => { if (document.fullscreenElement)
|
|
77
|
-
document.exitFullscreen?.();
|
|
78
|
-
else
|
|
79
|
-
boxRef.current?.requestFullscreen?.(); };
|
|
80
|
-
const syncVol = () => { const el = m(); if (el) {
|
|
81
|
-
setVolume(el.volume);
|
|
82
|
-
setMuted(el.muted);
|
|
83
|
-
} };
|
|
84
|
-
const syncBuf = () => { const el = m(); if (el && el.buffered.length)
|
|
85
|
-
setBuffered(el.buffered.end(el.buffered.length - 1)); };
|
|
86
|
-
const onMeta = () => { const el = m(); if (el) {
|
|
87
|
-
setDuration(el.duration);
|
|
88
|
-
setHasCaptions(el.textTracks.length > 0);
|
|
89
|
-
syncVol();
|
|
90
|
-
} };
|
|
91
|
-
const playedPct = duration > 0 ? Math.min(100, (current / duration) * 100) : 0;
|
|
92
|
-
const bufferedPct = duration > 0 ? Math.min(100, (buffered / duration) * 100) : 0;
|
|
93
|
-
const volPct = muted ? 0 : Math.round(volume * 100);
|
|
94
|
-
const valueText = duration > 0 ? `${spokenTime(current)} of ${spokenTime(duration)}` : spokenTime(current);
|
|
95
|
-
const MediaTag = kind === "audio" ? "audio" : "video";
|
|
96
|
-
return _jsxs("div", { ref: boxRef, className: cx("media-player", className), role: "group", "aria-label": typeof title === "string" ? title : s.mediaPlayer, children: [_jsxs("div", { className: "media-viewport", children: [kind === "audio" && _jsxs("div", { className: "media-placeholder", children: [_jsx(Icon, { name: "volume--up" }), title && _jsx("strong", { children: title }), subtitle && _jsx("span", { children: subtitle })] }), _jsx(MediaTag, { ref: mediaRef, src: src, ...rest, ...(kind === "video" ? { poster, playsInline: true, className: "media-fill", onClick: (e) => { onClick?.(e); togglePlay(); } } : {}), onPlay: (e) => { setPlaying(true); onPlay?.(e); }, onPause: (e) => { setPlaying(false); onPause?.(e); }, onEnded: (e) => { setPlaying(false); onEnded?.(e); }, onTimeUpdate: (e) => { const el = m(); if (el)
|
|
97
|
-
setCurrent(el.currentTime); onTimeUpdate?.(e); }, onLoadedMetadata: (e) => { onMeta(); onLoadedMetadata?.(e); }, onDurationChange: (e) => { const el = m(); if (el)
|
|
98
|
-
setDuration(el.duration); onDurationChange?.(e); }, onProgress: (e) => { syncBuf(); onProgress?.(e); }, onVolumeChange: (e) => { syncVol(); onVolumeChange?.(e); }, children: children })] }), kind === "video" && title && _jsx("div", { className: "media-overlay-title", children: _jsxs("div", { children: [_jsx("strong", { children: title }), subtitle && _jsx("span", { children: subtitle })] }) }), _jsxs("div", { className: "media-controls", children: [_jsxs("div", { className: "media-seek", style: { "--media-played": `${playedPct}%` }, children: [_jsxs("div", { className: "media-seek-track", children: [_jsx("span", { className: "media-seek-buffered", style: { width: `${bufferedPct}%` } }), _jsx("span", { className: "media-seek-played" })] }), _jsx("input", { type: "range", min: 0, max: duration > 0 ? duration : 0, step: "any", value: Math.min(current, duration || 0), "aria-label": s.mediaSeek, "aria-valuetext": valueText, onChange: e => seek(Number(e.target.value)) })] }), _jsxs("div", { className: "media-control-row", children: [_jsxs("div", { className: "media-control-group", children: [_jsx("button", { className: "media-control", type: "button", "aria-label": playing ? s.mediaPause : s.mediaPlay, onClick: togglePlay, children: _jsx(Icon, { name: playing ? "pause" : "play" }) }), _jsx("button", { className: "media-control", type: "button", "aria-label": s.mediaSkipBack, onClick: () => skip(-10), children: _jsx(Icon, { name: "rewind--10" }) }), _jsx("button", { className: "media-control", type: "button", "aria-label": s.mediaSkipForward, onClick: () => skip(10), children: _jsx(Icon, { name: "forward--10" }) }), _jsxs("span", { className: "media-time", children: [clockTime(current), " / ", clockTime(duration)] })] }), _jsxs("div", { className: "media-control-group", children: [_jsx("button", { className: "media-control", type: "button", "aria-label": muted ? s.mediaUnmute : s.mediaMute, onClick: toggleMute, children: _jsx(Icon, { name: muted || volume === 0 ? "volume--mute" : "volume--up" }) }), _jsx("input", { className: "media-volume", type: "range", min: 0, max: 100, value: volPct, "aria-label": s.mediaVolume, "aria-valuetext": `${volPct}%`, onChange: e => setVol(Number(e.target.value) / 100) }), hasCaptions && _jsx("button", { className: cx("media-control", captionsOn && "active"), type: "button", "aria-label": captionsOn ? s.mediaCaptionsHide : s.mediaCaptionsShow, "aria-pressed": captionsOn, onClick: toggleCaptions, children: _jsx(Icon, { name: "closed-caption" }) }), kind === "video" && _jsx("button", { className: "media-control", type: "button", "aria-label": fullscreen ? s.mediaFullscreenExit : s.mediaFullscreenEnter, onClick: toggleFullscreen, children: _jsx(Icon, { name: fullscreen ? "minimize" : "maximize" }) })] })] })] })] });
|
|
99
|
-
}
|
|
1
|
+
// VITRINE da categoria — é este arquivo que `@aurea-uds/react/media` resolve. Sem `"use client"`
|
|
2
|
+
// de propósito: a diretiva contamina o módulo inteiro e faria a marcação pura chegar como cliente
|
|
3
|
+
// por vizinhança (ADR-0026; o check 26b reprova quem a puser de volta aqui).
|
|
4
|
+
export * from "./media-client.js";
|
|
5
|
+
export { MediaPlayerShell } from "./markup.js";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, { type HTMLAttributes, type RefAttributes, type ReactNode, type ReactElement } from "react";
|
|
2
|
+
import { type IconName } from "./system.js";
|
|
3
|
+
export type StepState = "default" | "active" | "done" | "error";
|
|
4
|
+
export interface StepItem {
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
state?: StepState;
|
|
7
|
+
optional?: ReactNode;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function Stepper({ items, label, className }: {
|
|
11
|
+
items: StepItem[];
|
|
12
|
+
label?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
}): React.JSX.Element;
|
|
15
|
+
export declare function Breadcrumb({ items, label }: {
|
|
16
|
+
items: Array<{
|
|
17
|
+
label: ReactNode;
|
|
18
|
+
href?: string;
|
|
19
|
+
}>;
|
|
20
|
+
label?: string;
|
|
21
|
+
}): React.JSX.Element;
|
|
22
|
+
export declare function Tabs({ tabs, value, onChange, label }: {
|
|
23
|
+
tabs: Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
label: ReactNode;
|
|
26
|
+
content: ReactNode;
|
|
27
|
+
}>;
|
|
28
|
+
value: string;
|
|
29
|
+
onChange: (id: string) => void;
|
|
30
|
+
label?: string;
|
|
31
|
+
}): React.JSX.Element;
|
|
32
|
+
export declare function Pagination({ page, total, onPageChange }: {
|
|
33
|
+
page: number;
|
|
34
|
+
total: number;
|
|
35
|
+
onPageChange: (p: number) => void;
|
|
36
|
+
}): React.JSX.Element;
|
|
37
|
+
export interface TocItem {
|
|
38
|
+
id: string;
|
|
39
|
+
label: string;
|
|
40
|
+
sub?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function TableOfContents({ items, current, label, className, ...props }: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement> & {
|
|
43
|
+
items: TocItem[];
|
|
44
|
+
current?: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
}): React.JSX.Element;
|
|
47
|
+
export interface TreeNode {
|
|
48
|
+
id: string;
|
|
49
|
+
label: ReactNode;
|
|
50
|
+
icon?: IconName;
|
|
51
|
+
children?: TreeNode[];
|
|
52
|
+
}
|
|
53
|
+
export declare function TreeView({ items, defaultExpandedIds, onSelect, label, className }: {
|
|
54
|
+
items: TreeNode[];
|
|
55
|
+
defaultExpandedIds?: string[];
|
|
56
|
+
onSelect?: (node: TreeNode) => void;
|
|
57
|
+
label?: string;
|
|
58
|
+
className?: string;
|
|
59
|
+
}): ReactElement<unknown, string | React.JSXElementConstructor<any>>;
|
|
60
|
+
export interface SidebarItem {
|
|
61
|
+
id: string;
|
|
62
|
+
label: ReactNode;
|
|
63
|
+
href?: string;
|
|
64
|
+
icon?: IconName;
|
|
65
|
+
badge?: ReactNode;
|
|
66
|
+
onClick?: () => void;
|
|
67
|
+
items?: SidebarItem[];
|
|
68
|
+
}
|
|
69
|
+
export declare function Sidebar({ items, current, collapsed, label, children, className, ...props }: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement> & {
|
|
70
|
+
items?: SidebarItem[];
|
|
71
|
+
current?: string;
|
|
72
|
+
collapsed?: boolean;
|
|
73
|
+
label?: string;
|
|
74
|
+
}): React.JSX.Element;
|
|
75
|
+
export interface CommandItem {
|
|
76
|
+
id: string;
|
|
77
|
+
label: string;
|
|
78
|
+
icon?: IconName;
|
|
79
|
+
kbd?: string;
|
|
80
|
+
run: () => void;
|
|
81
|
+
}
|
|
82
|
+
export declare function CommandPalette({ open, onClose, items, placeholder, label }: {
|
|
83
|
+
open: boolean;
|
|
84
|
+
onClose: () => void;
|
|
85
|
+
items: CommandItem[];
|
|
86
|
+
placeholder?: string;
|
|
87
|
+
label?: string;
|
|
88
|
+
}): React.JSX.Element | null;
|
|
89
|
+
export declare function CommandPaletteShell({ open, query, onQueryChange, children }: {
|
|
90
|
+
open: boolean;
|
|
91
|
+
query: string;
|
|
92
|
+
onQueryChange: (v: string) => void;
|
|
93
|
+
children?: ReactNode;
|
|
94
|
+
}): React.JSX.Element | null;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// Fase 9 (achado A5): este arquivo saiu do index.tsx de 970 linhas. Um módulo por categoria
|
|
4
|
+
// do registry — a taxonomia já existia e é gateada. A ordem de import entre eles é um DAG:
|
|
5
|
+
// internal → system → actions → feedback → inputs → navigation → layout → data-display → resto.
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Tabs as BaseTabs } from "@base-ui/react/tabs";
|
|
8
|
+
import { Autocomplete as BaseAutocomplete } from "@base-ui/react/autocomplete";
|
|
9
|
+
import { cx, useAureaStrings, usePortalContainer } from "./internal.js";
|
|
10
|
+
import { Kbd } from "./markup.js";
|
|
11
|
+
import { Icon } from "./system.js";
|
|
12
|
+
import { Button } from "./actions.js";
|
|
13
|
+
import { Badge } from "./feedback.js";
|
|
14
|
+
import { SearchField } from "./inputs.js";
|
|
15
|
+
export function Stepper({ items, label, className }) {
|
|
16
|
+
const s = useAureaStrings();
|
|
17
|
+
return _jsx("div", { role: "list", "aria-label": label ?? s.stepperLabel, className: cx("stepper", className), children: items.map((it, n) => {
|
|
18
|
+
const st = it.state ?? "default";
|
|
19
|
+
const marca = st === "done" ? _jsx(Icon, { name: "checkmark" }) : st === "error" ? _jsx(Icon, { name: "error" }) : n + 1;
|
|
20
|
+
const miolo = _jsxs(_Fragment, { children: [_jsx("span", { className: "step-dot", children: marca }), _jsx("strong", { children: it.label }), it.optional && _jsx("small", { className: "step-optional", children: it.optional })] });
|
|
21
|
+
return _jsx("div", { role: "listitem", className: cx("step", st !== "default" && `step-${st}`), "aria-current": st === "active" ? "step" : undefined, children: it.onClick ? _jsx("button", { type: "button", className: "step-trigger", onClick: it.onClick, children: miolo }) : miolo }, n);
|
|
22
|
+
}) });
|
|
23
|
+
}
|
|
24
|
+
export function Breadcrumb({ items, label }) { const s = useAureaStrings(); return _jsx("nav", { className: "breadcrumb", "aria-label": label ?? s.breadcrumbLabel, children: items.map((i, n) => _jsxs(React.Fragment, { children: [n > 0 && _jsx(Icon, { name: "chevron--right", size: "sm" }), " ", i.href ? _jsx("a", { href: i.href, children: i.label }) : _jsx("strong", { "aria-current": "page", children: i.label })] }, n)) }); }
|
|
25
|
+
export function Tabs({ tabs, value, onChange, label }) { const s = useAureaStrings(); return _jsxs(BaseTabs.Root, { value: value, onValueChange: v => onChange(String(v)), children: [_jsx(BaseTabs.List, { className: "tabs", "aria-label": label ?? s.tabsLabel, activateOnFocus: true, children: tabs.map(t => _jsx(BaseTabs.Tab, { value: t.id, className: "tab", children: t.label }, t.id)) }), tabs.map(t => _jsx(BaseTabs.Panel, { value: t.id, className: "card card-inset", tabIndex: 0, children: t.content }, t.id))] }); }
|
|
26
|
+
export function Pagination({ page, total, onPageChange }) { const s = useAureaStrings(); return _jsxs("nav", { className: "pagination", "aria-label": s.paginationLabel, children: [_jsx(Button, { variant: "ghost", size: "sm", disabled: page <= 1, onClick: () => onPageChange(page - 1), children: s.previous }), _jsxs(Badge, { variant: "primary", children: [page, " / ", total] }), _jsx(Button, { variant: "ghost", size: "sm", disabled: page >= total, onClick: () => onPageChange(page + 1), children: s.next })] }); }
|
|
27
|
+
export function TableOfContents({ items, current, label, className, ...props }) {
|
|
28
|
+
const s = useAureaStrings();
|
|
29
|
+
return _jsxs("nav", { className: cx("toc", className), "aria-label": label ?? s.tocLabel, ...props, children: [_jsx("p", { className: "toc-label", children: label ?? s.tocLabel }), items.map(i => _jsx("a", { href: `#${i.id}`, className: cx(i.sub && "toc-sub"), ...(i.id === current ? { "aria-current": "true" } : {}), children: i.label }, i.id))] });
|
|
30
|
+
}
|
|
31
|
+
function flattenVisible(nodes, expanded, level = 1, parentId, acc = []) {
|
|
32
|
+
for (const node of nodes) {
|
|
33
|
+
acc.push({ node, level, parentId });
|
|
34
|
+
if (node.children?.length && expanded.has(node.id))
|
|
35
|
+
flattenVisible(node.children, expanded, level + 1, node.id, acc);
|
|
36
|
+
}
|
|
37
|
+
return acc;
|
|
38
|
+
}
|
|
39
|
+
export function TreeView({ items, defaultExpandedIds, onSelect, label, className }) {
|
|
40
|
+
const s = useAureaStrings();
|
|
41
|
+
const baseId = React.useId();
|
|
42
|
+
const [expanded, setExpanded] = React.useState(() => new Set(defaultExpandedIds));
|
|
43
|
+
const [selected, setSelected] = React.useState();
|
|
44
|
+
const [active, setActive] = React.useState(() => items[0]?.id);
|
|
45
|
+
const rootRef = React.useRef(null);
|
|
46
|
+
const visible = flattenVisible(items, expanded);
|
|
47
|
+
// Roving tab stop derivado: se o nó ativo saiu do conjunto visível (dados
|
|
48
|
+
// trocados, nó removido), o primeiro visível volta a ser tabulável — senão a
|
|
49
|
+
// árvore inteira fica tabIndex=-1 e some da ordem do Tab (auditoria, MÉDIO 1).
|
|
50
|
+
const effectiveActive = active !== undefined && visible.some(v => v.node.id === active) ? active : visible[0]?.node.id;
|
|
51
|
+
const focusId = (id) => { setActive(id); rootRef.current?.querySelector(`[data-tree-id="${CSS.escape(id)}"]`)?.focus(); };
|
|
52
|
+
const toggle = (id, open) => setExpanded(prev => { const n = new Set(prev); if (open)
|
|
53
|
+
n.add(id);
|
|
54
|
+
else
|
|
55
|
+
n.delete(id); return n; });
|
|
56
|
+
const select = (node) => { setSelected(node.id); onSelect?.(node); };
|
|
57
|
+
const onKeyDown = (e) => {
|
|
58
|
+
const idx = visible.findIndex(v => v.node.id === effectiveActive);
|
|
59
|
+
if (idx < 0)
|
|
60
|
+
return;
|
|
61
|
+
const cur = visible[idx], hasChildren = !!cur.node.children?.length, isOpen = expanded.has(cur.node.id);
|
|
62
|
+
const rtl = getComputedStyle(e.currentTarget).direction === "rtl";
|
|
63
|
+
const expandKey = rtl ? "ArrowLeft" : "ArrowRight", collapseKey = rtl ? "ArrowRight" : "ArrowLeft";
|
|
64
|
+
switch (e.key) {
|
|
65
|
+
case "ArrowDown":
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
if (idx < visible.length - 1)
|
|
68
|
+
focusId(visible[idx + 1].node.id);
|
|
69
|
+
break;
|
|
70
|
+
case "ArrowUp":
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
if (idx > 0)
|
|
73
|
+
focusId(visible[idx - 1].node.id);
|
|
74
|
+
break;
|
|
75
|
+
case expandKey:
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
if (hasChildren && !isOpen)
|
|
78
|
+
toggle(cur.node.id, true);
|
|
79
|
+
else if (hasChildren && isOpen)
|
|
80
|
+
focusId(cur.node.children[0].id);
|
|
81
|
+
break;
|
|
82
|
+
case collapseKey:
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
if (hasChildren && isOpen)
|
|
85
|
+
toggle(cur.node.id, false);
|
|
86
|
+
else if (cur.parentId)
|
|
87
|
+
focusId(cur.parentId);
|
|
88
|
+
break;
|
|
89
|
+
case "Home":
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
focusId(visible[0].node.id);
|
|
92
|
+
break;
|
|
93
|
+
case "End":
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
focusId(visible[visible.length - 1].node.id);
|
|
96
|
+
break;
|
|
97
|
+
case "Enter":
|
|
98
|
+
case " ":
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
select(cur.node);
|
|
101
|
+
if (hasChildren)
|
|
102
|
+
toggle(cur.node.id, !isOpen);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const renderNodes = (nodes, level) => (_jsx("ul", { ref: level === 1 ? rootRef : undefined, className: cx(level === 1 ? "tree" : "tree-group", level === 1 && className), role: level === 1 ? "tree" : "group", "aria-label": level === 1 ? (label ?? s.treeLabel) : undefined, onKeyDown: level === 1 ? onKeyDown : undefined, children: nodes.map(node => {
|
|
107
|
+
const hasChildren = !!node.children?.length, isOpen = expanded.has(node.id), isSelected = selected === node.id, labelId = baseId + node.id;
|
|
108
|
+
return _jsxs("li", { className: "tree-item", role: "treeitem", "data-tree-id": node.id, "aria-level": level, "aria-expanded": hasChildren ? isOpen : undefined, "aria-selected": isSelected, "aria-labelledby": labelId, tabIndex: node.id === effectiveActive ? 0 : -1, children: [_jsxs("span", { className: "tree-node", "data-selected": isSelected || undefined, style: { paddingInlineStart: `calc(var(--space-3) + ${level - 1} * var(--space-4))` }, onClick: () => { focusId(node.id); select(node); if (hasChildren)
|
|
109
|
+
toggle(node.id, !isOpen); }, children: [hasChildren ? _jsx(Icon, { name: "chevron--right", size: "sm", className: "tree-twist" }) : _jsx("span", { className: "tree-indent", "aria-hidden": "true" }), node.icon && _jsx(Icon, { name: node.icon, size: "sm" }), _jsx("span", { id: labelId, className: "tree-label", children: node.label })] }), hasChildren && isOpen && renderNodes(node.children, level + 1)] }, node.id);
|
|
110
|
+
}) }));
|
|
111
|
+
return renderNodes(items, 1);
|
|
112
|
+
}
|
|
113
|
+
function sidebarList(items, ctx, sub, labelledBy) {
|
|
114
|
+
return _jsx("ul", { className: cx("sidebar-list", sub && "sidebar-sub"), "aria-labelledby": labelledBy, children: items.map(it => {
|
|
115
|
+
const lid = ctx.baseId + it.id;
|
|
116
|
+
const filhos = it.items?.length ? it.items : undefined;
|
|
117
|
+
// Rótulo escondido vira `.sr-only` em vez de sumir do DOM: na lateral recolhida o item
|
|
118
|
+
// continua tendo nome para quem usa leitor de tela. Ícone sozinho não nomeia nada.
|
|
119
|
+
const oculto = (no) => ctx.collapsed ? _jsx("span", { className: "sr-only", children: no }) : no;
|
|
120
|
+
if (filhos && !it.href && !it.onClick)
|
|
121
|
+
return _jsxs("li", { children: [_jsx("p", { id: lid, className: cx("sidebar-group-label", ctx.collapsed && "sr-only"), children: it.label }), sidebarList(filhos, ctx, false, lid)] }, it.id);
|
|
122
|
+
const ativo = it.id === ctx.current;
|
|
123
|
+
const miolo = _jsxs(_Fragment, { children: [it.icon && _jsx(Icon, { name: it.icon, size: "sm" }), _jsx("span", { className: cx("sidebar-label", ctx.collapsed && "sr-only"), children: it.label }), it.badge != null && oculto(it.badge)] });
|
|
124
|
+
return _jsxs("li", { children: [it.href
|
|
125
|
+
? _jsx("a", { id: lid, href: it.href, className: "sidebar-item", "aria-current": ativo ? "page" : undefined, onClick: it.onClick, children: miolo })
|
|
126
|
+
: _jsx("button", { id: lid, type: "button", className: "sidebar-item", "aria-current": ativo ? "page" : undefined, onClick: it.onClick, children: miolo }), filhos && sidebarList(filhos, ctx, true, lid)] }, it.id);
|
|
127
|
+
}) });
|
|
128
|
+
}
|
|
129
|
+
export function Sidebar({ items, current, collapsed, label, children, className, ...props }) {
|
|
130
|
+
const s = useAureaStrings();
|
|
131
|
+
const baseId = React.useId();
|
|
132
|
+
return _jsxs("aside", { className: cx("sidebar", collapsed && "sidebar-collapsed", className), ...props, children: [items && items.length > 0 && _jsx("nav", { className: "sidebar-nav", "aria-label": label ?? s.sidebarLabel, children: sidebarList(items, { baseId, current, collapsed }) }), children] });
|
|
133
|
+
}
|
|
134
|
+
export function CommandPalette({ open, onClose, items, placeholder, label }) {
|
|
135
|
+
const s = useAureaStrings();
|
|
136
|
+
const portal = usePortalContainer();
|
|
137
|
+
if (!open)
|
|
138
|
+
return null;
|
|
139
|
+
// SEM AGRUPAMENTO na v1, e a medição é que decidiu (13/08/2026). O `Autocomplete.Root` não
|
|
140
|
+
// consome a estrutura agrupada como o `Combobox.Root` consome — e o caminho alternativo, dar a
|
|
141
|
+
// cada `Group` a sua fatia de itens, PASSA POR CIMA do filtro do motor: digitar "the" devolvia
|
|
142
|
+
// os três comandos. Entre agrupar e filtrar, filtrar é o ponto de uma paleta de comandos.
|
|
143
|
+
// Fica registrado como limite, não como esquecimento: quando alguém precisar de grupo aqui, o
|
|
144
|
+
// caminho é o `Combobox.Root`, e isso é troca de motor, não ajuste.
|
|
145
|
+
const executa = (item) => { onClose(); item.run(); };
|
|
146
|
+
const linha = (item) => _jsxs(BaseAutocomplete.Item, { value: item, className: "menu-item command-item", onClick: () => executa(item), children: [item.icon && _jsx(Icon, { name: item.icon }), _jsx("span", { className: "command-item-label", children: item.label }), item.kbd && _jsx(Kbd, { children: item.kbd })] }, item.id);
|
|
147
|
+
// Escape fecha, e é o teclado que a pessoa tenta primeiro. O motor não fecha sozinho porque
|
|
148
|
+
// quem é dono do `open` é o consumidor — mesma regra do Dialog e do Drawer daqui.
|
|
149
|
+
return _jsx("div", { className: "command-overlay", onKeyDown: e => { if (e.key === "Escape")
|
|
150
|
+
onClose(); }, children: _jsx("div", { className: "command-palette", role: "dialog", "aria-label": label ?? s.commandLabel, children: _jsxs(BaseAutocomplete.Root, { items: items, itemToStringValue: (i) => i.label, mode: "list", open: true, children: [_jsx(BaseAutocomplete.Input, { autoFocus: true, className: "input", onKeyDown: e => { if (e.key === "Escape")
|
|
151
|
+
onClose(); }, "aria-label": placeholder ?? s.commandPlaceholder, placeholder: placeholder ?? s.commandPlaceholder }), _jsx(BaseAutocomplete.Portal, { container: portal, children: _jsx(BaseAutocomplete.Positioner, { sideOffset: 6, className: "command-positioner", children: _jsxs(BaseAutocomplete.Popup, { className: "menu command-list", children: [_jsx(BaseAutocomplete.Empty, { className: "combobox-empty", children: s.comboboxEmpty }), _jsx(BaseAutocomplete.List, { children: _jsx(BaseAutocomplete.Collection, { children: linha }) })] }) }) })] }) }) });
|
|
152
|
+
}
|
|
153
|
+
export function CommandPaletteShell({ open, query, onQueryChange, children }) { const s = useAureaStrings(); if (!open)
|
|
154
|
+
return null; return _jsx("div", { className: "command-overlay", children: _jsxs("div", { className: "command-palette", role: "dialog", "aria-label": s.commandLabel, children: [_jsx(SearchField, { autoFocus: true, "aria-label": s.commandPlaceholder, value: query, onChange: e => onQueryChange(e.target.value), placeholder: s.commandPlaceholder }), children] }) }); }
|