@aurea-uds/react 0.1.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 +20 -0
- package/dist/actions.js +59 -5
- 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-client.d.ts +6 -0
- package/dist/code-client.js +24 -0
- package/dist/code-editor.js +1 -0
- package/dist/code.d.ts +2 -13
- package/dist/code.js +5 -18
- package/dist/communication.js +1 -0
- 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 -13
- package/dist/data-grid.d.ts +52 -3
- package/dist/data-grid.js +205 -25
- package/dist/feedback-client.d.ts +64 -0
- package/dist/feedback-client.js +110 -0
- package/dist/feedback.d.ts +2 -53
- package/dist/feedback.js +5 -71
- package/dist/file-input.d.ts +21 -2
- package/dist/file-input.js +202 -21
- package/dist/graph.d.ts +33 -0
- package/dist/graph.js +178 -0
- package/dist/identity-client.d.ts +9 -0
- package/dist/identity-client.js +12 -0
- package/dist/identity.d.ts +2 -8
- package/dist/identity.js +5 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +33 -3
- package/dist/inputs-client.d.ts +105 -0
- package/dist/inputs-client.js +264 -0
- package/dist/inputs.d.ts +2 -65
- package/dist/inputs.js +9 -61
- package/dist/internal.d.ts +23 -70
- package/dist/internal.js +115 -23
- package/dist/layout-client.d.ts +9 -0
- package/dist/layout-client.js +72 -0
- package/dist/layout.d.ts +2 -14
- package/dist/layout.js +5 -22
- 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 -98
- package/dist/navigation-client.d.ts +94 -0
- package/dist/navigation-client.js +154 -0
- package/dist/navigation.d.ts +2 -59
- package/dist/navigation.js +5 -113
- package/dist/overlays.d.ts +28 -0
- package/dist/overlays.js +66 -10
- package/dist/pure.d.ts +196 -0
- package/dist/pure.js +111 -0
- package/dist/qrcode.d.ts +2 -1
- package/dist/qrcode.js +3 -2
- package/dist/system.d.ts +11 -1
- package/dist/system.js +59 -4
- package/package.json +40 -4
package/dist/feedback.js
CHANGED
|
@@ -1,71 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { Popover as BasePopover } from "@base-ui/react/popover";
|
|
7
|
-
import { cx, useAureaStrings } from "./internal.js";
|
|
8
|
-
import { Icon } from "./system.js";
|
|
9
|
-
import { Button, IconButton } from "./actions.js";
|
|
10
|
-
export function Badge({ variant = "neutral", className, ...props }) { return _jsx("span", { className: cx("badge", variant !== "neutral" && `badge-${variant}`, className), ...props }); }
|
|
11
|
-
// Status (DIRECTION §3.6): condição OPERACIONAL — ponto + rótulo. Não é Badge: Badge é
|
|
12
|
-
// metadado curto num pill; Status diz em que estado a coisa está. Reusa o .status-dot que
|
|
13
|
-
// já existia solto. A variante colore só o PONTO (currentColor); o rótulo fica legível em
|
|
14
|
-
// --foreground. Cor não é o único sinal — quem diz o estado é o texto (WCAG 1.4.1); o
|
|
15
|
-
// ponto é decorativo e sai do leitor de tela. ponytail: rótulo é do consumidor (sem i18n
|
|
16
|
-
// nova) — a variante é só a cor.
|
|
17
|
-
export function Status({ variant = "neutral", children, className, ...props }) { return _jsxs("span", { className: cx("status", variant !== "neutral" && `status-${variant}`, className), ...props, children: [_jsx("i", { className: "status-dot", "aria-hidden": "true" }), _jsx("span", { className: "status-label", children: children })] }); }
|
|
18
|
-
export function Alert({ variant = "info", title, children, className, ...props }) { return _jsxs("div", { className: cx("alert", `alert-${variant}`, className), role: variant === "danger" ? "alert" : "status", ...props, children: [title && _jsx("strong", { children: title }), children] }); }
|
|
19
|
-
export function Banner({ variant = "info", title, icon, onDismiss, children, className, ...props }) { const s = useAureaStrings(); return _jsxs("div", { className: cx("banner", `banner-${variant}`, className), role: variant === "danger" ? "alert" : "status", ...props, children: [icon ? _jsx(Icon, { name: icon }) : _jsx("span", {}), _jsxs("div", { children: [title && _jsx("strong", { children: title }), children] }), onDismiss ? _jsx(IconButton, { variant: "ghost", size: "sm", icon: "close", label: s.close, onClick: onDismiss }) : _jsx("span", {})] }); }
|
|
20
|
-
export function Progress({ value, label }) { return _jsx("div", { children: _jsx("div", { className: "progress", role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": value, children: _jsx("span", { style: { width: `${Math.max(0, Math.min(100, value))}%` } }) }) }); }
|
|
21
|
-
export function Skeleton({ className, ...props }) { return _jsx("div", { className: cx("skeleton", className), "aria-hidden": "true", ...props }); }
|
|
22
|
-
// titleAs: o nível do título é do DOCUMENTO, não do componente. Fixo em h3, um empty state
|
|
23
|
-
// no alto de uma página vira h1→h3 e a hierarquia quebra (axe heading-order). Default h3
|
|
24
|
-
// para não mexer em quem já consome; quem sabe o contexto passa o nível certo.
|
|
25
|
-
export function EmptyState({ icon = "document--blank", title, titleAs: TitleTag = "h3", description, action }) { return _jsxs("div", { className: "empty-state", children: [_jsx(Icon, { name: icon, size: "xl" }), _jsx(TitleTag, { className: "empty-title", children: title }), description && _jsx("p", { className: "muted", children: description }), action] }); }
|
|
26
|
-
function groupNotifications(items) {
|
|
27
|
-
const out = [];
|
|
28
|
-
for (const it of items) {
|
|
29
|
-
const last = out[out.length - 1];
|
|
30
|
-
if (last && last.label === it.group)
|
|
31
|
-
last.items.push(it);
|
|
32
|
-
else
|
|
33
|
-
out.push({ label: it.group, items: [it] });
|
|
34
|
-
}
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
export function NotificationCenter({ items, onItemClick, onMarkAllRead, label, icon = "notification", side = "bottom" }) {
|
|
38
|
-
const s = useAureaStrings();
|
|
39
|
-
const title = label ?? s.notificationsLabel;
|
|
40
|
-
const unread = items.filter(i => !i.read).length;
|
|
41
|
-
const groups = groupNotifications(items);
|
|
42
|
-
const baseId = React.useId();
|
|
43
|
-
const seen = React.useRef(undefined);
|
|
44
|
-
const [announce, setAnnounce] = React.useState("");
|
|
45
|
-
React.useEffect(() => {
|
|
46
|
-
const ids = new Set(items.map(i => i.id));
|
|
47
|
-
if (seen.current === undefined) {
|
|
48
|
-
seen.current = ids;
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
const fresh = items.filter(i => !seen.current.has(i.id)).length;
|
|
52
|
-
seen.current = ids;
|
|
53
|
-
// Texto idêntico duas vezes seguidas não muta o DOM e o leitor silencia a 2ª
|
|
54
|
-
// chegada (auditoria 18/07/2026, MÉDIO 4). Um NBSP alternado no fim força a
|
|
55
|
-
// mutação sem mudar o que se ouve.
|
|
56
|
-
if (fresh)
|
|
57
|
-
setAnnounce(prev => { const text = `${fresh} ${s.notificationNew}`; return prev === text ? text + " " : text; });
|
|
58
|
-
}, [items, s.notificationNew]);
|
|
59
|
-
const renderRow = (it) => {
|
|
60
|
-
const body = _jsxs(_Fragment, { children: [_jsx("span", { className: "notification-dot", "aria-hidden": "true" }), _jsxs("span", { className: "notification-item-title", children: [it.icon && _jsx(Icon, { name: it.icon, size: "sm" }), !it.read && _jsxs("span", { className: "sr-only", children: [s.notificationUnread, " "] }), it.title] }), it.time && _jsx("span", { className: "notification-time", children: it.time }), it.description && _jsx("span", { className: "notification-item-desc", children: it.description })] });
|
|
61
|
-
return it.onClick || onItemClick
|
|
62
|
-
? _jsx("button", { type: "button", className: "notification-item", "data-read": it.read || undefined, onClick: () => { it.onClick?.(); onItemClick?.(it); }, children: body })
|
|
63
|
-
: _jsx("div", { className: "notification-item", "data-read": it.read || undefined, children: body });
|
|
64
|
-
};
|
|
65
|
-
return _jsxs(BasePopover.Root, { children: [_jsxs("span", { className: "notification-trigger", children: [_jsx(BasePopover.Trigger, { render: _jsx(IconButton, { variant: "ghost", icon: icon, label: unread ? `${title} (${unread})` : title }) }), unread > 0 && _jsx("span", { className: "notification-count", "aria-hidden": "true", children: unread > 99 ? "99+" : unread })] }), _jsx(BasePopover.Portal, { children: _jsx(BasePopover.Positioner, { side: side, sideOffset: 8, children: _jsxs(BasePopover.Popup, { className: "popover notification-panel", "aria-label": title, children: [_jsxs("div", { className: "notification-head", children: [_jsx(BasePopover.Title, { render: _jsx("strong", {}), children: title }), unread > 0 && onMarkAllRead && _jsx(Button, { variant: "ghost", size: "sm", onClick: onMarkAllRead, children: s.notificationMarkAll })] }), items.length
|
|
66
|
-
? _jsx("div", { className: "notification-list", children: groups.map((g, gi) => {
|
|
67
|
-
const gid = baseId + gi;
|
|
68
|
-
return _jsxs(React.Fragment, { children: [g.label && _jsx("p", { className: "notification-group-label", id: gid, children: g.label }), _jsx("ul", { className: "notification-sublist", "aria-labelledby": g.label ? gid : undefined, children: g.items.map(it => _jsx("li", { children: renderRow(it) }, it.id)) })] }, gi);
|
|
69
|
-
}) })
|
|
70
|
-
: _jsx("p", { className: "notification-empty", children: s.notificationEmpty })] }) }) }), _jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", children: announce })] });
|
|
71
|
-
}
|
|
1
|
+
// VITRINE da categoria — é este arquivo que `@aurea-uds/react/feedback` resolve. Sem
|
|
2
|
+
// `"use client"` de propósito: a diretiva contamina o módulo inteiro e faria a marcação pura
|
|
3
|
+
// chegar como cliente por vizinhança (ADR-0026; o check 26b reprova quem a puser de volta aqui).
|
|
4
|
+
export * from "./feedback-client.js";
|
|
5
|
+
export { Badge, Progress, Skeleton } from "./markup.js";
|
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,47 @@ 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
|
+
}
|
|
45
|
+
// AUD-0006 (12/08/2026): um item restaurado NÃO tem os bytes — a plataforma não devolve `File` de
|
|
46
|
+
// um armazenamento —, então ele não está subindo e não vai subir. Persistido como
|
|
47
|
+
// `pending`/`uploading`/`paused`, ele mostrava Pausar/Retomar/Cancelar INERTES (não há controller
|
|
48
|
+
// em memória para abortar, e `retomar` exige `e.file`) e contava como "em voo", o que travava o
|
|
49
|
+
// recibo para sempre: `emVoo` nunca zerava.
|
|
50
|
+
//
|
|
51
|
+
// `canceled` é o único terminal HONESTO aqui. A transferência foi de fato abortada — pelo fim da
|
|
52
|
+
// página —, e não houve falha de servidor que justificasse `error`. `pending` seria mentira dupla:
|
|
53
|
+
// não está esperando nada e continuaria contando como em voo.
|
|
54
|
+
//
|
|
55
|
+
// E o ganho é que a marcação certa sai de graça: o ramo de `canceled` já esconde o botão de
|
|
56
|
+
// repetir quando não há `File` (era uma decisão do item G1) e já oferece só a remoção. Zero string
|
|
57
|
+
// nova, zero branch novo.
|
|
58
|
+
const ATIVOS = new Set(["pending", "uploading", "paused"]);
|
|
59
|
+
const restaurar = (q) => ({ ...q, restored: true, status: ATIVOS.has(q.status) ? "canceled" : q.status });
|
|
18
60
|
function formatSize(bytes) {
|
|
19
61
|
if (bytes < 1024)
|
|
20
62
|
return `${bytes} B`;
|
|
@@ -26,43 +68,114 @@ function formatSize(bytes) {
|
|
|
26
68
|
}
|
|
27
69
|
return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
|
|
28
70
|
}
|
|
29
|
-
export function FileInput({ accept, maxSize, multiple, onFilesChange, upload, label, hint, id, className }) {
|
|
71
|
+
export function FileInput({ accept, maxSize, multiple, onFilesChange, upload, label, hint, id, className, initialQueue, onQueueChange, preview, checksum }) {
|
|
30
72
|
const s = useAureaStrings();
|
|
31
73
|
const autoId = React.useId();
|
|
32
74
|
const inputId = id ?? autoId;
|
|
75
|
+
// G1: a fila restaurada entra como estado INICIAL, não como prop controlada — e a
|
|
76
|
+
// palavra "inicial" é literal: um item restaurado não tem `File`, então controlá-lo
|
|
77
|
+
// de fora seria prometer um round-trip que a plataforma não faz.
|
|
78
|
+
// AUD-0002 (12/08/2026): o contador SEMPRE começava em zero, e os ids restaurados vêm do
|
|
79
|
+
// consumidor — inclusive `f0`, que é exatamente o que ele gera. Restaurar `f0` e escolher um
|
|
80
|
+
// arquivo novo dava `id: "f0"` aos DOIS: remover um removia os dois (`filter` por id), `patch`
|
|
81
|
+
// atualizava os dois, e o React avisava chave duplicada. Perda de registro no fluxo oficial de
|
|
82
|
+
// persistência.
|
|
83
|
+
// O conjunto dos ids em uso faz o gerador PULAR, e por ser conjunto ele vale para id fora do
|
|
84
|
+
// padrão também — semear o contador com "o maior sufixo numérico" só resolveria `f<N>`.
|
|
33
85
|
const nextId = React.useRef(0);
|
|
34
|
-
const
|
|
86
|
+
const usados = React.useRef(null);
|
|
87
|
+
if (!usados.current)
|
|
88
|
+
usados.current = new Set((initialQueue ?? []).map(q => q.id));
|
|
89
|
+
const novoId = () => { let novo; do {
|
|
90
|
+
novo = `f${nextId.current++}`;
|
|
91
|
+
} while (usados.current.has(novo)); usados.current.add(novo); return novo; };
|
|
92
|
+
const [files, setFiles] = React.useState(() => (initialQueue ?? []).map(restaurar));
|
|
35
93
|
const [rejected, setRejected] = React.useState([]);
|
|
94
|
+
// a soma chega DEPOIS do envio começar (é assíncrona), então a comparação lê a
|
|
95
|
+
// lista atual por ref em vez da closure do início do upload.
|
|
96
|
+
// semeada com a MESMA lista inicial do estado, e não com `[]`: com fila restaurada as duas
|
|
97
|
+
// divergiam até a primeira emissão, e é desta ref que o AUD-0009 abaixo lê a lista anterior.
|
|
98
|
+
const filesRef = React.useRef(files);
|
|
99
|
+
// E guarda a PROMESSA, não só o valor: um arquivo pequeno com envio rápido termina
|
|
100
|
+
// antes de o hash sair, e comparar contra `undefined` pularia a conferência em
|
|
101
|
+
// silêncio — que é pior que não conferir, porque parece que conferiu. Quem achou
|
|
102
|
+
// isso foi o teste, não a leitura.
|
|
103
|
+
const somas = React.useRef(new Map());
|
|
36
104
|
const [announce, setAnnounce] = React.useState("");
|
|
105
|
+
// G5: conflito NÃO é rejeição — o arquivo é válido, só o nome já está na fila.
|
|
106
|
+
// Fica em espera com a escolha na tela, porque sobrescrever em silêncio é o
|
|
107
|
+
// comportamento que o item existe para proibir.
|
|
108
|
+
const [conflitos, setConflitos] = React.useState([]);
|
|
37
109
|
const [dragging, setDragging] = React.useState(false);
|
|
38
110
|
const controllers = React.useRef(new Map());
|
|
39
111
|
// Desmontar aborta tudo em voo — sem isso requisições e closures sobrevivem à
|
|
40
112
|
// tela (auditoria 18/07/2026, MÉDIO 5). Concorrência/agenda é do consumidor: o
|
|
41
113
|
// transporte é dele (UploadFn); o componente dispara um upload() por arquivo aceito.
|
|
42
114
|
React.useEffect(() => { const map = controllers.current; return () => map.forEach(c => c.abort()); }, []);
|
|
43
|
-
|
|
44
|
-
|
|
115
|
+
// As URLs de objeto vivas moram numa ref para poderem ser revogadas no desmonte —
|
|
116
|
+
// ler `files` de dentro do cleanup pegaria a lista do primeiro render.
|
|
117
|
+
const previas = React.useRef(new Set());
|
|
118
|
+
React.useEffect(() => { const set = previas.current; return () => set.forEach(u => URL.revokeObjectURL(u)); }, []);
|
|
119
|
+
// G1: toda mudança da fila avisa por fora, em forma serializável. É o que o
|
|
120
|
+
// consumidor guarda; onde ele guarda — armazenamento local, servidor, nada — é
|
|
121
|
+
// decisão dele, a mesma linha do estado da grade na URL (PLANO-1.0 F4).
|
|
122
|
+
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 } : {}) }));
|
|
123
|
+
// AUD-0009 (12/08/2026): a lista PERDE itens por dois caminhos — a troca em modo single e o
|
|
124
|
+
// "substituir" do conflito —, e a URL de objeto da prévia deles ficava viva até o desmonte. O
|
|
125
|
+
// navegador segura o blob inteiro nesse tempo, então trocar a mesma foto dez vezes acumulava dez
|
|
126
|
+
// imagens em memória com uma na tela.
|
|
127
|
+
// Revogado AQUI, num lugar só, porque `emit` é por onde toda substituição passa. Corrigir em
|
|
128
|
+
// cada chamador é exatamente o que a regra "quem MAIS tem esse problema?" existe para impedir —
|
|
129
|
+
// e por isso o `remove` deixou de revogar por conta própria: ele passa por aqui.
|
|
130
|
+
const emit = (next) => {
|
|
131
|
+
const idsNovos = new Set(next.map(e => e.id));
|
|
132
|
+
for (const e of filesRef.current)
|
|
133
|
+
if (e.preview && !idsNovos.has(e.id)) {
|
|
134
|
+
URL.revokeObjectURL(e.preview);
|
|
135
|
+
previas.current.delete(e.preview);
|
|
136
|
+
}
|
|
137
|
+
filesRef.current = next;
|
|
138
|
+
setFiles(next);
|
|
139
|
+
onFilesChange?.(next.flatMap(f => f.file ? [f.file] : []));
|
|
140
|
+
onQueueChange?.(paraFila(next));
|
|
141
|
+
};
|
|
142
|
+
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
143
|
const abortId = (fid) => controllers.current.get(fid)?.abort();
|
|
46
144
|
// Cada arquivo envia por conta própria (promessa independente + catch por arquivo):
|
|
47
145
|
// erro ou cancelamento de um não derruba a fila. Cancelar = abort() no controller;
|
|
48
146
|
// no catch, signal.aborted distingue "cancelado" de "falhou" seja qual for o erro
|
|
49
147
|
// que o consumidor lançou. patch é setState funcional → seguro sob closures velhas.
|
|
50
|
-
|
|
148
|
+
// G2: pausar HTTP não é pausar — é abortar. Então `pausando` marca a INTENÇÃO antes
|
|
149
|
+
// do abort, e o catch a lê para separar "pausei" de "cancelei": os dois chegam como
|
|
150
|
+
// `signal.aborted`, e tratá-los igual perderia o progresso e o direito de retomar.
|
|
151
|
+
const pausando = React.useRef(new Set());
|
|
152
|
+
const startUpload = (fid, file, resumeFrom = 0) => {
|
|
51
153
|
if (!upload)
|
|
52
154
|
return;
|
|
53
155
|
const c = new AbortController();
|
|
54
156
|
controllers.current.set(fid, c);
|
|
55
|
-
patch(fid, { status: "uploading", progress:
|
|
157
|
+
patch(fid, { status: "uploading", progress: resumeFrom });
|
|
56
158
|
(async () => {
|
|
57
159
|
try {
|
|
58
|
-
await upload(file, { signal: c.signal, onProgress: f => patch(fid, { progress: Math.max(0, Math.min(1, f)) }) });
|
|
59
|
-
|
|
160
|
+
const devolvido = await upload(file, { signal: c.signal, resumeFrom, onProgress: f => patch(fid, { progress: Math.max(0, Math.min(1, f)) }) });
|
|
161
|
+
// G4: conferir é comparar. Se o servidor devolveu uma soma e ela difere da
|
|
162
|
+
// nossa, o arquivo que chegou lá NÃO é o que saiu daqui — e isso é erro, não
|
|
163
|
+
// detalhe. Sem soma devolvida não há o que conferir, e o envio segue válido.
|
|
164
|
+
const local = await (somas.current.get(fid) ?? Promise.resolve(filesRef.current.find(e => e.id === fid)?.checksum));
|
|
165
|
+
const doServidor = devolvido && typeof devolvido === "object" ? devolvido.checksum : undefined;
|
|
166
|
+
if (doServidor && local && doServidor.toLowerCase() !== local.toLowerCase()) {
|
|
167
|
+
patch(fid, { status: "error", erro: s.uploadChecksumBad, finishedAt: agora() });
|
|
168
|
+
setAnnounce(`${s.uploadChecksumBad}: ${file.name}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
patch(fid, { status: "done", progress: 1, finishedAt: agora() });
|
|
60
172
|
setAnnounce(`${s.uploadComplete}: ${file.name}`);
|
|
61
173
|
}
|
|
62
174
|
catch {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
175
|
+
const foiPausa = pausando.current.delete(fid);
|
|
176
|
+
const st = foiPausa ? "paused" : c.signal.aborted ? "canceled" : "error";
|
|
177
|
+
patch(fid, { status: st, ...(st === "paused" ? {} : { finishedAt: agora() }) });
|
|
178
|
+
setAnnounce(`${st === "paused" ? s.uploadPaused : st === "canceled" ? s.uploadCanceled : s.uploadError}: ${file.name}`);
|
|
66
179
|
}
|
|
67
180
|
finally {
|
|
68
181
|
controllers.current.delete(fid);
|
|
@@ -72,14 +185,22 @@ export function FileInput({ accept, maxSize, multiple, onFilesChange, upload, la
|
|
|
72
185
|
const add = (list) => {
|
|
73
186
|
if (!list || !list.length)
|
|
74
187
|
return;
|
|
75
|
-
const ok = [], bad = [];
|
|
188
|
+
const ok = [], bad = [], novosConflitos = [];
|
|
76
189
|
for (const file of Array.from(list)) {
|
|
77
190
|
const reason = !matchesAccept(file, accept) ? s.fileWrongType : (maxSize != null && file.size > maxSize) ? s.fileTooLarge : null;
|
|
78
|
-
if (reason)
|
|
191
|
+
if (reason) {
|
|
79
192
|
bad.push({ file, reason });
|
|
80
|
-
|
|
81
|
-
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const jaTem = multiple ? files.find(e => e.name === file.name) : undefined;
|
|
196
|
+
if (jaTem) {
|
|
197
|
+
novosConflitos.push({ file, existente: jaTem.id });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
ok.push({ id: novoId(), file, name: file.name, bytes: file.size, status: "pending", progress: 0 });
|
|
82
201
|
}
|
|
202
|
+
if (novosConflitos.length)
|
|
203
|
+
setConflitos(c => [...c, ...novosConflitos]);
|
|
83
204
|
// multiple acumula; single substitui (input nativo já entrega 1 arquivo).
|
|
84
205
|
// só emite se algo passou — drop 100% rejeitado não mexe na lista atual.
|
|
85
206
|
const added = multiple ? ok : ok.slice(-1);
|
|
@@ -87,20 +208,80 @@ export function FileInput({ accept, maxSize, multiple, onFilesChange, upload, la
|
|
|
87
208
|
if (!multiple)
|
|
88
209
|
files.forEach(f => abortId(f.id)); // single troca o arquivo: aborta o envio anterior
|
|
89
210
|
emit(multiple ? [...files, ...added] : added);
|
|
211
|
+
if (preview)
|
|
212
|
+
added.forEach(e => { if (e.file && ehImagem(e.file)) {
|
|
213
|
+
const u = URL.createObjectURL(e.file);
|
|
214
|
+
previas.current.add(u);
|
|
215
|
+
patch(e.id, { preview: u });
|
|
216
|
+
} });
|
|
217
|
+
if (checksum)
|
|
218
|
+
added.forEach(e => { if (e.file)
|
|
219
|
+
somas.current.set(e.id, somaSha256(e.file).then(h => { if (h)
|
|
220
|
+
patch(e.id, { checksum: h }); return h; })); });
|
|
90
221
|
added.forEach(e => startUpload(e.id, e.file));
|
|
91
222
|
}
|
|
92
223
|
setRejected(bad);
|
|
93
|
-
setAnnounce([ok.length && `${s.fileAdded}: ${ok.map(f => f.
|
|
224
|
+
setAnnounce([ok.length && `${s.fileAdded}: ${ok.map(f => f.name).join(", ")}`, ...bad.map(b => `${b.reason}: ${b.file.name}`)].filter(Boolean).join(". "));
|
|
225
|
+
};
|
|
226
|
+
// G2: pausar é abortar com intenção declarada; retomar é chamar de novo, passando
|
|
227
|
+
// o quanto já subiu. Por ITEM e no CONJUNTO — e o conjunto é só o laço, porque a
|
|
228
|
+
// regra tem de ser a mesma nos dois: senão "pausar tudo" e "pausar cada um" acabam
|
|
229
|
+
// em estados diferentes, que é o defeito clássico deste tipo de barra.
|
|
230
|
+
const pausar = (fid) => { pausando.current.add(fid); abortId(fid); };
|
|
231
|
+
const retomar = (e) => { if (e.file)
|
|
232
|
+
startUpload(e.id, e.file, e.progress); };
|
|
233
|
+
const pausarTudo = () => files.filter(f => f.status === "uploading").forEach(f => pausar(f.id));
|
|
234
|
+
const retomarTudo = () => files.filter(f => f.status === "paused").forEach(retomar);
|
|
235
|
+
const enviando = files.some(f => f.status === "uploading"), pausados = files.some(f => f.status === "paused");
|
|
236
|
+
// As três saídas do conflito, e nenhuma é silenciosa. "Manter os dois" não renomeia
|
|
237
|
+
// nada: os ids é que distinguem, e inventar "arquivo (1).txt" seria decidir pelo
|
|
238
|
+
// consumidor um nome que o servidor dele talvez não aceite.
|
|
239
|
+
// A lista base vem por ARGUMENTO, não da closure: em "substituir" o item removido
|
|
240
|
+
// ainda está em `files` quando esta função roda — o React não re-renderizou — e ler
|
|
241
|
+
// a closure deixava os DOIS na fila. Foi o teste que achou.
|
|
242
|
+
const aceitarConflito = (file, base = files) => {
|
|
243
|
+
const e = { id: novoId(), file, name: file.name, bytes: file.size, status: "pending", progress: 0 };
|
|
244
|
+
emit([...base, e]);
|
|
245
|
+
if (preview && ehImagem(file)) {
|
|
246
|
+
const u = URL.createObjectURL(file);
|
|
247
|
+
previas.current.add(u);
|
|
248
|
+
patch(e.id, { preview: u });
|
|
249
|
+
}
|
|
250
|
+
if (checksum)
|
|
251
|
+
somas.current.set(e.id, somaSha256(file).then(h => { if (h)
|
|
252
|
+
patch(e.id, { checksum: h }); return h; }));
|
|
253
|
+
startUpload(e.id, file);
|
|
254
|
+
};
|
|
255
|
+
const resolverConflito = (i, escolha) => {
|
|
256
|
+
const c = conflitos[i];
|
|
257
|
+
setConflitos(l => l.filter((_, n) => n !== i));
|
|
258
|
+
if (escolha === "skip")
|
|
259
|
+
return;
|
|
260
|
+
if (escolha === "replace") {
|
|
261
|
+
abortId(c.existente);
|
|
262
|
+
aceitarConflito(c.file, files.filter(x => x.id !== c.existente));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
aceitarConflito(c.file);
|
|
94
266
|
};
|
|
95
|
-
|
|
267
|
+
// a revogação da prévia saiu daqui: quem revoga é o `emit`, para todo item que a lista perder.
|
|
268
|
+
// Duplicar a regra em cada chamador é como o AUD-0009 nasceu — o `remove` revogava e os outros
|
|
269
|
+
// dois caminhos de substituição, não.
|
|
270
|
+
const remove = (rid) => { const f = files.find(x => x.id === rid); abortId(rid); emit(files.filter(x => x.id !== rid)); setAnnounce(f ? `${s.fileRemoved}: ${f.name}` : ""); };
|
|
271
|
+
// G6: "terminou" é não haver mais nada em voo nem esperando — pausado conta como em
|
|
272
|
+
// voo, porque a pessoa ainda vai retomar.
|
|
273
|
+
const emVoo = files.some(f => f.status === "uploading" || f.status === "paused" || f.status === "pending");
|
|
274
|
+
const recibo = files.filter(f => f.status === "done" || f.status === "error" || f.status === "canceled");
|
|
275
|
+
const reciboTexto = recibo.map(r => [r.name, r.status, r.finishedAt ?? ""].join(TAB)).join(NL);
|
|
96
276
|
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 })] }),
|
|
277
|
+
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 })
|
|
278
|
+
: _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
279
|
// A barra reusa .progress + .file-progress (largura fixa em classe); só o
|
|
99
280
|
// preenchimento (width %) fica inline, por ser data-driven. Erro reusa
|
|
100
281
|
// .field-error. Como .file-progress não é usada nos docs, adicioná-la ao core
|
|
101
282
|
// não muda pixel de screenshot nenhum (auditoria 18/07, M14: CSS não usado
|
|
102
283
|
// pelos docs não altera baseline — a justificativa antiga estava invertida).
|
|
103
284
|
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 })] });
|
|
285
|
+
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);
|
|
286
|
+
}) }), 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
287
|
}
|
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;
|