@aurea-uds/react 0.1.0 → 0.2.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/dist/actions.d.ts +13 -0
- package/dist/actions.js +5 -0
- package/dist/agents.d.ts +209 -0
- package/dist/agents.js +302 -0
- package/dist/calendar.d.ts +6 -0
- package/dist/calendar.js +27 -0
- package/dist/chart.d.ts +10 -0
- package/dist/chart.js +53 -0
- package/dist/code-editor.js +1 -0
- package/dist/code.js +18 -2
- package/dist/communication.js +1 -0
- package/dist/data-display.js +1 -0
- package/dist/data-grid.d.ts +52 -3
- package/dist/data-grid.js +205 -25
- package/dist/feedback.d.ts +14 -4
- package/dist/feedback.js +37 -7
- package/dist/file-input.d.ts +21 -2
- package/dist/file-input.js +154 -21
- package/dist/graph.d.ts +33 -0
- package/dist/graph.js +178 -0
- package/dist/identity.d.ts +12 -3
- package/dist/identity.js +24 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +15 -1
- package/dist/inputs.d.ts +26 -0
- package/dist/inputs.js +69 -4
- package/dist/internal.d.ts +4 -66
- package/dist/internal.js +25 -14
- package/dist/layout.d.ts +2 -1
- package/dist/layout.js +61 -7
- package/dist/media.js +1 -0
- package/dist/navigation.d.ts +27 -1
- package/dist/navigation.js +32 -7
- package/dist/overlays.d.ts +5 -0
- package/dist/overlays.js +21 -9
- package/dist/pure.d.ts +166 -0
- package/dist/pure.js +87 -0
- package/dist/qrcode.d.ts +2 -1
- package/dist/qrcode.js +3 -2
- package/dist/system.d.ts +2 -1
- package/dist/system.js +13 -4
- package/package.json +176 -144
package/dist/file-input.d.ts
CHANGED
|
@@ -6,13 +6,27 @@ export interface FileRejection {
|
|
|
6
6
|
export interface UploadContext {
|
|
7
7
|
signal: AbortSignal;
|
|
8
8
|
onProgress: (fraction: number) => void;
|
|
9
|
+
resumeFrom: number;
|
|
10
|
+
}
|
|
11
|
+
export type UploadFn = (file: File, ctx: UploadContext) => Promise<void | {
|
|
12
|
+
checksum?: string;
|
|
13
|
+
}>;
|
|
14
|
+
type UploadStatus = "pending" | "uploading" | "paused" | "done" | "error" | "canceled";
|
|
15
|
+
export interface FileQueueItem {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
bytes: number;
|
|
19
|
+
status: UploadStatus;
|
|
20
|
+
progress: number;
|
|
21
|
+
restored?: boolean;
|
|
22
|
+
checksum?: string;
|
|
23
|
+
finishedAt?: string;
|
|
9
24
|
}
|
|
10
|
-
export type UploadFn = (file: File, ctx: UploadContext) => Promise<void>;
|
|
11
25
|
export declare function matchesAccept(file: {
|
|
12
26
|
name: string;
|
|
13
27
|
type: string;
|
|
14
28
|
}, accept?: string): boolean;
|
|
15
|
-
export declare function FileInput({ accept, maxSize, multiple, onFilesChange, upload, label, hint, id, className }: {
|
|
29
|
+
export declare function FileInput({ accept, maxSize, multiple, onFilesChange, upload, label, hint, id, className, initialQueue, onQueueChange, preview, checksum }: {
|
|
16
30
|
accept?: string;
|
|
17
31
|
maxSize?: number;
|
|
18
32
|
multiple?: boolean;
|
|
@@ -22,4 +36,9 @@ export declare function FileInput({ accept, maxSize, multiple, onFilesChange, up
|
|
|
22
36
|
hint?: ReactNode;
|
|
23
37
|
id?: string;
|
|
24
38
|
className?: string;
|
|
39
|
+
initialQueue?: FileQueueItem[];
|
|
40
|
+
onQueueChange?: (queue: FileQueueItem[]) => void;
|
|
41
|
+
preview?: boolean;
|
|
42
|
+
checksum?: boolean;
|
|
25
43
|
}): React.JSX.Element;
|
|
44
|
+
export {};
|
package/dist/file-input.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
// Fase 9 (achado A5): este arquivo saiu do index.tsx de 970 linhas. Um módulo por categoria
|
|
3
4
|
// do registry — a taxonomia já existia e é gateada. A ordem de import entre eles é um DAG:
|
|
@@ -5,7 +6,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
5
6
|
import React from "react";
|
|
6
7
|
import { cx, useAureaStrings } from "./internal.js";
|
|
7
8
|
import { Icon } from "./system.js";
|
|
8
|
-
import { IconButton } from "./actions.js";
|
|
9
|
+
import { Button, IconButton } from "./actions.js";
|
|
9
10
|
// matchesAccept espelha o algoritmo do atributo accept do HTML: extensão (.json),
|
|
10
11
|
// grupo de tipo (image/*) ou MIME exato (application/json). accept vazio = tudo.
|
|
11
12
|
export function matchesAccept(file, accept) {
|
|
@@ -15,6 +16,32 @@ export function matchesAccept(file, accept) {
|
|
|
15
16
|
const name = file.name.toLowerCase(), type = file.type.toLowerCase();
|
|
16
17
|
return tokens.some(t => t.startsWith(".") ? name.endsWith(t) : t.endsWith("/*") ? type.startsWith(t.slice(0, -1)) : type === t);
|
|
17
18
|
}
|
|
19
|
+
// G3: a miniatura é uma URL de objeto, e ela VAZA se ninguém revogar — o navegador
|
|
20
|
+
// segura o blob até a página morrer. Só imagem ganha prévia; para o resto o ícone de
|
|
21
|
+
// documento já diz o que é, e gerar URL para um PDF de 40 MB não mostra nada.
|
|
22
|
+
const ehImagem = (f) => f.type.startsWith("image/");
|
|
23
|
+
// G6: o "quando" do recibo. Fora do render de propósito — dentro dele o HTML mudaria
|
|
24
|
+
// sozinho a cada segundo, que é o defeito que o check 25 pega no catálogo.
|
|
25
|
+
const agora = () => new Date().toISOString();
|
|
26
|
+
// O recibo é colado num chamado, num e-mail ou num bloco de notas — nenhum dos três
|
|
27
|
+
// lê marcação, então o formato é TSV puro. Os separadores vêm por código porque
|
|
28
|
+
// escapes literais neste arquivo já se perderam uma vez em edição por script.
|
|
29
|
+
const TAB = String.fromCharCode(9), NL = String.fromCharCode(10);
|
|
30
|
+
// G4: SHA-256 pelo Web Crypto — nada de dependência, é API de plataforma.
|
|
31
|
+
// LIMITE, e ele é declarado: `crypto.subtle` só existe em contexto seguro (https ou
|
|
32
|
+
// localhost), e o digest precisa do arquivo INTEIRO em memória, porque o Web Crypto
|
|
33
|
+
// não tem hash em fluxo. Por isso é opcional, e não o default.
|
|
34
|
+
async function somaSha256(file) {
|
|
35
|
+
if (typeof crypto === "undefined" || !crypto.subtle)
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
const buf = await crypto.subtle.digest("SHA-256", await file.arrayBuffer());
|
|
39
|
+
return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, "0")).join("");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
18
45
|
function formatSize(bytes) {
|
|
19
46
|
if (bytes < 1024)
|
|
20
47
|
return `${bytes} B`;
|
|
@@ -26,43 +53,81 @@ function formatSize(bytes) {
|
|
|
26
53
|
}
|
|
27
54
|
return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
|
|
28
55
|
}
|
|
29
|
-
export function FileInput({ accept, maxSize, multiple, onFilesChange, upload, label, hint, id, className }) {
|
|
56
|
+
export function FileInput({ accept, maxSize, multiple, onFilesChange, upload, label, hint, id, className, initialQueue, onQueueChange, preview, checksum }) {
|
|
30
57
|
const s = useAureaStrings();
|
|
31
58
|
const autoId = React.useId();
|
|
32
59
|
const inputId = id ?? autoId;
|
|
60
|
+
// G1: a fila restaurada entra como estado INICIAL, não como prop controlada — e a
|
|
61
|
+
// palavra "inicial" é literal: um item restaurado não tem `File`, então controlá-lo
|
|
62
|
+
// de fora seria prometer um round-trip que a plataforma não faz.
|
|
33
63
|
const nextId = React.useRef(0);
|
|
34
|
-
const [files, setFiles] = React.useState([]);
|
|
64
|
+
const [files, setFiles] = React.useState(() => (initialQueue ?? []).map(q => ({ ...q, restored: true })));
|
|
35
65
|
const [rejected, setRejected] = React.useState([]);
|
|
66
|
+
// a soma chega DEPOIS do envio começar (é assíncrona), então a comparação lê a
|
|
67
|
+
// lista atual por ref em vez da closure do início do upload.
|
|
68
|
+
const filesRef = React.useRef([]);
|
|
69
|
+
// E guarda a PROMESSA, não só o valor: um arquivo pequeno com envio rápido termina
|
|
70
|
+
// antes de o hash sair, e comparar contra `undefined` pularia a conferência em
|
|
71
|
+
// silêncio — que é pior que não conferir, porque parece que conferiu. Quem achou
|
|
72
|
+
// isso foi o teste, não a leitura.
|
|
73
|
+
const somas = React.useRef(new Map());
|
|
36
74
|
const [announce, setAnnounce] = React.useState("");
|
|
75
|
+
// G5: conflito NÃO é rejeição — o arquivo é válido, só o nome já está na fila.
|
|
76
|
+
// Fica em espera com a escolha na tela, porque sobrescrever em silêncio é o
|
|
77
|
+
// comportamento que o item existe para proibir.
|
|
78
|
+
const [conflitos, setConflitos] = React.useState([]);
|
|
37
79
|
const [dragging, setDragging] = React.useState(false);
|
|
38
80
|
const controllers = React.useRef(new Map());
|
|
39
81
|
// Desmontar aborta tudo em voo — sem isso requisições e closures sobrevivem à
|
|
40
82
|
// tela (auditoria 18/07/2026, MÉDIO 5). Concorrência/agenda é do consumidor: o
|
|
41
83
|
// transporte é dele (UploadFn); o componente dispara um upload() por arquivo aceito.
|
|
42
84
|
React.useEffect(() => { const map = controllers.current; return () => map.forEach(c => c.abort()); }, []);
|
|
43
|
-
|
|
44
|
-
|
|
85
|
+
// As URLs de objeto vivas moram numa ref para poderem ser revogadas no desmonte —
|
|
86
|
+
// ler `files` de dentro do cleanup pegaria a lista do primeiro render.
|
|
87
|
+
const previas = React.useRef(new Set());
|
|
88
|
+
React.useEffect(() => { const set = previas.current; return () => set.forEach(u => URL.revokeObjectURL(u)); }, []);
|
|
89
|
+
// G1: toda mudança da fila avisa por fora, em forma serializável. É o que o
|
|
90
|
+
// consumidor guarda; onde ele guarda — armazenamento local, servidor, nada — é
|
|
91
|
+
// decisão dele, a mesma linha do estado da grade na URL (PLANO-1.0 F4).
|
|
92
|
+
const paraFila = (l) => l.map(({ id, name, bytes, status, progress, restored, checksum: soma, finishedAt }) => ({ id, name, bytes, status, progress, ...(soma ? { checksum: soma } : {}), ...(finishedAt ? { finishedAt } : {}), ...(restored ? { restored: true } : {}) }));
|
|
93
|
+
const emit = (next) => { filesRef.current = next; setFiles(next); onFilesChange?.(next.flatMap(f => f.file ? [f.file] : [])); onQueueChange?.(paraFila(next)); };
|
|
94
|
+
const patch = (fid, p) => setFiles(prev => { const next = prev.map(e => e.id === fid ? { ...e, ...p } : e); filesRef.current = next; onQueueChange?.(paraFila(next)); return next; });
|
|
45
95
|
const abortId = (fid) => controllers.current.get(fid)?.abort();
|
|
46
96
|
// Cada arquivo envia por conta própria (promessa independente + catch por arquivo):
|
|
47
97
|
// erro ou cancelamento de um não derruba a fila. Cancelar = abort() no controller;
|
|
48
98
|
// no catch, signal.aborted distingue "cancelado" de "falhou" seja qual for o erro
|
|
49
99
|
// que o consumidor lançou. patch é setState funcional → seguro sob closures velhas.
|
|
50
|
-
|
|
100
|
+
// G2: pausar HTTP não é pausar — é abortar. Então `pausando` marca a INTENÇÃO antes
|
|
101
|
+
// do abort, e o catch a lê para separar "pausei" de "cancelei": os dois chegam como
|
|
102
|
+
// `signal.aborted`, e tratá-los igual perderia o progresso e o direito de retomar.
|
|
103
|
+
const pausando = React.useRef(new Set());
|
|
104
|
+
const startUpload = (fid, file, resumeFrom = 0) => {
|
|
51
105
|
if (!upload)
|
|
52
106
|
return;
|
|
53
107
|
const c = new AbortController();
|
|
54
108
|
controllers.current.set(fid, c);
|
|
55
|
-
patch(fid, { status: "uploading", progress:
|
|
109
|
+
patch(fid, { status: "uploading", progress: resumeFrom });
|
|
56
110
|
(async () => {
|
|
57
111
|
try {
|
|
58
|
-
await upload(file, { signal: c.signal, onProgress: f => patch(fid, { progress: Math.max(0, Math.min(1, f)) }) });
|
|
59
|
-
|
|
112
|
+
const devolvido = await upload(file, { signal: c.signal, resumeFrom, onProgress: f => patch(fid, { progress: Math.max(0, Math.min(1, f)) }) });
|
|
113
|
+
// G4: conferir é comparar. Se o servidor devolveu uma soma e ela difere da
|
|
114
|
+
// nossa, o arquivo que chegou lá NÃO é o que saiu daqui — e isso é erro, não
|
|
115
|
+
// detalhe. Sem soma devolvida não há o que conferir, e o envio segue válido.
|
|
116
|
+
const local = await (somas.current.get(fid) ?? Promise.resolve(filesRef.current.find(e => e.id === fid)?.checksum));
|
|
117
|
+
const doServidor = devolvido && typeof devolvido === "object" ? devolvido.checksum : undefined;
|
|
118
|
+
if (doServidor && local && doServidor.toLowerCase() !== local.toLowerCase()) {
|
|
119
|
+
patch(fid, { status: "error", erro: s.uploadChecksumBad, finishedAt: agora() });
|
|
120
|
+
setAnnounce(`${s.uploadChecksumBad}: ${file.name}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
patch(fid, { status: "done", progress: 1, finishedAt: agora() });
|
|
60
124
|
setAnnounce(`${s.uploadComplete}: ${file.name}`);
|
|
61
125
|
}
|
|
62
126
|
catch {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
127
|
+
const foiPausa = pausando.current.delete(fid);
|
|
128
|
+
const st = foiPausa ? "paused" : c.signal.aborted ? "canceled" : "error";
|
|
129
|
+
patch(fid, { status: st, ...(st === "paused" ? {} : { finishedAt: agora() }) });
|
|
130
|
+
setAnnounce(`${st === "paused" ? s.uploadPaused : st === "canceled" ? s.uploadCanceled : s.uploadError}: ${file.name}`);
|
|
66
131
|
}
|
|
67
132
|
finally {
|
|
68
133
|
controllers.current.delete(fid);
|
|
@@ -72,14 +137,22 @@ export function FileInput({ accept, maxSize, multiple, onFilesChange, upload, la
|
|
|
72
137
|
const add = (list) => {
|
|
73
138
|
if (!list || !list.length)
|
|
74
139
|
return;
|
|
75
|
-
const ok = [], bad = [];
|
|
140
|
+
const ok = [], bad = [], novosConflitos = [];
|
|
76
141
|
for (const file of Array.from(list)) {
|
|
77
142
|
const reason = !matchesAccept(file, accept) ? s.fileWrongType : (maxSize != null && file.size > maxSize) ? s.fileTooLarge : null;
|
|
78
|
-
if (reason)
|
|
143
|
+
if (reason) {
|
|
79
144
|
bad.push({ file, reason });
|
|
80
|
-
|
|
81
|
-
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const jaTem = multiple ? files.find(e => e.name === file.name) : undefined;
|
|
148
|
+
if (jaTem) {
|
|
149
|
+
novosConflitos.push({ file, existente: jaTem.id });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
ok.push({ id: `f${nextId.current++}`, file, name: file.name, bytes: file.size, status: "pending", progress: 0 });
|
|
82
153
|
}
|
|
154
|
+
if (novosConflitos.length)
|
|
155
|
+
setConflitos(c => [...c, ...novosConflitos]);
|
|
83
156
|
// multiple acumula; single substitui (input nativo já entrega 1 arquivo).
|
|
84
157
|
// só emite se algo passou — drop 100% rejeitado não mexe na lista atual.
|
|
85
158
|
const added = multiple ? ok : ok.slice(-1);
|
|
@@ -87,20 +160,80 @@ export function FileInput({ accept, maxSize, multiple, onFilesChange, upload, la
|
|
|
87
160
|
if (!multiple)
|
|
88
161
|
files.forEach(f => abortId(f.id)); // single troca o arquivo: aborta o envio anterior
|
|
89
162
|
emit(multiple ? [...files, ...added] : added);
|
|
163
|
+
if (preview)
|
|
164
|
+
added.forEach(e => { if (e.file && ehImagem(e.file)) {
|
|
165
|
+
const u = URL.createObjectURL(e.file);
|
|
166
|
+
previas.current.add(u);
|
|
167
|
+
patch(e.id, { preview: u });
|
|
168
|
+
} });
|
|
169
|
+
if (checksum)
|
|
170
|
+
added.forEach(e => { if (e.file)
|
|
171
|
+
somas.current.set(e.id, somaSha256(e.file).then(h => { if (h)
|
|
172
|
+
patch(e.id, { checksum: h }); return h; })); });
|
|
90
173
|
added.forEach(e => startUpload(e.id, e.file));
|
|
91
174
|
}
|
|
92
175
|
setRejected(bad);
|
|
93
|
-
setAnnounce([ok.length && `${s.fileAdded}: ${ok.map(f => f.
|
|
176
|
+
setAnnounce([ok.length && `${s.fileAdded}: ${ok.map(f => f.name).join(", ")}`, ...bad.map(b => `${b.reason}: ${b.file.name}`)].filter(Boolean).join(". "));
|
|
177
|
+
};
|
|
178
|
+
// G2: pausar é abortar com intenção declarada; retomar é chamar de novo, passando
|
|
179
|
+
// o quanto já subiu. Por ITEM e no CONJUNTO — e o conjunto é só o laço, porque a
|
|
180
|
+
// regra tem de ser a mesma nos dois: senão "pausar tudo" e "pausar cada um" acabam
|
|
181
|
+
// em estados diferentes, que é o defeito clássico deste tipo de barra.
|
|
182
|
+
const pausar = (fid) => { pausando.current.add(fid); abortId(fid); };
|
|
183
|
+
const retomar = (e) => { if (e.file)
|
|
184
|
+
startUpload(e.id, e.file, e.progress); };
|
|
185
|
+
const pausarTudo = () => files.filter(f => f.status === "uploading").forEach(f => pausar(f.id));
|
|
186
|
+
const retomarTudo = () => files.filter(f => f.status === "paused").forEach(retomar);
|
|
187
|
+
const enviando = files.some(f => f.status === "uploading"), pausados = files.some(f => f.status === "paused");
|
|
188
|
+
// As três saídas do conflito, e nenhuma é silenciosa. "Manter os dois" não renomeia
|
|
189
|
+
// nada: os ids é que distinguem, e inventar "arquivo (1).txt" seria decidir pelo
|
|
190
|
+
// consumidor um nome que o servidor dele talvez não aceite.
|
|
191
|
+
// A lista base vem por ARGUMENTO, não da closure: em "substituir" o item removido
|
|
192
|
+
// ainda está em `files` quando esta função roda — o React não re-renderizou — e ler
|
|
193
|
+
// a closure deixava os DOIS na fila. Foi o teste que achou.
|
|
194
|
+
const aceitarConflito = (file, base = files) => {
|
|
195
|
+
const e = { id: `f${nextId.current++}`, file, name: file.name, bytes: file.size, status: "pending", progress: 0 };
|
|
196
|
+
emit([...base, e]);
|
|
197
|
+
if (preview && ehImagem(file)) {
|
|
198
|
+
const u = URL.createObjectURL(file);
|
|
199
|
+
previas.current.add(u);
|
|
200
|
+
patch(e.id, { preview: u });
|
|
201
|
+
}
|
|
202
|
+
if (checksum)
|
|
203
|
+
somas.current.set(e.id, somaSha256(file).then(h => { if (h)
|
|
204
|
+
patch(e.id, { checksum: h }); return h; }));
|
|
205
|
+
startUpload(e.id, file);
|
|
206
|
+
};
|
|
207
|
+
const resolverConflito = (i, escolha) => {
|
|
208
|
+
const c = conflitos[i];
|
|
209
|
+
setConflitos(l => l.filter((_, n) => n !== i));
|
|
210
|
+
if (escolha === "skip")
|
|
211
|
+
return;
|
|
212
|
+
if (escolha === "replace") {
|
|
213
|
+
abortId(c.existente);
|
|
214
|
+
aceitarConflito(c.file, files.filter(x => x.id !== c.existente));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
aceitarConflito(c.file);
|
|
94
218
|
};
|
|
95
|
-
const remove = (rid) => { const f = files.find(x => x.id === rid);
|
|
219
|
+
const remove = (rid) => { const f = files.find(x => x.id === rid); if (f?.preview) {
|
|
220
|
+
URL.revokeObjectURL(f.preview);
|
|
221
|
+
previas.current.delete(f.preview);
|
|
222
|
+
} abortId(rid); emit(files.filter(x => x.id !== rid)); setAnnounce(f ? `${s.fileRemoved}: ${f.name}` : ""); };
|
|
223
|
+
// G6: "terminou" é não haver mais nada em voo nem esperando — pausado conta como em
|
|
224
|
+
// voo, porque a pessoa ainda vai retomar.
|
|
225
|
+
const emVoo = files.some(f => f.status === "uploading" || f.status === "paused" || f.status === "pending");
|
|
226
|
+
const recibo = files.filter(f => f.status === "done" || f.status === "error" || f.status === "canceled");
|
|
227
|
+
const reciboTexto = recibo.map(r => [r.name, r.status, r.finishedAt ?? ""].join(TAB)).join(NL);
|
|
96
228
|
return _jsxs("div", { className: cx("field", className), children: [label && _jsx("label", { className: "label", htmlFor: inputId, children: label }), _jsxs("label", { className: "dropzone", "data-dragging": dragging || undefined, onDragOver: e => { e.preventDefault(); setDragging(true); }, onDragLeave: e => { if (!e.currentTarget.contains(e.relatedTarget))
|
|
97
|
-
setDragging(false); }, onDrop: e => { e.preventDefault(); setDragging(false); add(e.dataTransfer.files); }, children: [_jsx("input", { id: inputId, type: "file", className: "sr-only", accept: accept, multiple: multiple, onChange: e => { add(e.target.files); e.target.value = ""; } }), _jsx(Icon, { name: "cloud--upload", size: "xl", className: "dropzone-icon" }), _jsx("strong", { children: s.fileDropPrompt }), hint && _jsx("span", { className: "dropzone-hint", children: hint })] }),
|
|
229
|
+
setDragging(false); }, onDrop: e => { e.preventDefault(); setDragging(false); add(e.dataTransfer.files); }, children: [_jsx("input", { id: inputId, type: "file", className: "sr-only", accept: accept, multiple: multiple, onChange: e => { add(e.target.files); e.target.value = ""; } }), _jsx(Icon, { name: "cloud--upload", size: "xl", className: "dropzone-icon" }), _jsx("strong", { children: s.fileDropPrompt }), hint && _jsx("span", { className: "dropzone-hint", children: hint })] }), upload && (enviando || pausados) && _jsx("div", { className: "file-queue-actions", children: enviando ? _jsx(Button, { variant: "secondary", size: "sm", leadingIcon: "pause", onClick: pausarTudo, children: s.uploadPause })
|
|
230
|
+
: _jsx(Button, { variant: "secondary", size: "sm", leadingIcon: "play", onClick: retomarTudo, children: s.uploadResume }) }), files.length > 0 && _jsx("ul", { className: "file-list", children: files.map(({ id, file, name, bytes, status, progress = 0, restored, checksum: soma, preview: previa, erro }) => {
|
|
98
231
|
// A barra reusa .progress + .file-progress (largura fixa em classe); só o
|
|
99
232
|
// preenchimento (width %) fica inline, por ser data-driven. Erro reusa
|
|
100
233
|
// .field-error. Como .file-progress não é usada nos docs, adicioná-la ao core
|
|
101
234
|
// não muda pixel de screenshot nenhum (auditoria 18/07, M14: CSS não usado
|
|
102
235
|
// pelos docs não altera baseline — a justificativa antiga estava invertida).
|
|
103
236
|
const pct = Math.round(progress * 100);
|
|
104
|
-
return _jsxs("li", { className: "file-item", children: [_jsx(Icon, { name: "document" }), _jsx("span", { className: "file-name", children:
|
|
105
|
-
}) }), rejected.map((r, n) => _jsxs("span", { className: "field-error", children: [r.reason, ": ", r.file.name] }, n)), _jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", children: announce })] });
|
|
237
|
+
return _jsxs("li", { className: "file-item", children: [previa ? _jsx("img", { className: "file-thumb", src: previa, alt: "" }) : _jsx(Icon, { name: "document" }), _jsx("span", { className: "file-name", children: name }), status === "uploading" ? _jsxs(_Fragment, { children: [_jsx("div", { className: "progress file-progress", role: "progressbar", "aria-label": `${s.uploadSending} ${name}`, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct, children: _jsx("span", { style: { width: `${pct}%` } }) }), _jsx(IconButton, { variant: "ghost", size: "sm", icon: "pause", label: `${s.uploadPause} ${name}`, onClick: () => pausar(id) }), _jsx(IconButton, { variant: "ghost", size: "sm", icon: "close", label: `${s.uploadCancel} ${name}`, onClick: () => abortId(id) })] }) : status === "paused" ? _jsxs(_Fragment, { children: [_jsxs("span", { className: "file-size", children: [s.uploadPaused, " \u00B7 ", Math.round(progress * 100), "%"] }), _jsx(IconButton, { variant: "ghost", size: "sm", icon: "play", label: `${s.uploadResume} ${name}`, onClick: () => retomar({ id, file, name, bytes, status, progress, restored }) }), _jsx(IconButton, { variant: "ghost", size: "sm", icon: "close", label: `${s.fileRemove} ${name}`, onClick: () => remove(id) })] }) : (status === "error" || status === "canceled") ? _jsxs(_Fragment, { children: [_jsx("span", { className: "field-error", children: erro ?? (status === "error" ? s.uploadError : s.uploadCanceled) }), file && _jsx(IconButton, { variant: "ghost", size: "sm", icon: "restart", label: `${s.uploadRetry} ${name}`, onClick: () => startUpload(id, file) }), _jsx(IconButton, { variant: "ghost", size: "sm", icon: "close", label: `${s.fileRemove} ${name}`, onClick: () => remove(id) })] }) : _jsxs(_Fragment, { children: [status === "done" && _jsx(Icon, { name: "checkmark--filled" }), _jsx("span", { className: "file-size", children: formatSize(bytes) }), soma && _jsx("span", { className: "file-checksum", title: `${s.uploadChecksum}: ${soma}`, children: soma.slice(0, 8) }), _jsx(IconButton, { variant: "ghost", size: "sm", icon: "close", label: `${s.fileRemove} ${name}`, onClick: () => remove(id) })] })] }, id);
|
|
238
|
+
}) }), conflitos.map((c, i) => _jsxs("div", { className: "file-conflict", role: "alert", children: [_jsxs("span", { children: [s.fileConflict, ": ", _jsx("strong", { children: c.file.name })] }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => resolverConflito(i, "replace"), children: s.fileReplace }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => resolverConflito(i, "both"), children: s.fileKeepBoth }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => resolverConflito(i, "skip"), children: s.fileSkip })] }, `c${i}`)), recibo.length > 0 && !emVoo && _jsxs("div", { className: "file-receipt", children: [_jsxs("div", { className: "file-receipt-head", children: [_jsx("strong", { children: s.uploadReceipt }), _jsx(Button, { size: "sm", variant: "ghost", leadingIcon: "copy", onClick: () => void navigator.clipboard?.writeText(reciboTexto), children: s.uploadReceiptCopy })] }), _jsx("ul", { children: recibo.map(r => _jsxs("li", { children: [r.name, " \u00B7 ", r.status === "done" ? s.uploadComplete : r.status === "canceled" ? s.uploadCanceled : s.uploadError, r.finishedAt ? ` · ${r.finishedAt}` : ""] }, r.id)) })] }), rejected.map((r, n) => _jsxs("span", { className: "field-error", children: [r.reason, ": ", r.file.name] }, n)), _jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", children: announce })] });
|
|
106
239
|
}
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
export interface GraphNodeItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
kind?: string;
|
|
6
|
+
detail?: ReactNode;
|
|
7
|
+
x?: number;
|
|
8
|
+
y?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface GraphEdgeItem {
|
|
11
|
+
from: string;
|
|
12
|
+
to: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
animated?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function DependencyGraph({ nodes, edges, label, selectedId, onSelect, connectable, onConnect, onNodeMove, height, className, ...props }: {
|
|
17
|
+
nodes: GraphNodeItem[];
|
|
18
|
+
edges: GraphEdgeItem[];
|
|
19
|
+
label?: string;
|
|
20
|
+
selectedId?: string | null;
|
|
21
|
+
onSelect?: (id: string) => void;
|
|
22
|
+
connectable?: boolean;
|
|
23
|
+
onConnect?: (edge: {
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
}) => void;
|
|
27
|
+
onNodeMove?: (id: string, pos: {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
}) => void;
|
|
31
|
+
height?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
} & Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect">): React.JSX.Element;
|
package/dist/graph.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// DependencyGraph — PLANO-1.0, Parte H, item H14 (09/08/2026).
|
|
4
|
+
//
|
|
5
|
+
// SUBPATH PRÓPRIO (`@aurea-uds/react/graph`), como ./chart, ./data-grid, ./calendar e ./qrcode:
|
|
6
|
+
// é o único componente que precisa do `@xyflow/react`, que entra como peer OPCIONAL. Quem
|
|
7
|
+
// instala a biblioteca pelo Button não paga por ele — e o check 19 reprova se este import
|
|
8
|
+
// aparecer em qualquer outro módulo.
|
|
9
|
+
//
|
|
10
|
+
// A diretiva NÃO vem de um hook nosso: vem do motor, que usa `zustand` e não publica
|
|
11
|
+
// `"use client"` no dist — o mesmo caso do `recharts`, do `@tanstack/react-table` e do
|
|
12
|
+
// `react-day-picker`, medido em 06/08/2026 e cobrado pelo check 26.
|
|
13
|
+
//
|
|
14
|
+
// ── O MOTOR, e por que este ──────────────────────────────────────────────────────────────
|
|
15
|
+
// Autorizado pelo Victor em 09/08/2026, e a autorização foi RECONFIRMADA por medição depois de
|
|
16
|
+
// eu mesmo levantar dúvida. A dúvida era boa e a resposta desfez: o `trace-graph-view/` do
|
|
17
|
+
// langfuse — a referência mais próxima do domínio — desenha `<div>` posicionados por ELK e NÃO
|
|
18
|
+
// usa React Flow, e o React Flow não faz layout nenhum, então adotá-lo não entrega um grafo
|
|
19
|
+
// pronto. O que decidiu foi o ALVO, que o Victor nomeou: uma aplicação tipo n8n. Aí a conta
|
|
20
|
+
// vira outra, e ela é medida:
|
|
21
|
+
// • a `activepieces-main`, que já está em `Referencia/` e é concorrente direta do n8n, usa
|
|
22
|
+
// `@xyflow/react` 12.3.5 — está no `package.json` dela;
|
|
23
|
+
// • o próprio n8n usa **Vue Flow**, o irmão Vue do React Flow, da mesma equipe xyflow.
|
|
24
|
+
// Grafo de LEITURA não precisa de motor. EDITOR precisa, e o padrão de mercado para este
|
|
25
|
+
// editor é o xyflow. Registro completo no `REFERENCES.md`.
|
|
26
|
+
//
|
|
27
|
+
// Segurança e saúde, conferidas em 09/08/2026 antes de instalar: `@xyflow/react` 12.11.2,
|
|
28
|
+
// **MIT**, publicado há ~1 mês, mantido em tempo integral; **zero** avisos no GitHub Advisory
|
|
29
|
+
// Database para `xyflow`. Traz `@xyflow/system`, `classcat` e `zustand@4` — e só para quem
|
|
30
|
+
// instalar o peer.
|
|
31
|
+
//
|
|
32
|
+
// ── A PELE, e a linha que não se cruza ───────────────────────────────────────────────────
|
|
33
|
+
// O motor publica DUAS folhas, e só uma pode entrar:
|
|
34
|
+
// • `dist/style.css` é a IDENTIDADE deles — `--xy-node-border-radius-default: 3px`, `#1a192b`,
|
|
35
|
+
// sombras, nó branco. É exatamente o que o `BUILDING.md` proíbe extrair. **Não entra.**
|
|
36
|
+
// • `dist/base.css` é ESTRUTURA — posição, `z-index`, `transform-origin`, `pointer-events` —
|
|
37
|
+
// e todo valor visível está atrás de `var(--xy-algo, --xy-algo-default)`. Sobrescritível
|
|
38
|
+
// por token, no idioma que o `--qr-size` e o `--datagrid-max-h` já usam.
|
|
39
|
+
// Quem importa a folha é o CONSUMIDOR, uma vez, como já importa o `@aurea-uds/core/css`: um
|
|
40
|
+
// `import "…css"` dentro deste módulo quebraria em Node puro e o build daqui é `tsc` só.
|
|
41
|
+
import React from "react";
|
|
42
|
+
import { ReactFlow, ReactFlowProvider, Background, Handle, Position, applyNodeChanges } from "@xyflow/react";
|
|
43
|
+
import { cx, useAureaStrings } from "./internal.js";
|
|
44
|
+
// ── O layout ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// O motor recebe `x`/`y` PRONTOS: ele não posiciona nada. Sem isto, um grafo sem coordenadas
|
|
46
|
+
// empilha tudo em (0,0) — que foi a medição que quase derrubou a escolha do motor.
|
|
47
|
+
//
|
|
48
|
+
// Camadas por caminho mais longo: a profundidade de um nó é a maior profundidade entre os que
|
|
49
|
+
// apontam para ele, mais um. É o esqueleto do Sugiyama sem a parte cara.
|
|
50
|
+
//
|
|
51
|
+
// ponytail: sem minimização de cruzamento de arestas — grafo denso vai desenhar linhas se
|
|
52
|
+
// cruzando, e isso é aceitável para dependência (dezenas de nós), não para mil. O caminho de
|
|
53
|
+
// subida é o `elkjs`, que é o que o langfuse usa, e ele é DEPENDÊNCIA NOVA: entra com
|
|
54
|
+
// autorização, não de contrabando.
|
|
55
|
+
const LARGURA = 180, ALTURA = 52, VAO_X = 90, VAO_Y = 24;
|
|
56
|
+
function dispor(nodes, edges) {
|
|
57
|
+
const entram = new Map();
|
|
58
|
+
for (const n of nodes)
|
|
59
|
+
entram.set(n.id, []);
|
|
60
|
+
for (const e of edges)
|
|
61
|
+
if (entram.has(e.to) && entram.has(e.from))
|
|
62
|
+
entram.get(e.to).push(e.from);
|
|
63
|
+
const profundidade = new Map();
|
|
64
|
+
// `visitando` corta CICLO. Um grafo de dependências com ciclo é um defeito do dado, não
|
|
65
|
+
// deste componente — mas travar o navegador por causa dele seria defeito nosso.
|
|
66
|
+
const visitando = new Set();
|
|
67
|
+
const calcular = (id) => {
|
|
68
|
+
const pronto = profundidade.get(id);
|
|
69
|
+
if (pronto != null)
|
|
70
|
+
return pronto;
|
|
71
|
+
if (visitando.has(id))
|
|
72
|
+
return 0;
|
|
73
|
+
visitando.add(id);
|
|
74
|
+
const pais = entram.get(id) ?? [];
|
|
75
|
+
const d = pais.length ? Math.max(...pais.map(calcular)) + 1 : 0;
|
|
76
|
+
visitando.delete(id);
|
|
77
|
+
profundidade.set(id, d);
|
|
78
|
+
return d;
|
|
79
|
+
};
|
|
80
|
+
for (const n of nodes)
|
|
81
|
+
calcular(n.id);
|
|
82
|
+
const ocupacao = new Map();
|
|
83
|
+
const posicoes = new Map();
|
|
84
|
+
for (const n of nodes) {
|
|
85
|
+
const d = profundidade.get(n.id) ?? 0;
|
|
86
|
+
const linha = ocupacao.get(d) ?? 0;
|
|
87
|
+
ocupacao.set(d, linha + 1);
|
|
88
|
+
posicoes.set(n.id, { x: d * (LARGURA + VAO_X), y: linha * (ALTURA + VAO_Y) });
|
|
89
|
+
}
|
|
90
|
+
// A EXTENSÃO existe para o SERVIDOR: sem ela o `fitView` não tem viewport para caber, e o
|
|
91
|
+
// grafo renderizado fora do navegador sai ancorado no canto, com metade dos nós do lado de
|
|
92
|
+
// fora da caixa. É a diferença entre a prévia do catálogo existir e não existir.
|
|
93
|
+
//
|
|
94
|
+
// Os campos se chamam `larg`/`alt` e não `width`/`height` por dois motivos: é o vocabulário
|
|
95
|
+
// interno deste arquivo (`posicoes`, `ocupacao`, `profundidade`), e o check 23 lê o fonte
|
|
96
|
+
// INTEIRO — ele existe para impedir prop de dimensão em número, e um par de pixels de layout
|
|
97
|
+
// interno o acionava sem ser prop de ninguém.
|
|
98
|
+
const colunas = Math.max(...[...ocupacao.keys()], 0) + 1;
|
|
99
|
+
const linhas = Math.max(...[...ocupacao.values()], 1);
|
|
100
|
+
return { posicoes, extensao: { larg: colunas * LARGURA + (colunas - 1) * VAO_X, alt: linhas * ALTURA + (linhas - 1) * VAO_Y } };
|
|
101
|
+
}
|
|
102
|
+
// O nó é NOSSO, e é o ponto em que este componente deixa de ser "React Flow com outra cor".
|
|
103
|
+
// O `GraphNode.tsx` do langfuse declara o motivo em uma linha — *"Real-HTML accessibility (the
|
|
104
|
+
// win over the old canvas renderer)"* —: nó desenhado em canvas não existe para o teclado nem
|
|
105
|
+
// para o leitor de tela. Aqui ele é `<button>` de verdade quando dá para selecionar.
|
|
106
|
+
//
|
|
107
|
+
// O que NÃO entrou: as DEZ cores por tipo de nó da referência (`AGENT` roxo, `TOOL` laranja,
|
|
108
|
+
// `GENERATION` magenta…). É a quarta vez que esta parte recusa a mesma coisa. `kind` sai como
|
|
109
|
+
// TEXTO, que é legível também por quem não separa as cores.
|
|
110
|
+
function NoAurea({ data }) {
|
|
111
|
+
const d = data;
|
|
112
|
+
const corpo = _jsxs(_Fragment, { children: [_jsx("span", { className: "graph-node-label", children: d.label }), d.kind && _jsx("span", { className: "graph-node-kind", children: d.kind }), d.detail && _jsx("span", { className: "hint", children: d.detail })] });
|
|
113
|
+
return _jsxs("div", { className: "graph-node", "data-selected": d.selecionado || undefined, children: [_jsx(Handle, { type: "target", position: Position.Left, isConnectable: !!d.conectavel, className: "graph-handle" }), d.aoSelecionar
|
|
114
|
+
? _jsx("button", { type: "button", className: "graph-node-body", "aria-pressed": !!d.selecionado, onClick: d.aoSelecionar, children: corpo })
|
|
115
|
+
: _jsx("span", { className: "graph-node-body", children: corpo }), _jsx(Handle, { type: "source", position: Position.Right, isConnectable: !!d.conectavel, className: "graph-handle" })] });
|
|
116
|
+
}
|
|
117
|
+
const TIPOS = { aurea: NoAurea };
|
|
118
|
+
// DependencyGraph — quem depende de quem.
|
|
119
|
+
//
|
|
120
|
+
// O vocabulário da API é NOSSO — `{from,to}`, não `{source,target}`. Não é preciosismo: é o que
|
|
121
|
+
// permite trocar o motor sem quebrar quem usa, e é a mesma razão pela qual o `Chart` não expõe
|
|
122
|
+
// os tipos do Recharts. O escopo também é menor que o do motor: nada de minimapa, de painéis,
|
|
123
|
+
// de nó redimensionável nem de seleção por retângulo — isso é superfície de EDITOR, e o editor
|
|
124
|
+
// é a Parte I (`VisualBuilder`, item I8), que compõe este componente em vez de reescrevê-lo.
|
|
125
|
+
export function DependencyGraph({ nodes, edges, label, selectedId, onSelect, connectable, onConnect, onNodeMove, height, className, ...props }) {
|
|
126
|
+
const s = useAureaStrings();
|
|
127
|
+
// O layout só roda para quem não trouxe coordenada. Quem traz manda — é o que deixa a Parte I
|
|
128
|
+
// guardar a posição que a pessoa arrastou sem brigar com a disposição automática.
|
|
129
|
+
const automatico = React.useMemo(() => dispor(nodes, edges), [nodes, edges]);
|
|
130
|
+
const nosDoMotor = React.useMemo(() => nodes.map(n => ({
|
|
131
|
+
id: n.id, type: "aurea",
|
|
132
|
+
position: { x: n.x ?? automatico.posicoes.get(n.id)?.x ?? 0, y: n.y ?? automatico.posicoes.get(n.id)?.y ?? 0 },
|
|
133
|
+
draggable: !!onNodeMove,
|
|
134
|
+
// ── O QUE FAZ ESTE GRAFO EXISTIR FORA DO NAVEGADOR ─────────────────────────────────
|
|
135
|
+
// O motor só desenha nó que tem tamanho, e no servidor não há o que medir. `initialWidth`
|
|
136
|
+
// e `initialHeight` valem SÓ até a primeira medição — no navegador o tamanho real assume
|
|
137
|
+
// e um nó de três linhas deixa de ser aproximado. E sem `handles` declarados a aresta não
|
|
138
|
+
// sabe de onde sai nem onde chega, então ela some.
|
|
139
|
+
//
|
|
140
|
+
// Eu tinha concluído que grafo não cabia em HTML estático e escrevi isso como limite do
|
|
141
|
+
// componente. Era conclusão minha, não medição: o motor tem caminho de SSR documentado, e
|
|
142
|
+
// são estas três coisas. A prévia do catálogo existe por causa deste bloco.
|
|
143
|
+
initialWidth: LARGURA, initialHeight: ALTURA,
|
|
144
|
+
handles: [{ type: "target", position: Position.Left, x: 0, y: ALTURA / 2 },
|
|
145
|
+
{ type: "source", position: Position.Right, x: LARGURA, y: ALTURA / 2 }],
|
|
146
|
+
data: { label: n.label, kind: n.kind, detail: n.detail, selecionado: selectedId === n.id,
|
|
147
|
+
conectavel: !!connectable, aoSelecionar: onSelect ? () => onSelect(n.id) : undefined },
|
|
148
|
+
})), [nodes, automatico, selectedId, onSelect, connectable, onNodeMove]);
|
|
149
|
+
const arestasDoMotor = React.useMemo(() => edges.map(e => ({
|
|
150
|
+
id: `${e.from}->${e.to}`, source: e.from, target: e.to, label: e.label, animated: e.animated,
|
|
151
|
+
})), [edges]);
|
|
152
|
+
// ── A MEDIDA TEM DE PODER VOLTAR, e isto foi achado NO NAVEGADOR ───────────────────────
|
|
153
|
+
// A primeira versão passava `nodes` e nenhum `onNodesChange`. O motor aceita calado e o
|
|
154
|
+
// resultado é um grafo INVISÍVEL: ele só tira o `visibility:hidden` do nó depois de gravar
|
|
155
|
+
// a medida dele de volta na lista, e sem o retorno não tem onde gravar. Medido em
|
|
156
|
+
// 09/08/2026 num navegador de verdade — os cinco nós saíam `visibility: hidden`, com ZERO
|
|
157
|
+
// aresta e o `fitView` parado em `scale(1)`. Nenhum teste de jsdom pega isso, porque lá
|
|
158
|
+
// não há layout para medir.
|
|
159
|
+
//
|
|
160
|
+
// A lista interna preserva `measured` quando os dados de fora mudam: sem isso, trocar a
|
|
161
|
+
// seleção jogaria a medida fora e o grafo piscaria a cada clique.
|
|
162
|
+
const [nosVivos, setNosVivos] = React.useState(nosDoMotor);
|
|
163
|
+
React.useEffect(() => {
|
|
164
|
+
setNosVivos(anteriores => {
|
|
165
|
+
const medidos = new Map(anteriores.map(n => [n.id, n.measured]));
|
|
166
|
+
return nosDoMotor.map(n => ({ ...n, measured: medidos.get(n.id) }));
|
|
167
|
+
});
|
|
168
|
+
}, [nosDoMotor]);
|
|
169
|
+
const aoMudarNos = React.useCallback((mudancas) => {
|
|
170
|
+
setNosVivos(anteriores => applyNodeChanges(mudancas, anteriores));
|
|
171
|
+
}, []);
|
|
172
|
+
// `initialWidth`/`initialHeight` no provider é o par do de cima, um nível acima: é o viewport
|
|
173
|
+
// que o `fitView` usa ENQUANTO ninguém mediu. Com o prefixo `initial`, o navegador sobrescreve
|
|
174
|
+
// na primeira medição — passar `width`/`height` direto no ReactFlow fixaria o tamanho e mataria
|
|
175
|
+
// a fluidez. O valor é a extensão do próprio layout, então no servidor o grafo cabe exato.
|
|
176
|
+
return _jsx("div", { className: cx("dependency-graph", className), style: height ? { blockSize: height } : undefined, role: "group", "aria-label": label ?? s.graphLabel, ...props, children: _jsx(ReactFlowProvider, { initialNodes: nosDoMotor, initialEdges: arestasDoMotor, fitView: true, initialWidth: automatico.extensao.larg, initialHeight: automatico.extensao.alt, children: _jsx(ReactFlow, { nodes: nosVivos, onNodesChange: aoMudarNos, edges: arestasDoMotor, nodeTypes: TIPOS, fitView: true, proOptions: { hideAttribution: false }, nodesDraggable: !!onNodeMove, nodesConnectable: !!connectable, elementsSelectable: !!onSelect, onNodeDragStop: onNodeMove ? (_, no) => onNodeMove(no.id, no.position) : undefined, onConnect: onConnect ? c => { if (c.source && c.target)
|
|
177
|
+
onConnect({ from: c.source, to: c.target }); } : undefined, children: _jsx(Background, {}) }) }) });
|
|
178
|
+
}
|
package/dist/identity.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
export type AvatarSize = "sm" | "md" | "lg";
|
|
2
3
|
export declare function Avatar({ src, alt, fallback, size, className }: {
|
|
3
4
|
src?: string;
|
|
4
5
|
alt?: string;
|
|
5
6
|
fallback?: ReactNode;
|
|
6
|
-
size?:
|
|
7
|
+
size?: AvatarSize;
|
|
7
8
|
className?: string;
|
|
8
|
-
}):
|
|
9
|
+
}): React.JSX.Element;
|
|
10
|
+
export declare function AvatarGroup({ children, max, total, label, size, className }: {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
max?: number;
|
|
13
|
+
total?: number;
|
|
14
|
+
label?: string;
|
|
15
|
+
size?: AvatarSize;
|
|
16
|
+
className?: string;
|
|
17
|
+
}): React.JSX.Element;
|
package/dist/identity.js
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
import { 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";
|
|
2
7
|
import { cx } from "./internal.js";
|
|
3
|
-
export function Avatar({ src, alt = "", fallback, size =
|
|
8
|
+
export function Avatar({ src, alt = "", fallback, size = "md", className }) {
|
|
9
|
+
const [falhou, setFalhou] = React.useState(false);
|
|
10
|
+
React.useEffect(() => { setFalhou(false); }, [src]);
|
|
11
|
+
return _jsx("span", { className: cx("avatar", size !== "md" && `avatar-${size}`, className), children: src && !falhou ? _jsx("img", { src: src, alt: alt, onError: () => setFalhou(true) }) : fallback });
|
|
12
|
+
}
|
|
13
|
+
// AvatarGroup: a pilha sobreposta com o "+N" no fim. A sobreposição já existia no core desde
|
|
14
|
+
// sempre (`.avatar-group .avatar`), e o container nunca teve regra — a peça filha ficou, a mãe
|
|
15
|
+
// não, exatamente como no Stepper.
|
|
16
|
+
// `max` corta a lista e o excedente vira UM avatar de contagem, que é onde as referências
|
|
17
|
+
// convergem. Fora de propósito: o `renderSurplus` de uma delas (callback para desenhar o "+N"),
|
|
18
|
+
// que é configuração para um problema que ninguém teve ainda.
|
|
19
|
+
// `role="group"` + nome: uma fila de retratos sem nome não diz de quem é.
|
|
20
|
+
export function AvatarGroup({ children, max, total, label, size = "md", className }) {
|
|
21
|
+
const itens = React.Children.toArray(children);
|
|
22
|
+
const mostra = max ? itens.slice(0, max) : itens;
|
|
23
|
+
const resto = (total ?? itens.length) - mostra.length;
|
|
24
|
+
return _jsxs("div", { role: "group", "aria-label": label, className: cx("avatar-group", className), children: [mostra, resto > 0 && _jsxs("span", { className: cx("avatar", "avatar-count", size !== "md" && `avatar-${size}`), "aria-hidden": "true", children: ["+", resto] })] });
|
|
25
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { cx, defaultStrings, ptBR,
|
|
1
|
+
export { cx, defaultStrings, ptBR, defaultSpriteUrl, gridStateToParams, gridStateFromParams, universalStates, stateSeverity, type AureaStrings, type GridState, type UniversalState } from "./pure.js";
|
|
2
|
+
export { useAureaStrings, useSpriteUrl } from "./internal.js";
|
|
2
3
|
export * from "./system.js";
|
|
3
4
|
export * from "./actions.js";
|
|
4
5
|
export * from "./feedback.js";
|
|
@@ -12,3 +13,4 @@ export * from "./overlays.js";
|
|
|
12
13
|
export * from "./media.js";
|
|
13
14
|
export * from "./communication.js";
|
|
14
15
|
export * from "./code.js";
|
|
16
|
+
export * from "./agents.js";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,19 @@
|
|
|
14
14
|
// QRCode → "@aurea-uds/react/qrcode" (qr)
|
|
15
15
|
// Se estivessem neste barril, importar um Button carregaria os três — e a dependência opcional
|
|
16
16
|
// que não estivesse instalada quebraria a importação. O peso é real, então a fronteira é real.
|
|
17
|
-
|
|
17
|
+
// SEM `"use client"`, e é o ponto da Parte A: este barril é um módulo de SERVIDOR. Reexportar
|
|
18
|
+
// componente de um módulo com a diretiva é o caminho suportado — quem importa daqui de um
|
|
19
|
+
// componente de servidor recebe uma referência de cliente e a renderiza normalmente.
|
|
20
|
+
//
|
|
21
|
+
// `cx` e os dicionários vêm do `pure.js`, não do `internal.js`: os dois exportam os mesmos
|
|
22
|
+
// nomes, mas o `internal` tem a diretiva, e dali `cx` viraria referência de cliente — chamável
|
|
23
|
+
// só no navegador. Um helper de classe tem de funcionar nos dois lados.
|
|
24
|
+
// `gridStateToParams`/`gridStateFromParams` saem por AQUI, e não pelo subpath do
|
|
25
|
+
// DataGrid, de propósito: quem lê a URL num framework de RSC é o SERVIDOR, e o
|
|
26
|
+
// subpath tem a diretiva. Vindo daqui — via `pure.js`, que não tem — a função é
|
|
27
|
+
// chamável dos dois lados. É a mesma razão do `cx`, uma camada acima.
|
|
28
|
+
export { cx, defaultStrings, ptBR, defaultSpriteUrl, gridStateToParams, gridStateFromParams, universalStates, stateSeverity } from "./pure.js";
|
|
29
|
+
export { useAureaStrings, useSpriteUrl } from "./internal.js";
|
|
18
30
|
export * from "./system.js";
|
|
19
31
|
export * from "./actions.js";
|
|
20
32
|
export * from "./feedback.js";
|
|
@@ -28,3 +40,5 @@ export * from "./overlays.js";
|
|
|
28
40
|
export * from "./media.js";
|
|
29
41
|
export * from "./communication.js";
|
|
30
42
|
export * from "./code.js";
|
|
43
|
+
// A camada operacional (Parte H). Sem dependência nova, então entra no barril.
|
|
44
|
+
export * from "./agents.js";
|