@thecodecodex/design-system 0.1.2 → 0.2.1

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.
Files changed (59) hide show
  1. package/README.md +53 -2
  2. package/dist/components/AppShell.d.ts +32 -0
  3. package/dist/components/AppShell.js +28 -0
  4. package/dist/components/Button.d.ts +10 -0
  5. package/dist/components/Button.js +20 -0
  6. package/dist/components/ChipToggles.d.ts +11 -0
  7. package/dist/components/ChipToggles.js +9 -0
  8. package/dist/components/ConfirmDialog.d.ts +14 -0
  9. package/dist/components/ConfirmDialog.js +10 -0
  10. package/dist/components/CopyButton.d.ts +10 -0
  11. package/dist/components/CopyButton.js +12 -0
  12. package/dist/components/DataGrid.d.ts +15 -0
  13. package/dist/components/DataGrid.js +54 -0
  14. package/dist/components/Dialog.d.ts +15 -0
  15. package/dist/components/Dialog.js +26 -0
  16. package/dist/components/Disclosure.d.ts +25 -0
  17. package/dist/components/Disclosure.js +12 -0
  18. package/dist/components/LogView.d.ts +14 -0
  19. package/dist/components/LogView.js +36 -0
  20. package/dist/components/Markdown.d.ts +4 -0
  21. package/dist/components/Markdown.js +119 -0
  22. package/dist/components/Meter.d.ts +7 -0
  23. package/dist/components/Meter.js +7 -0
  24. package/dist/components/Notice.d.ts +9 -0
  25. package/dist/components/Notice.js +19 -0
  26. package/dist/components/PageHeader.d.ts +7 -0
  27. package/dist/components/PageHeader.js +5 -0
  28. package/dist/components/Popover.d.ts +16 -0
  29. package/dist/components/Popover.js +31 -0
  30. package/dist/components/SearchField.d.ts +8 -0
  31. package/dist/components/SearchField.js +8 -0
  32. package/dist/components/SectionPanel.d.ts +13 -0
  33. package/dist/components/SectionPanel.js +9 -0
  34. package/dist/components/Segmented.d.ts +12 -6
  35. package/dist/components/Segmented.js +7 -3
  36. package/dist/components/Stat.d.ts +12 -0
  37. package/dist/components/Stat.js +7 -0
  38. package/dist/components/StatusTag.d.ts +15 -0
  39. package/dist/components/StatusTag.js +28 -0
  40. package/dist/components/Tabs.d.ts +12 -0
  41. package/dist/components/Tabs.js +12 -0
  42. package/dist/components/ThemeMenu.d.ts +4 -0
  43. package/dist/components/ThemeMenu.js +52 -0
  44. package/dist/components/Toast.d.ts +5 -0
  45. package/dist/components/Toast.js +6 -0
  46. package/dist/components/icons.d.ts +19 -0
  47. package/dist/components/icons.js +17 -0
  48. package/dist/components/index.d.ts +21 -0
  49. package/dist/components/index.js +21 -0
  50. package/dist/components/labels.d.ts +42 -0
  51. package/dist/components/labels.js +34 -0
  52. package/package.json +35 -8
  53. package/src/styles/base.css +1 -37
  54. package/src/styles/content.css +39 -0
  55. package/src/styles/index.css +1 -0
  56. package/src/styles/raw.d.ts +5 -0
  57. package/src/styles/themes.css +30 -0
  58. package/src/styles/themes.test.ts +25 -0
  59. package/src/styles/tokens.css +10 -0
package/README.md CHANGED
@@ -19,10 +19,26 @@ No CSS principal do app, depois do Tailwind:
19
19
  @source "../node_modules/@thecodecodex/design-system/dist";
20
20
  ```
21
21
 
22
- `styles.css` = `tokens.css` + `themes.css` + `base.css`. Para não aplicar os estilos base (títulos, foco, tabelas, barras de rolagem), importe só `tokens.css` e `themes.css`.
22
+ `styles.css` = `tokens.css` + `themes.css` + `base.css` + `content.css`.
23
+
24
+ - `base.css`: barras de rolagem, foco visível e seleção. Serve a sites e apps.
25
+ - `content.css`: títulos com escala e margens de artigo e bordas em toda tabela (conteúdo editorial).
26
+
27
+ Apps e painéis costumam dispensar o `content.css`:
28
+
29
+ ```css
30
+ @import "tailwindcss";
31
+ @import "@thecodecodex/design-system/tokens.css";
32
+ @import "@thecodecodex/design-system/themes.css";
33
+ @import "@thecodecodex/design-system/base.css";
34
+ ```
23
35
 
24
36
  Utilidades geradas: `bg-surface`, `bg-inset`, `bg-ground`, `text-text-primary`, `text-text-secondary`, `bg-btn-primary`, `text-on-accent`, `border-border`, `border-soft`, `text-contrast`, `bg-chart-1`, `bg-diff-add`, `bg-cat-*` etc.
25
37
 
38
+ ### Status
39
+
40
+ `ok`, `warn`, `danger` e `info` (ligado/desligado, OK/atenção/erro), com valores próprios em cada tema e contraste ≥ 4.5:1 sobre a superfície: `text-ok`, `border-danger`, `bg-warn` (pontos e marcadores). Para avisos há o fundo suave `bg-ok-soft`, `bg-warn-soft`, `bg-danger-soft`, `bg-info-soft`. Use os tons como texto, contorno e ponto, nunca como único sinal: `StatusTag`, `StatusMark` e `StatusDot` mudam também a forma.
41
+
26
42
  ### Fontes
27
43
 
28
44
  Os tokens usam `--font-roboto` (display), `--font-poppins` (corpo) e `--font-jetbrains-mono` (mono). Cada app carrega as fontes e define essas variáveis (ex.: `next/font` com `variable: "--font-roboto"`, ou Astro Fonts).
@@ -56,7 +72,42 @@ No cliente: `watchTheme()` mantém o tema certo (virada de horário, mudança no
56
72
  import { Badge, NumberField, Segmented, ToolShell, ParamGroup } from "@thecodecodex/design-system"
57
73
  ```
58
74
 
59
- `Badge`, `Checkbox`, `DataTable`, `KeyValue`, `NumberField`, `OutputList`, `ParamGroup`, `Presets`, `RangeField`, `Segmented`, `Stepper`, `TextField`, `ToolShell`, as classes `panel`/`panelHeader` e os hooks `useCopy` e `useHydrated`.
75
+ Formulários e ferramentas: `Badge`, `Checkbox`, `DataTable`, `KeyValue`, `NumberField`, `OutputList`, `ParamGroup`, `Presets`, `RangeField`, `Segmented`, `Stepper`, `TextField`, `ToolShell`, as classes `panel`/`panelHeader` e os hooks `useCopy` e `useHydrated`.
76
+
77
+ Apps e painéis:
78
+
79
+ | Componente | Para quê |
80
+ |---|---|
81
+ | `AppShell` (+ `navLinkClass`) | casca de app: topo com marca e ações, menu lateral recolhível com grupos numerados, rodapé; `renderLink` recebe o `Link` do roteador |
82
+ | `PageHeader` | título da tela (Roboto 900, caixa alta), descrição e ações |
83
+ | `Panel` / `panelRows` | painel com faixa de título, contagem e ações; `panelRows` divide linhas de lista |
84
+ | `Button` / `buttonClass()` | variantes `ink` (principal), `outline`, `ghost` e `danger`, tamanhos `md` (44px) e `sm`; `buttonClass` serve para `<a>`/`<Link>` |
85
+ | `CopyButton` | "Copiar" → "Copiado!" |
86
+ | `Popover` / `MenuItem` | botão que abre painel ancorado (menu, seletor); fecha com Esc e clique fora |
87
+ | `Tabs` | abas `buttons` (visões da tela) ou `underline` (arquivos, seções) |
88
+ | `Segmented` | escolha única; opções aceitam `description` (cartões) e `tone: "danger"` (ex.: produção); `hideLabel`, `size`, `disabled` |
89
+ | `ChipToggles` | escolha múltipla em chips |
90
+ | `SearchField` | busca com ícone e rótulo acessível |
91
+ | `StatusTag`, `StatusMark`, `StatusDot` | selo, marcador de forma e ponto ligado/desligado nos tons de status |
92
+ | `Notice` | aviso com fundo suave, borda e ícone do tom |
93
+ | `Toast` | aviso flutuante (quem usa decide quando some) |
94
+ | `Stat` | número-resumo com rótulo, dica, tom e destaque |
95
+ | `Meter` | barra 0–100 com marca de meta |
96
+ | `Disclosure` | bloco recolhível (saída de comando, manutenção), neutro ou em tom ok/danger |
97
+ | `Dialog` | modal nativo (`<dialog>` + `showModal`): foco preso, Esc fecha, título, rodapé de botões |
98
+ | `ConfirmDialog` | confirmação com Cancelar e a ação (vermelha quando destrutiva) |
99
+ | `LogView` | console de log: cabeçalho, barra com filtros, copiar/baixar, linhas destacadas por tom, rola até o fim |
100
+ | `DataGrid` | resultado de consulta: filtro, paginação, CSV, JSON, NULL e booleanos destacados |
101
+ | `Markdown` | markdown enxuto renderizado como elementos React (sem HTML cru) |
102
+ | `ThemeMenu` | botão "Tema" com Auto, Sistema e os 5 temas (chama `watchTheme` uma vez por aba) |
103
+
104
+ ```tsx
105
+ <PageHeader title="Dev Launcher" actions={<Button variant="ink">Subir todos</Button>} />
106
+ <Panel title="Repositórios" count={2} bodyClass="px-6">
107
+ <div className={panelRows}>…</div>
108
+ </Panel>
109
+ <Dialog open={aberto} onClose={fechar} title="Excluir?" footer={<Button variant="danger">Excluir</Button>}>…</Dialog>
110
+ ```
60
111
 
61
112
  Textos fixos vêm em pt-BR; passe `labels` para traduzir (ex.: `<OutputList labels={{ copy: "Copy", copied: "Copied!" }} … />`).
62
113
 
@@ -0,0 +1,32 @@
1
+ import { type ReactNode } from "react";
2
+ import { type AppShellLabels } from "./labels.js";
3
+ export interface NavItem {
4
+ href: string;
5
+ label: string;
6
+ icon?: ReactNode;
7
+ }
8
+ export interface NavGroup {
9
+ title: string;
10
+ items: NavItem[];
11
+ }
12
+ export interface LinkProps {
13
+ href: string;
14
+ className: string;
15
+ title?: string;
16
+ "aria-current"?: "page";
17
+ children: ReactNode;
18
+ }
19
+ export declare function navLinkClass(active: boolean, collapsed?: boolean): string;
20
+ export default function AppShell({ brand, headerActions, nav, navTop, currentPath, renderLink, footer, numbered, defaultCollapsed, labels, children }: {
21
+ brand: ReactNode;
22
+ headerActions?: ReactNode;
23
+ nav: NavGroup[];
24
+ navTop?: (collapsed: boolean) => ReactNode;
25
+ currentPath: string;
26
+ renderLink?: (props: LinkProps) => ReactNode;
27
+ footer?: ReactNode;
28
+ numbered?: boolean;
29
+ defaultCollapsed?: boolean;
30
+ labels?: Partial<AppShellLabels>;
31
+ children: ReactNode;
32
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { Icon } from "./icons.js";
5
+ import { appShellLabels } from "./labels.js";
6
+ // <a> por padrão; apps com roteador passam o próprio Link (ex.: next/link)
7
+ const anchor = (props) => _jsx("a", { ...props });
8
+ // Classes de um link do menu lateral: use em itens extras (navTop) para casar com o resto
9
+ export function navLinkClass(active, collapsed = false) {
10
+ return `flex min-h-10 items-center gap-2.5 rounded-[10px] px-3 text-sm font-semibold transition-colors ${active ? "bg-text-primary text-bg-primary hover:text-bg-primary" : "text-text-primary hover:bg-inset hover:text-text-primary"} ${collapsed ? "justify-center px-0" : ""}`;
11
+ }
12
+ // Casca de app: topo (marca + ações), menu lateral recolhível com grupos numerados, conteúdo e rodapé
13
+ export default function AppShell({ brand, headerActions, nav, navTop, currentPath, renderLink = anchor, footer, numbered = true, defaultCollapsed = false, labels, children }) {
14
+ const text = { ...appShellLabels, ...labels };
15
+ const [collapsed, setCollapsed] = useState(defaultCollapsed);
16
+ return (_jsxs("div", { className: "flex h-screen w-full flex-col bg-ground text-text-primary", children: [_jsxs("header", { className: "flex w-full shrink-0 flex-wrap items-center justify-between gap-x-4 gap-y-2 border-b-2 border-border bg-surface px-4 py-2.5 md:px-6", children: [_jsx("div", { className: "min-w-0", children: brand }), headerActions && _jsx("div", { className: "flex flex-wrap items-center gap-2", children: headerActions })] }), _jsxs("div", { className: "flex flex-1 overflow-hidden", children: [_jsxs("aside", { className: `flex h-full shrink-0 flex-col border-r-[1.5px] border-soft bg-surface transition-[width] duration-300 ${collapsed ? "w-[68px]" : "w-64"}`, children: [_jsxs("nav", { "aria-label": text.nav, className: "flex-1 space-y-5 overflow-y-auto px-3 py-4", children: [navTop && _jsx("div", { className: "space-y-2", children: navTop(collapsed) }), nav.map((group, g) => (_jsxs("div", { className: "space-y-0.5", children: [collapsed
17
+ ? _jsx("div", { className: "my-2 h-[1.5px] bg-soft" })
18
+ : (_jsxs("div", { className: "px-3 pb-1.5 font-mono text-[13px] font-semibold lowercase text-btn-primary", children: [numbered && `${String(g + 1).padStart(2, "0")} / `, group.title] })), group.items.map((item) => {
19
+ const active = currentPath === item.href;
20
+ return (_jsx("div", { children: renderLink({
21
+ href: item.href,
22
+ title: collapsed ? item.label : undefined,
23
+ "aria-current": active ? "page" : undefined,
24
+ className: navLinkClass(active, collapsed),
25
+ children: (_jsxs(_Fragment, { children: [item.icon, !collapsed && _jsx("span", { className: "truncate", children: item.label })] })),
26
+ }) }, item.href));
27
+ })] }, group.title)))] }), _jsx("div", { className: "border-t-[1.5px] border-soft p-3", children: _jsxs("button", { type: "button", onClick: () => setCollapsed(!collapsed), "aria-label": collapsed ? text.expand : text.collapse, className: "flex min-h-10 w-full items-center gap-2 rounded-[10px] px-3 text-[13px] font-semibold text-text-secondary transition-colors hover:bg-inset hover:text-text-primary", children: [_jsx(Icon, { name: collapsed ? "arrowRight" : "arrowLeft", size: 14, className: collapsed ? "mx-auto" : "" }), !collapsed && _jsx("span", { children: text.collapse })] }) })] }), _jsx("main", { className: "w-full flex-1 overflow-y-auto bg-ground px-4 py-6 md:px-10 md:py-9", children: _jsx("div", { className: "flex min-h-full w-full flex-col", children: children }) })] }), footer && (_jsx("footer", { className: "flex w-full shrink-0 flex-wrap items-center justify-between gap-x-6 gap-y-1 border-t-[1.5px] border-soft bg-surface px-4 py-2 font-mono text-xs text-text-secondary md:px-6", children: footer }))] }));
28
+ }
@@ -0,0 +1,10 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ export type ButtonVariant = "ink" | "outline" | "ghost" | "danger";
3
+ export type ButtonSize = "md" | "sm";
4
+ export declare function buttonClass(variant?: ButtonVariant, size?: ButtonSize, extra?: string): string;
5
+ type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
6
+ variant?: ButtonVariant;
7
+ size?: ButtonSize;
8
+ };
9
+ export default function Button({ variant, size, className, type, ...props }: Props): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ const BASE = "inline-flex items-center justify-center gap-2 rounded-[10px] font-bold whitespace-nowrap transition-colors disabled:opacity-50 disabled:pointer-events-none";
3
+ const VARIANTS = {
4
+ ink: "border-2 border-text-primary bg-text-primary text-bg-primary hover:bg-bg-tertiary-hover hover:border-bg-tertiary-hover",
5
+ outline: "border-2 border-border bg-surface text-text-primary hover:border-contrast",
6
+ ghost: "border-2 border-transparent text-text-primary hover:bg-inset",
7
+ danger: "border-2 border-danger bg-surface text-danger hover:bg-danger hover:text-bg-primary",
8
+ };
9
+ // alvo de toque ≥ 44px no md
10
+ const SIZES = {
11
+ md: "min-h-11 px-4 text-sm",
12
+ sm: "min-h-9 px-3 text-[13px]",
13
+ };
14
+ // Classes do botão: use também em <a>/<Link> com cara de botão
15
+ export function buttonClass(variant = "outline", size = "md", extra = "") {
16
+ return `${BASE} ${VARIANTS[variant]} ${SIZES[size]} ${extra}`.trim();
17
+ }
18
+ export default function Button({ variant = "outline", size = "md", className = "", type = "button", ...props }) {
19
+ return _jsx("button", { type: type, className: buttonClass(variant, size, className), ...props });
20
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from "react";
2
+ export default function ChipToggles<T extends string>({ label, options, values, onValues, hideLabel }: {
3
+ label: ReactNode;
4
+ options: {
5
+ value: T;
6
+ label: ReactNode;
7
+ }[];
8
+ values: T[];
9
+ onValues: (values: NoInfer<T>[]) => void;
10
+ hideLabel?: boolean;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // Escolha múltipla em chips (checkbox com cara de pílula)
4
+ export default function ChipToggles({ label, options, values, onValues, hideLabel = false }) {
5
+ return (_jsxs("fieldset", { className: "flex min-w-0 flex-col gap-1.5", children: [_jsx("legend", { className: hideLabel ? "sr-only" : "mb-1.5 text-sm font-semibold", children: label }), _jsx("div", { className: "flex flex-wrap gap-2", children: options.map((option) => {
6
+ const on = values.includes(option.value);
7
+ return (_jsxs("label", { className: `inline-flex min-h-10 cursor-pointer items-center rounded-full border-2 border-border px-3.5 text-[13px] font-bold has-[:focus-visible]:outline-3 has-[:focus-visible]:outline-contrast ${on ? "bg-text-primary text-bg-primary" : "bg-surface hover:border-contrast"}`, children: [_jsx("input", { type: "checkbox", className: "sr-only", checked: on, onChange: () => onValues(on ? values.filter((v) => v !== option.value) : [...values, option.value]) }), option.label] }, option.value));
8
+ }) })] }));
9
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ import { type ConfirmDialogLabels } from "./labels.js";
3
+ export default function ConfirmDialog({ open, title, children, confirmLabel, onConfirm, onClose, danger, busy, icon, labels }: {
4
+ open: boolean;
5
+ title: ReactNode;
6
+ children: ReactNode;
7
+ confirmLabel: ReactNode;
8
+ onConfirm: () => void;
9
+ onClose: () => void;
10
+ danger?: boolean;
11
+ busy?: boolean;
12
+ icon?: ReactNode;
13
+ labels?: Partial<ConfirmDialogLabels>;
14
+ }): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import Button from "./Button.js";
4
+ import Dialog from "./Dialog.js";
5
+ import { confirmDialogLabels } from "./labels.js";
6
+ // Confirmação de ação: texto, Cancelar e o botão da ação (vermelho quando destrutiva)
7
+ export default function ConfirmDialog({ open, title, children, confirmLabel, onConfirm, onClose, danger = false, busy = false, icon, labels }) {
8
+ const text = { ...confirmDialogLabels, ...labels };
9
+ return (_jsx(Dialog, { open: open, onClose: onClose, locked: busy, width: "max-w-md", icon: icon, title: title, footer: _jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", onClick: onClose, disabled: busy, children: text.cancel }), _jsx(Button, { size: "sm", variant: danger ? "danger" : "ink", onClick: onConfirm, disabled: busy, children: confirmLabel })] }), children: _jsx("div", { className: "text-sm leading-relaxed text-text-secondary", children: children }) }));
10
+ }
@@ -0,0 +1,10 @@
1
+ import { type ButtonSize, type ButtonVariant } from "./Button.js";
2
+ import { type CopyLabels } from "./labels.js";
3
+ export default function CopyButton({ value, variant, size, labels, className, disabled }: {
4
+ value: string;
5
+ variant?: ButtonVariant;
6
+ size?: ButtonSize;
7
+ labels?: Partial<CopyLabels>;
8
+ className?: string;
9
+ disabled?: boolean;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { buttonClass } from "./Button.js";
4
+ import { Icon } from "./icons.js";
5
+ import { copyLabels } from "./labels.js";
6
+ import { useCopy } from "./useCopy.js";
7
+ // Botão "Copiar" que vira "Copiado!" por alguns segundos
8
+ export default function CopyButton({ value, variant = "outline", size = "sm", labels, className = "", disabled = false }) {
9
+ const text = { ...copyLabels, ...labels };
10
+ const { copied, copy } = useCopy(2000);
11
+ return (_jsxs("button", { type: "button", className: buttonClass(variant, size, className), disabled: disabled, onClick: () => copy(value, "v"), "aria-live": "polite", children: [_jsx(Icon, { name: copied ? "check" : "copy", size: 14 }), copied ? text.copied : text.copy] }));
12
+ }
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from "react";
2
+ import { type DataGridLabels } from "./labels.js";
3
+ type Cell = string | number | boolean | null | undefined | object;
4
+ interface Props {
5
+ columns: string[];
6
+ rows: Cell[][];
7
+ meta?: ReactNode;
8
+ truncated?: boolean;
9
+ onClose?: () => void;
10
+ fileName?: string;
11
+ pageSize?: number;
12
+ labels?: Partial<DataGridLabels>;
13
+ }
14
+ export default function DataGrid({ columns, rows, meta, truncated, onClose, fileName, pageSize, labels }: Props): import("react").JSX.Element;
15
+ export {};
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { Icon } from "./icons.js";
5
+ import { dataGridLabels } from "./labels.js";
6
+ import SearchField from "./SearchField.js";
7
+ import { useCopy } from "./useCopy.js";
8
+ const toolButton = "inline-flex min-h-9 items-center gap-1.5 rounded-[10px] border-2 border-border bg-surface px-3 text-[13px] font-bold text-text-primary hover:border-contrast";
9
+ const pageButton = "flex h-9 w-9 items-center justify-center rounded-[10px] border-2 border-border bg-surface text-text-primary hover:border-contrast disabled:pointer-events-none disabled:opacity-40";
10
+ function texto(cell) {
11
+ if (cell === null || cell === undefined)
12
+ return "";
13
+ return typeof cell === "object" ? JSON.stringify(cell) : String(cell);
14
+ }
15
+ function csvCell(cell) {
16
+ const s = texto(cell);
17
+ return /[",\n]/.test(s) ? `"${s.replace(/"/g, "\"\"")}"` : s;
18
+ }
19
+ // Grade de resultado de consulta: filtro em memória, paginação, CSV e JSON, NULL e booleanos destacados
20
+ export default function DataGrid({ columns, rows, meta, truncated = false, onClose, fileName = "resultado", pageSize = 50, labels }) {
21
+ const text = { ...dataGridLabels, ...labels };
22
+ const [filter, setFilter] = useState("");
23
+ const [page, setPage] = useState(1);
24
+ const { copied, copy } = useCopy(2000);
25
+ const filtered = useMemo(() => {
26
+ const term = filter.trim().toLowerCase();
27
+ return term ? rows.filter((row) => row.some((cell) => texto(cell).toLowerCase().includes(term))) : rows;
28
+ }, [rows, filter]);
29
+ const pages = Math.max(1, Math.ceil(filtered.length / pageSize));
30
+ const visible = filtered.slice((page - 1) * pageSize, page * pageSize);
31
+ const exportCsv = () => {
32
+ const body = [columns.map(csvCell).join(","), ...rows.map((row) => row.map(csvCell).join(","))].join("\n");
33
+ const url = URL.createObjectURL(new Blob([body], { type: "text/csv;charset=utf-8" }));
34
+ const a = document.createElement("a");
35
+ a.href = url;
36
+ a.download = `${fileName}.csv`;
37
+ a.click();
38
+ URL.revokeObjectURL(url);
39
+ };
40
+ const json = () => JSON.stringify(rows.map((row) => Object.fromEntries(columns.map((col, i) => [col, row[i] ?? null]))), null, 2);
41
+ return (_jsxs("div", { className: "flex flex-col overflow-hidden rounded-[20px] border-[1.5px] border-soft bg-surface", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-b-[1.5px] border-soft bg-inset px-4 py-3 text-[13px]", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "font-display text-xs font-extrabold uppercase tracking-[0.08em]", children: text.title }), meta, _jsx("span", { className: "font-semibold text-ok", children: rows.length === 1 ? text.oneRow : text.rows.replace("{n}", String(rows.length)) }), truncated && _jsx("span", { className: "rounded-md border-[1.5px] border-warn px-2 py-0.5 text-xs font-bold uppercase tracking-[0.06em] text-warn", children: text.truncated })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [rows.length > 5 && (_jsx(SearchField, { size: "sm", label: text.filter, value: filter, onValue: (v) => {
42
+ setFilter(v);
43
+ setPage(1);
44
+ } })), _jsxs("button", { type: "button", className: toolButton, onClick: exportCsv, children: [_jsx(Icon, { name: "download", size: 13 }), text.csv] }), _jsxs("button", { type: "button", className: toolButton, onClick: () => copy(json(), "json"), "aria-live": "polite", children: [_jsx(Icon, { name: copied ? "check" : "copy", size: 13 }), copied ? text.copied : text.json] }), onClose && (_jsx("button", { type: "button", onClick: onClose, "aria-label": text.close, className: "flex h-9 w-9 items-center justify-center rounded-[10px] text-text-secondary hover:bg-surface hover:text-text-primary", children: _jsx(Icon, { name: "close" }) }))] })] }), _jsx("div", { className: "max-h-[460px] overflow-auto font-mono text-xs", children: columns.length === 0 ? (_jsx("p", { className: "m-0 p-8 text-center font-body text-sm text-text-secondary", children: text.noColumns })) : visible.length === 0 ? (_jsx("p", { className: "m-0 p-8 text-center font-body text-sm text-text-secondary", children: text.empty })) : (_jsxs("table", { className: "plain w-full border-collapse text-left", children: [_jsx("thead", { className: "sticky top-0 z-10 border-b-[1.5px] border-soft bg-surface text-text-secondary", children: _jsxs("tr", { children: [_jsx("th", { className: "w-12 border-r border-soft px-3 py-2.5 text-center", children: "#" }), columns.map((col) => (_jsx("th", { className: "whitespace-nowrap border-r border-soft px-3.5 py-2.5 font-bold text-btn-primary", children: col }, col)))] }) }), _jsx("tbody", { children: visible.map((row, r) => (_jsxs("tr", { className: "border-t border-soft even:bg-inset/50 hover:bg-inset", children: [_jsx("td", { className: "select-none border-r border-soft px-3 py-1.5 text-center text-text-secondary", children: (page - 1) * pageSize + r + 1 }), columns.map((_, c) => {
45
+ const cell = row[c];
46
+ if (cell === null || cell === undefined) {
47
+ return _jsx("td", { className: "border-r border-soft px-3.5 py-1.5 italic text-text-secondary", children: "<NULL>" }, c);
48
+ }
49
+ if (typeof cell === "boolean") {
50
+ return (_jsx("td", { className: "border-r border-soft px-3.5 py-1.5 font-bold", children: _jsx("span", { className: `rounded-md border-[1.5px] border-current px-1.5 py-0.5 ${cell ? "text-ok" : "text-danger"}`, children: cell ? "TRUE" : "FALSE" }) }, c));
51
+ }
52
+ return _jsx("td", { title: texto(cell), className: "max-w-xs truncate whitespace-pre border-r border-soft px-3.5 py-1.5", children: texto(cell) }, c);
53
+ })] }, r))) })] })) }), pages > 1 && (_jsxs("div", { className: "flex items-center justify-between border-t-[1.5px] border-soft bg-inset px-4 py-2 text-[13px] text-text-secondary", children: [_jsx("span", { children: text.page.replace("{page}", String(page)).replace("{pages}", String(pages)) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", className: pageButton, disabled: page === 1, onClick: () => setPage(page - 1), "aria-label": text.previous, children: _jsx(Icon, { name: "chevronLeft" }) }), _jsx("button", { type: "button", className: pageButton, disabled: page === pages, onClick: () => setPage(page + 1), "aria-label": text.next, children: _jsx(Icon, { name: "chevronRight" }) })] })] }))] }));
54
+ }
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from "react";
2
+ import { type DialogLabels } from "./labels.js";
3
+ interface Props {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ title: ReactNode;
7
+ icon?: ReactNode;
8
+ children: ReactNode;
9
+ footer?: ReactNode;
10
+ width?: string;
11
+ locked?: boolean;
12
+ labels?: Partial<DialogLabels>;
13
+ }
14
+ export default function Dialog({ open, onClose, title, icon, children, footer, width, locked, labels }: Props): import("react").JSX.Element;
15
+ export {};
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
4
+ import { dialogLabels } from "./labels.js";
5
+ // Diálogo modal nativo (<dialog> + showModal): foco preso, Esc fecha, fundo escurecido
6
+ export default function Dialog({ open, onClose, title, icon, children, footer, width = "max-w-lg", locked = false, labels }) {
7
+ const text = { ...dialogLabels, ...labels };
8
+ const ref = useRef(null);
9
+ useEffect(() => {
10
+ const el = ref.current;
11
+ if (!el)
12
+ return;
13
+ if (open && !el.open)
14
+ el.showModal();
15
+ if (!open && el.open)
16
+ el.close();
17
+ }, [open]);
18
+ return (_jsx("dialog", { ref: ref, onCancel: (e) => {
19
+ e.preventDefault();
20
+ if (!locked)
21
+ onClose();
22
+ }, onClick: (e) => {
23
+ if (e.target === e.currentTarget && !locked)
24
+ onClose();
25
+ }, className: `m-auto w-[calc(100%-2rem)] ${width} max-h-[calc(100dvh-2rem)] overflow-hidden rounded-[20px] border-2 border-border bg-surface p-0 text-text-primary shadow-[6px_6px_0_var(--border)] backdrop:bg-black/60 backdrop:backdrop-blur-xs`, children: open && (_jsxs("div", { className: "flex max-h-[calc(100dvh-2rem)] flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 border-b-[1.5px] border-soft bg-inset px-5 py-3.5", children: [_jsxs("h2", { className: "m-0 flex min-w-0 items-center gap-2.5 font-display text-sm font-extrabold uppercase tracking-[0.08em]", children: [icon, _jsx("span", { className: "truncate", children: title })] }), _jsx("button", { type: "button", onClick: onClose, disabled: locked, "aria-label": text.close, className: "inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[10px] text-text-secondary hover:bg-surface hover:text-text-primary disabled:opacity-40", children: _jsx("svg", { "aria-hidden": "true", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: _jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-5", children: children }), footer && _jsx("div", { className: "flex flex-wrap items-center justify-end gap-2 border-t-[1.5px] border-soft bg-inset px-5 py-3", children: footer })] })) }));
26
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+ declare const TONE: {
3
+ readonly neutral: {
4
+ readonly box: "border-soft bg-surface";
5
+ readonly title: "";
6
+ };
7
+ readonly ok: {
8
+ readonly box: "border-ok bg-ok-soft";
9
+ readonly title: "text-ok";
10
+ };
11
+ readonly danger: {
12
+ readonly box: "border-danger bg-danger-soft";
13
+ readonly title: "text-danger";
14
+ };
15
+ };
16
+ export default function Disclosure({ title, hint, tone, open, heading, className, children }: {
17
+ title: ReactNode;
18
+ hint?: ReactNode;
19
+ tone?: keyof typeof TONE;
20
+ open?: boolean;
21
+ heading?: boolean;
22
+ className?: string;
23
+ children: ReactNode;
24
+ }): import("react").JSX.Element;
25
+ export {};
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const TONE = {
3
+ neutral: { box: "border-soft bg-surface", title: "" },
4
+ ok: { box: "border-ok bg-ok-soft", title: "text-ok" },
5
+ danger: { box: "border-danger bg-danger-soft", title: "text-danger" },
6
+ };
7
+ // Bloco recolhível (<details>): saída de comando, seção avançada, manutenção.
8
+ // heading: faixa em caixa alta (seção) · plain: título em negrito (resultado)
9
+ export default function Disclosure({ title, hint, tone = "neutral", open, heading = false, className = "", children }) {
10
+ const style = TONE[tone];
11
+ return (_jsxs("details", { open: open, className: `rounded-[20px] border-[1.5px] px-5 sm:px-6 ${style.box} ${className}`, children: [_jsxs("summary", { className: `flex min-h-[52px] cursor-pointer flex-wrap items-center gap-x-2 ${heading ? "font-display text-sm font-extrabold uppercase tracking-[0.08em]" : "text-sm font-bold"} ${style.title}`, children: [title, hint && _jsx("span", { className: "font-body text-[13px] font-normal normal-case tracking-normal text-text-secondary", children: hint })] }), _jsx("div", { className: "pb-5", children: children })] }));
12
+ }
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from "react";
2
+ import { type LogViewLabels } from "./labels.js";
3
+ export type LogTone = "ok" | "warn" | "danger";
4
+ export default function LogView({ lines, toneOf, header, toolbar, empty, downloadName, autoScroll, className, labels }: {
5
+ lines: string[];
6
+ toneOf?: (line: string) => LogTone | undefined;
7
+ header?: ReactNode;
8
+ toolbar?: ReactNode;
9
+ empty?: ReactNode;
10
+ downloadName?: string;
11
+ autoScroll?: boolean;
12
+ className?: string;
13
+ labels?: Partial<LogViewLabels>;
14
+ }): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
4
+ import { Icon } from "./icons.js";
5
+ import { logViewLabels } from "./labels.js";
6
+ import { useCopy } from "./useCopy.js";
7
+ const LINE = {
8
+ ok: "text-ok",
9
+ warn: "bg-warn-soft text-warn",
10
+ danger: "bg-danger-soft text-danger",
11
+ };
12
+ const toolButton = "inline-flex min-h-9 items-center gap-1.5 rounded-[8px] px-2.5 font-body text-[13px] font-bold text-text-primary hover:bg-inset disabled:opacity-50";
13
+ // Console de log: cabeçalho opcional, barra (filtros/metadados + copiar/baixar) e linhas em mono que rolam até o fim
14
+ export default function LogView({ lines, toneOf, header, toolbar, empty, downloadName, autoScroll = true, className = "min-h-[420px]", labels }) {
15
+ const text = { ...logViewLabels, ...labels };
16
+ const { copied, copy } = useCopy(2000);
17
+ const box = useRef(null);
18
+ useEffect(() => {
19
+ if (autoScroll && box.current)
20
+ box.current.scrollTop = box.current.scrollHeight;
21
+ }, [lines, autoScroll]);
22
+ const download = () => {
23
+ const url = URL.createObjectURL(new Blob([lines.join("\n")], { type: "text/plain;charset=utf-8" }));
24
+ const a = document.createElement("a");
25
+ a.href = url;
26
+ a.download = downloadName ?? "log.txt";
27
+ a.click();
28
+ URL.revokeObjectURL(url);
29
+ };
30
+ return (_jsxs("div", { className: `flex flex-col overflow-hidden rounded-[20px] border-[1.5px] border-soft bg-surface ${className}`, children: [header && _jsx("div", { className: "border-b-[1.5px] border-soft px-5 py-4", children: header }), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b-[1.5px] border-soft px-4 py-2 font-mono text-xs text-text-secondary", children: [_jsx("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: toolbar }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("button", { type: "button", className: toolButton, disabled: lines.length === 0, onClick: () => copy(lines.join("\n"), "log"), "aria-live": "polite", children: [_jsx(Icon, { name: copied ? "check" : "copy", size: 13 }), copied ? text.copied : text.copy] }), downloadName && (_jsx("button", { type: "button", className: toolButton, disabled: lines.length === 0, onClick: download, "aria-label": text.download, title: text.download, children: _jsx(Icon, { name: "download", size: 14 }) }))] })] }), _jsx("div", { ref: box, className: "min-h-0 flex-1 select-text overflow-y-auto bg-inset px-4 py-3 font-mono text-xs leading-relaxed", children: lines.length === 0
31
+ ? _jsx("p", { className: "m-0 font-body text-sm text-text-secondary", children: empty })
32
+ : lines.map((line, i) => {
33
+ const tone = toneOf?.(line);
34
+ return (_jsx("div", { className: `whitespace-pre-wrap rounded-sm px-1.5 ${tone ? LINE[tone] : ""}`, children: line || " " }, i));
35
+ }) })] }));
36
+ }
@@ -0,0 +1,4 @@
1
+ export default function Markdown({ source, className }: {
2
+ source: string;
3
+ className?: string;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,119 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Markdown enxuto (títulos, parágrafos, listas, citações, código, tabelas, `código`, **negrito**, *itálico*, [links](url)).
3
+ // Monta elementos React, nunca HTML cru: o conteúdo não executa nada. Frontmatter (---) é ignorado.
4
+ function inline(source, key) {
5
+ const parts = [];
6
+ const re = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*\s][^*]*\*|_[^_\s][^_]*_)|(\[[^\]]+\]\([^)\s]+\))/g;
7
+ let last = 0;
8
+ let i = 0;
9
+ let m;
10
+ while ((m = re.exec(source))) {
11
+ if (m.index > last)
12
+ parts.push(source.slice(last, m.index));
13
+ const t = m[0];
14
+ const k = `${key}-${i++}`;
15
+ if (m[1])
16
+ parts.push(_jsx("code", { className: "rounded bg-inset px-1.5 py-px font-mono text-[0.92em]", children: t.slice(1, -1) }, k));
17
+ else if (m[2])
18
+ parts.push(_jsx("strong", { children: t.slice(2, -2) }, k));
19
+ else if (m[3])
20
+ parts.push(_jsx("em", { children: t.slice(1, -1) }, k));
21
+ else {
22
+ const [, label = "", url = ""] = t.match(/^\[([^\]]+)\]\(([^)\s]+)\)$/) ?? [];
23
+ parts.push(/^(https?:|\/|#|\.)/.test(url)
24
+ ? _jsx("a", { href: url, target: url.startsWith("http") ? "_blank" : undefined, rel: "noopener noreferrer", className: "font-semibold text-btn-primary underline underline-offset-2 hover:text-contrast", children: label }, k)
25
+ : _jsx("span", { children: label }, k));
26
+ }
27
+ last = m.index + t.length;
28
+ }
29
+ if (last < source.length)
30
+ parts.push(source.slice(last));
31
+ return parts;
32
+ }
33
+ const HEADING = [
34
+ "m-0 mt-2 font-display text-[22px] font-black uppercase leading-tight",
35
+ "m-0 mt-3 font-display text-lg font-extrabold leading-tight",
36
+ "m-0 mt-2 font-display text-[15px] font-bold",
37
+ ];
38
+ export default function Markdown({ source, className = "" }) {
39
+ let body = source.replace(/\r\n/g, "\n");
40
+ if (body.startsWith("---\n")) {
41
+ const end = body.indexOf("\n---", 4);
42
+ if (end !== -1)
43
+ body = body.slice(end + 4);
44
+ }
45
+ const lines = body.split("\n");
46
+ const at = (n) => lines[n] ?? "";
47
+ const blocks = [];
48
+ let i = 0;
49
+ let n = 0;
50
+ while (i < lines.length) {
51
+ const line = at(i);
52
+ const k = `b${n++}`;
53
+ if (!line.trim()) {
54
+ i++;
55
+ continue;
56
+ }
57
+ if (line.trim().startsWith("```")) {
58
+ const code = [];
59
+ i++;
60
+ while (i < lines.length && !at(i).trim().startsWith("```"))
61
+ code.push(at(i++));
62
+ i++;
63
+ blocks.push(_jsx("pre", { className: "m-0 overflow-x-auto rounded-[10px] bg-inset p-3.5 font-mono text-xs leading-relaxed", children: code.join("\n") }, k));
64
+ continue;
65
+ }
66
+ const heading = line.match(/^(#{1,4})\s+(.*)$/);
67
+ if (heading) {
68
+ const level = (heading[1] ?? "#").length;
69
+ const Tag = `h${Math.min(level + 1, 6)}`;
70
+ blocks.push(_jsx(Tag, { className: HEADING[Math.min(level, 3) - 1], children: inline(heading[2] ?? "", k) }, k));
71
+ i++;
72
+ continue;
73
+ }
74
+ if (/^(-{3,}|\*{3,})$/.test(line.trim())) {
75
+ blocks.push(_jsx("hr", { className: "border-soft" }, k));
76
+ i++;
77
+ continue;
78
+ }
79
+ if (line.trim().startsWith("|") && /^\s*\|?\s*:?-{3,}/.test(at(i + 1))) {
80
+ const cells = (l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
81
+ const head = cells(line);
82
+ i += 2;
83
+ const rows = [];
84
+ while (i < lines.length && at(i).trim().startsWith("|"))
85
+ rows.push(cells(at(i++)));
86
+ blocks.push(_jsx("div", { className: "overflow-x-auto rounded-[14px] border-[1.5px] border-soft", children: _jsxs("table", { className: "plain w-full border-collapse text-[13px]", children: [_jsx("thead", { children: _jsx("tr", { className: "bg-inset text-left", children: head.map((c, j) => _jsx("th", { className: "px-3 py-2 font-bold", children: inline(c, `${k}h${j}`) }, j)) }) }), _jsx("tbody", { children: rows.map((row, r) => (_jsx("tr", { className: "border-t-[1.5px] border-soft", children: row.map((c, j) => _jsx("td", { className: "px-3 py-2 align-top", children: inline(c, `${k}${r}${j}`) }, j)) }, r))) })] }) }, k));
87
+ continue;
88
+ }
89
+ if (line.startsWith(">")) {
90
+ const quote = [];
91
+ while (i < lines.length && at(i).startsWith(">"))
92
+ quote.push(at(i++).replace(/^>\s?/, ""));
93
+ blocks.push(_jsx("blockquote", { className: "m-0 border-l-[3px] border-contrast pl-3.5 text-text-secondary", children: inline(quote.join(" "), k) }, k));
94
+ continue;
95
+ }
96
+ const item = /^(\s*)([-*]|\d+\.)\s+/;
97
+ if (item.test(line)) {
98
+ const ordered = /^\s*\d+\./.test(line);
99
+ const items = [];
100
+ while (i < lines.length && (item.test(at(i)) || (/^\s{2,}\S/.test(at(i)) && items.length))) {
101
+ if (item.test(at(i)))
102
+ items.push(at(i).replace(item, ""));
103
+ else
104
+ items[items.length - 1] += ` ${at(i).trim()}`;
105
+ i++;
106
+ }
107
+ const List = ordered ? "ol" : "ul";
108
+ blocks.push(_jsx(List, { className: `m-0 flex flex-col gap-1 pl-5 ${ordered ? "list-decimal" : "list-disc"}`, children: items.map((t, j) => _jsx("li", { children: inline(t.replace(/^\[( |x)\]\s*/, (_, x) => (x === "x" ? "☑ " : "☐ ")), `${k}${j}`) }, j)) }, k));
109
+ continue;
110
+ }
111
+ const para = [];
112
+ while (i < lines.length && at(i).trim() && !/^(#{1,4}\s|```|>|\s*([-*]|\d+\.)\s|\|)/.test(at(i)))
113
+ para.push(at(i++).trim());
114
+ if (para.length === 0)
115
+ para.push(at(i++).trim());
116
+ blocks.push(_jsx("p", { className: "m-0", children: inline(para.join(" "), k) }, k));
117
+ }
118
+ return _jsx("div", { className: `flex flex-col gap-3 text-sm leading-relaxed text-text-primary ${className}`, children: blocks });
119
+ }
@@ -0,0 +1,7 @@
1
+ import type { StatusTone } from "./StatusTag.js";
2
+ export default function Meter({ value, target, tone, label }: {
3
+ value: number;
4
+ target?: number;
5
+ tone?: StatusTone;
6
+ label: string;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const FILL = { ok: "bg-ok", warn: "bg-warn", danger: "bg-danger", info: "bg-info", neutral: "bg-text-secondary" };
3
+ // Barra de progresso 0–100 com marca de meta opcional (ex.: cobertura de testes)
4
+ export default function Meter({ value, target, tone = "neutral", label }) {
5
+ const pct = Math.min(100, Math.max(0, value));
6
+ return (_jsxs("span", { className: "relative block h-2.5 rounded-full bg-inset", role: "img", "aria-label": label, children: [_jsx("span", { className: `absolute inset-y-0 left-0 rounded-full ${FILL[tone]}`, style: { width: `${pct}%` } }), target !== undefined && _jsx("span", { className: "absolute -bottom-1 -top-1 w-0.5 bg-text-primary", style: { left: `${target}%` } })] }));
7
+ }
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from "react";
2
+ export type NoticeTone = "ok" | "warn" | "danger" | "info";
3
+ export default function Notice({ tone, title, children, role, className }: {
4
+ tone?: NoticeTone;
5
+ title?: ReactNode;
6
+ children?: ReactNode;
7
+ role?: "status" | "alert";
8
+ className?: string;
9
+ }): import("react").JSX.Element;