@dtcode/ui 0.3.0 → 0.4.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.
- package/README.md +23 -0
- package/dist/animated-symbol.d.ts +3 -0
- package/dist/animated-symbol.js +81 -0
- package/dist/avatar.d.ts +15 -0
- package/dist/avatar.js +16 -0
- package/dist/brand.d.ts +11 -0
- package/dist/brand.js +13 -0
- package/dist/checkbox.d.ts +8 -0
- package/dist/checkbox.js +12 -0
- package/dist/dialog.d.ts +24 -0
- package/dist/dialog.js +21 -0
- package/dist/dropdown-menu.d.ts +12 -0
- package/dist/dropdown-menu.js +21 -0
- package/dist/empty-state.d.ts +13 -0
- package/dist/empty-state.js +8 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/initials.d.ts +2 -0
- package/dist/initials.js +10 -0
- package/dist/input.d.ts +5 -0
- package/dist/input.js +5 -1
- package/dist/login-template.d.ts +44 -0
- package/dist/login-template.js +67 -0
- package/dist/select.d.ts +7 -0
- package/dist/select.js +8 -0
- package/dist/sheet.d.ts +26 -0
- package/dist/sheet.js +21 -0
- package/dist/skeleton.d.ts +5 -0
- package/dist/skeleton.js +9 -0
- package/dist/styles.css +295 -1
- package/dist/table.d.ts +7 -0
- package/dist/table.js +21 -0
- package/dist/textarea.d.ts +5 -0
- package/dist/textarea.js +7 -0
- package/dist/theme-script.d.ts +11 -0
- package/dist/theme-script.js +10 -0
- package/dist/theme.d.ts +24 -0
- package/dist/theme.js +91 -0
- package/dist/toast.d.ts +24 -0
- package/dist/toast.js +49 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -40,6 +40,29 @@ import { Icon } from "@dtcode/ui";
|
|
|
40
40
|
|
|
41
41
|
A fonte é do Google, sob a licença Apache 2.0 (`dist/material-symbols-LICENSE.txt`).
|
|
42
42
|
|
|
43
|
+
## O que vem no pacote
|
|
44
|
+
|
|
45
|
+
- **Formulários**: `Button`, `Input`, `Field`, `Select`, `Textarea`, `Checkbox`, `Switch`, `Segmented`.
|
|
46
|
+
- **Feedback**: `Alert`, `Badge`, `Loader`, `Skeleton`, `EmptyState`, `ToastProvider` + `useToast`.
|
|
47
|
+
- **Sobreposição**: `Dialog`, `Sheet` (painel lateral), `DropdownMenu`.
|
|
48
|
+
- **Dados e layout**: `Card`, `Table`, `Avatar`, `Tabs`, `DefinitionList`.
|
|
49
|
+
- **Marca**: `Logo`, `BrandSymbol`, `AnimatedSymbol` (símbolo animado do login), `Icon`.
|
|
50
|
+
- **Tema**: `ThemeProvider`, `useTheme`, `ThemeToggle` e `themeScript()` (aplica o tema antes da primeira pintura).
|
|
51
|
+
- **Telas prontas**: `LoginTemplate`, com provedores (Google, SSO), modo só-SSO e aviso acima do formulário.
|
|
52
|
+
- **Utilitários**: `buttonClasses`, `interactiveCard`, `controlClasses`, `cn`, `initials`.
|
|
53
|
+
- **Classes no styles.css**: `eyebrow`, `stagger`, `text-gradient`, `bg-hero`, `bg-dots`, `bg-dots-light`, `scrollbar-dark`.
|
|
54
|
+
|
|
55
|
+
## Tema escuro
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { ThemeProvider, themeScript } from "@dtcode/ui";
|
|
59
|
+
|
|
60
|
+
// No <head>, antes do CSS: evita piscar o tema errado. No Vite, cole o retorno num <script> do index.html.
|
|
61
|
+
<script dangerouslySetInnerHTML={{ __html: themeScript() }} />
|
|
62
|
+
|
|
63
|
+
<ThemeProvider>{children}</ThemeProvider>
|
|
64
|
+
```
|
|
65
|
+
|
|
43
66
|
## Uso
|
|
44
67
|
|
|
45
68
|
```tsx
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { BrandSymbol } from "./brand.js";
|
|
5
|
+
import { cn } from "./cn.js";
|
|
6
|
+
/**
|
|
7
|
+
* Símbolo dtcode animado (tela de login).
|
|
8
|
+
* Entrada: as peças voam de longe e se encaixam; no encaixe, ondas em losango, aura e halo acendem
|
|
9
|
+
* e o chevron pisca como um cursor de terminal.
|
|
10
|
+
* Depois: flutua, recebe um reflexo de luz, solta partículas em losango e se inclina em 3D seguindo o
|
|
11
|
+
* ponteiro (parallax por peça). No hover, as peças se afastam (visão explodida).
|
|
12
|
+
* Clicar no símbolo roda a montagem de novo (útil em apresentações).
|
|
13
|
+
* Estilos no styles.css do pacote (.symbol-stage e relacionados). Respeita prefers-reduced-motion.
|
|
14
|
+
*/
|
|
15
|
+
// Partículas com posições fixas (sem aleatório: o HTML do servidor e o do navegador precisam bater).
|
|
16
|
+
const PARTICLES = [
|
|
17
|
+
{ x: "12%", s: "7px", d: "7s", delay: "1.8s", drift: "-18px", o: 0.8 },
|
|
18
|
+
{ x: "26%", s: "5px", d: "9s", delay: "3.4s", drift: "14px", o: 0.6 },
|
|
19
|
+
{ x: "40%", s: "9px", d: "8s", delay: "2.4s", drift: "-10px", o: 0.9 },
|
|
20
|
+
{ x: "58%", s: "6px", d: "10s", delay: "4.6s", drift: "20px", o: 0.7 },
|
|
21
|
+
{ x: "70%", s: "8px", d: "7.5s", delay: "2.1s", drift: "-22px", o: 0.85 },
|
|
22
|
+
{ x: "84%", s: "5px", d: "9.5s", delay: "5.2s", drift: "12px", o: 0.6 },
|
|
23
|
+
{ x: "33%", s: "4px", d: "11s", delay: "6.1s", drift: "8px", o: 0.5 },
|
|
24
|
+
{ x: "64%", s: "4px", d: "12s", delay: "3.9s", drift: "-6px", o: 0.5 },
|
|
25
|
+
];
|
|
26
|
+
export function AnimatedSymbol({ className }) {
|
|
27
|
+
const stage = useRef(null);
|
|
28
|
+
// Cada clique remonta o palco (key), o que reinicia todas as animações de entrada.
|
|
29
|
+
const [run, setRun] = useState(0);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const el = stage.current;
|
|
32
|
+
if (!el || matchMedia("(prefers-reduced-motion: reduce)").matches)
|
|
33
|
+
return;
|
|
34
|
+
const target = { x: 0, y: 0 };
|
|
35
|
+
const current = { x: 0, y: 0 };
|
|
36
|
+
let frame = 0;
|
|
37
|
+
// Suaviza o movimento (interpolação a cada quadro) até assentar no alvo.
|
|
38
|
+
const tick = () => {
|
|
39
|
+
current.x += (target.x - current.x) * 0.08;
|
|
40
|
+
current.y += (target.y - current.y) * 0.08;
|
|
41
|
+
el.style.setProperty("--px", current.x.toFixed(4));
|
|
42
|
+
el.style.setProperty("--py", current.y.toFixed(4));
|
|
43
|
+
const settled = Math.abs(target.x - current.x) < 0.001 &&
|
|
44
|
+
Math.abs(target.y - current.y) < 0.001;
|
|
45
|
+
frame = settled ? 0 : requestAnimationFrame(tick);
|
|
46
|
+
};
|
|
47
|
+
const wake = () => {
|
|
48
|
+
if (!frame)
|
|
49
|
+
frame = requestAnimationFrame(tick);
|
|
50
|
+
};
|
|
51
|
+
const onMove = (e) => {
|
|
52
|
+
const r = el.getBoundingClientRect();
|
|
53
|
+
const cx = r.left + r.width / 2;
|
|
54
|
+
const cy = r.top + r.height / 2;
|
|
55
|
+
// -1..1 em relação ao centro do símbolo, normalizado por meia tela
|
|
56
|
+
target.x = Math.max(-1, Math.min(1, (e.clientX - cx) / (innerWidth / 2)));
|
|
57
|
+
target.y = Math.max(-1, Math.min(1, (e.clientY - cy) / (innerHeight / 2)));
|
|
58
|
+
wake();
|
|
59
|
+
};
|
|
60
|
+
const onLeave = () => {
|
|
61
|
+
target.x = 0;
|
|
62
|
+
target.y = 0;
|
|
63
|
+
wake();
|
|
64
|
+
};
|
|
65
|
+
window.addEventListener("pointermove", onMove, { passive: true });
|
|
66
|
+
document.addEventListener("pointerleave", onLeave);
|
|
67
|
+
return () => {
|
|
68
|
+
window.removeEventListener("pointermove", onMove);
|
|
69
|
+
document.removeEventListener("pointerleave", onLeave);
|
|
70
|
+
cancelAnimationFrame(frame);
|
|
71
|
+
};
|
|
72
|
+
}, [run]);
|
|
73
|
+
return (_jsx("button", { type: "button", onClick: () => setRun((n) => n + 1), "aria-label": "dtcode: clique para rever a montagem do s\u00EDmbolo", title: "Clique para rever a montagem", className: cn("cursor-pointer rounded-md outline-offset-8 transition-[scale] duration-300 ease-brand hover:scale-105", className), children: _jsxs("div", { ref: stage, className: "symbol-stage relative grid size-full place-items-center", children: [_jsx("div", { "aria-hidden": true, className: "symbol-aurora" }), _jsx("div", { "aria-hidden": true, className: "symbol-halo" }), _jsx("div", { "aria-hidden": true, className: "symbol-particles", children: PARTICLES.map((p, i) => (_jsx("span", { style: {
|
|
74
|
+
"--x": p.x,
|
|
75
|
+
"--s": p.s,
|
|
76
|
+
"--d": p.d,
|
|
77
|
+
"--delay": p.delay,
|
|
78
|
+
"--drift": p.drift,
|
|
79
|
+
"--o": p.o,
|
|
80
|
+
} }, i))) }), _jsx("div", { "aria-hidden": true, className: "symbol-ring" }), _jsx("div", { "aria-hidden": true, className: "symbol-ring symbol-ring--late" }), _jsxs("div", { className: "symbol-tilt relative size-full", children: [_jsx(BrandSymbol, { className: "symbol-live absolute inset-0 size-full text-[#1D004E]" }), _jsx(BrandSymbol, { mono: true, className: "symbol-live symbol-shine absolute inset-0 size-full text-white" })] })] }, run) }));
|
|
81
|
+
}
|
package/dist/avatar.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare const sizes: {
|
|
2
|
+
sm: string;
|
|
3
|
+
md: string;
|
|
4
|
+
lg: string;
|
|
5
|
+
};
|
|
6
|
+
export type AvatarProps = {
|
|
7
|
+
/** Nome da pessoa: vira as iniciais e o texto alternativo da foto. */
|
|
8
|
+
name: string;
|
|
9
|
+
src?: string | null;
|
|
10
|
+
size?: keyof typeof sizes;
|
|
11
|
+
className?: string;
|
|
12
|
+
};
|
|
13
|
+
/** Foto da pessoa ou, sem foto (ou se ela falhar), as iniciais sobre o tom da marca. */
|
|
14
|
+
export declare function Avatar({ name, src, size, className }: AvatarProps): import("react").JSX.Element;
|
|
15
|
+
export {};
|
package/dist/avatar.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cn } from "./cn.js";
|
|
5
|
+
import { initials } from "./initials.js";
|
|
6
|
+
const sizes = { sm: "size-7 text-[11px]", md: "size-9 text-[13px]", lg: "size-12 text-base" };
|
|
7
|
+
/** Foto da pessoa ou, sem foto (ou se ela falhar), as iniciais sobre o tom da marca. */
|
|
8
|
+
export function Avatar({ name, src, size = "md", className }) {
|
|
9
|
+
const [failed, setFailed] = useState(false);
|
|
10
|
+
const cls = cn("inline-grid flex-none place-items-center overflow-hidden rounded-full", sizes[size], className);
|
|
11
|
+
if (src && !failed) {
|
|
12
|
+
// eslint-disable-next-line @next/next/no-img-element -- pacote independente de framework
|
|
13
|
+
return _jsx("img", { src: src, alt: name, referrerPolicy: "no-referrer", onError: () => setFailed(true), className: cn(cls, "object-cover") });
|
|
14
|
+
}
|
|
15
|
+
return (_jsx("span", { role: "img", "aria-label": name, className: cn(cls, "bg-tint font-display font-semibold text-tint-fg"), children: initials(name) }));
|
|
16
|
+
}
|
package/dist/brand.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Símbolo oficial dtcode: três losangos + chevron. Decorativo (aria-hidden). */
|
|
2
|
+
export declare function BrandSymbol({ className, mono }: {
|
|
3
|
+
className?: string;
|
|
4
|
+
mono?: boolean;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
/** Logo horizontal oficial. Tamanho segue o font-size (altura ≈ 1,3em). */
|
|
7
|
+
export declare function Logo({ className, tone, mono }: {
|
|
8
|
+
className?: string;
|
|
9
|
+
tone?: "auto" | "light";
|
|
10
|
+
mono?: boolean;
|
|
11
|
+
}): import("react").JSX.Element;
|
package/dist/brand.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "./cn.js";
|
|
3
|
+
// Gerado a partir dos SVGs oficiais do MIV (public/brand/dtcode/ no repositório do Design System).
|
|
4
|
+
// A cor escura (#1D004E) vira currentColor para o logo funcionar em fundo claro e escuro;
|
|
5
|
+
// `mono` aplica uma cor só (ex.: branco sobre foto).
|
|
6
|
+
/** Símbolo oficial dtcode: três losangos + chevron. Decorativo (aria-hidden). */
|
|
7
|
+
export function BrandSymbol({ className, mono }) {
|
|
8
|
+
return (_jsxs("svg", { viewBox: "0 0 350 477.43", className: cn("flex-none", className ?? "size-[1.2em]"), "aria-hidden": true, children: [_jsxs("defs", { children: [_jsxs("linearGradient", { id: "dts-5", x1: "3.3", y1: "107.99", x2: "219.27", y2: "107.99", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#ff9199" }), _jsx("stop", { offset: "0", stopColor: "#ff9199" }), _jsx("stop", { offset: ".29", stopColor: "#fb799e" }), _jsx("stop", { offset: ".88", stopColor: "#f13cad" }), _jsx("stop", { offset: "1", stopColor: "#ef2fb1" })] }), _jsx("linearGradient", { id: "dts-51", x1: "134.03", y1: "238.72", x2: "350", y2: "238.72", href: "#dts-5" }), _jsx("linearGradient", { id: "dts-52", y1: "369.45", y2: "369.45", href: "#dts-5" })] }), _jsx("path", { fill: mono ? "currentColor" : "url(#dts-5)", d: "M111.19,0c-5.99.03-11.98,2.32-16.55,6.9L10.19,91.34c-9.19,9.19-9.19,24.1,0,33.29l84.44,84.44c9.19,9.19,24.1,9.19,33.29,0l84.44-84.44c9.19-9.19,9.19-24.1,0-33.29L127.93,6.9C123.36,2.32,117.37.03,111.38,0h-.2Z" }), _jsx("path", { fill: mono ? "currentColor" : "url(#dts-51)", d: "M225.37,137.62l-84.44,84.44c-9.19,9.19-9.19,24.1,0,33.29l84.44,84.44c9.19,9.19,24.1,9.19,33.29,0l84.44-84.44c9.19-9.19,9.19-24.1,0-33.29l-84.44-84.44c-4.6-4.6-10.62-6.89-16.65-6.89s-12.05,2.3-16.65,6.89" }), _jsx("path", { fill: mono ? "currentColor" : "url(#dts-52)", d: "M94.64,268.36L10.19,352.8c-9.19,9.19-9.19,24.1,0,33.29l84.44,84.44c4.57,4.57,10.56,6.87,16.55,6.89h.2c5.99-.03,11.97-2.32,16.55-6.89l84.44-84.44c9.19-9.19,9.19-24.1,0-33.29l-84.44-84.44c-4.6-4.6-10.62-6.9-16.65-6.9s-12.05,2.3-16.65,6.9" }), _jsx("path", { fill: "currentColor", d: "M83.21,224.65l-13.56-13.56c-.29-.29-.61-.55-.94-.77l-34.75-34.75c-2.51-2.51-6.57-2.51-9.07,0L1.88,198.57c-2.5,2.5-2.5,6.56,0,9.07l31.08,31.08-31.08,31.08c-2.5,2.5-2.5,6.56,0,9.07l23.01,23.01c2.5,2.5,6.56,2.5,9.07,0l49.25-49.25c7.72-7.72,7.72-20.24,0-27.96" })] }));
|
|
9
|
+
}
|
|
10
|
+
/** Logo horizontal oficial. Tamanho segue o font-size (altura ≈ 1,3em). */
|
|
11
|
+
export function Logo({ className, tone = "auto", mono }) {
|
|
12
|
+
return (_jsx("span", { className: cn("inline-flex items-center leading-none", !className?.includes("text-[") && "text-[22px]", tone === "light" ? "text-white" : "text-[#1D004E] dark:text-white", className), children: _jsxs("svg", { viewBox: "0 0 1000 272.06", className: "h-[1.3em] w-auto", role: "img", "aria-label": "dtcode", children: [_jsxs("defs", { children: [_jsxs("linearGradient", { id: "dtl-5", x1: "328.61", y1: "192.23", x2: "328.61", y2: "32.14", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#ff9199" }), _jsx("stop", { offset: "0", stopColor: "#ff9199" }), _jsx("stop", { offset: ".29", stopColor: "#fb799e" }), _jsx("stop", { offset: ".88", stopColor: "#f13cad" }), _jsx("stop", { offset: "1", stopColor: "#ef2fb1" })] }), _jsx("linearGradient", { id: "dtl-51", x1: "438.74", y1: "190.81", x2: "438.74", y2: "50.08", href: "#dtl-5" }), _jsx("linearGradient", { id: "dtl-52", x1: "1.88", y1: "61.53", x2: "124.95", y2: "61.53", href: "#dtl-5" }), _jsx("linearGradient", { id: "dtl-53", x1: "76.37", y1: "136.03", x2: "199.44", y2: "136.03", href: "#dtl-5" }), _jsx("linearGradient", { id: "dtl-54", x1: "1.88", y1: "210.53", x2: "124.95", y2: "210.53", href: "#dtl-5" })] }), _jsx("path", { fill: mono ? "currentColor" : "url(#dtl-5)", d: "M294.56,134.52c0-31.81,5.3-38.34,34.47-38.34s34.26,7.14,34.26,31v14.89c0,23.66-6.53,30.8-34.26,30.8-29.16,0-34.47-6.32-34.47-38.34M380.21,32.14c-9.46,0-17.13,7.67-17.13,17.13v46.09h-.41c-5.3-12.44-12.44-18.35-40.79-18.35-38.75,0-48.95,14.07-48.95,57.51s10.2,57.72,48.95,57.72c28.35,0,36.51-6.32,41.2-18.56h.41c0,9.46,7.67,17.13,17.13,17.13h3.67V32.14h-4.08Z" }), _jsx("path", { fill: mono ? "currentColor" : "url(#dtl-51)", d: "M419.57,50.08v28.35h-15.3v18.56h15.3v64.24c0,20.6,7.75,29.57,30.39,29.57h23.25v-18.76h-15.3c-13.87,0-17.13-3.06-17.13-15.09v-59.96h32.43v-18.56h-32.43v-28.35h-21.21Z" }), _jsx("path", { fill: "currentColor", d: "M489.53,134.52c0-43.85,12.44-57.72,54.45-57.72,36.1,0,54.45,8.36,54.45,43.24v1.43h-21.21v-.41c0-21.41-10.4-24.68-33.24-24.68-26.72,0-32.84,6.53-32.84,38.14s6.12,38.14,32.84,38.14c25.09,0,33.45-3.67,33.45-26.92v-.41h21.21v1.43c0,37.32-17.34,45.68-54.66,45.68-42.01,0-54.45-13.87-54.45-57.92" }), _jsx("path", { fill: "currentColor", d: "M619.64,134.52c0-43.64,11.42-57.72,56.49-57.72s56.7,14.07,56.7,57.72-11.63,57.92-56.7,57.92-56.49-14.07-56.49-57.92M676.14,172.65c29.78,0,35.08-6.93,35.08-38.14s-5.3-38.14-35.08-38.14-34.87,6.93-34.87,38.14,5.3,38.14,34.87,38.14" }), _jsx("path", { fill: "currentColor", d: "M754.66,134.52c0-43.44,10.2-57.51,48.95-57.51,28.35,0,35.49,5.91,40.79,18.35h.41v-63.22h.41c11.49,0,20.8,9.31,20.8,20.8v117.06c0,11.49-9.31,20.8-20.8,20.8v-17.13h-.41c-4.69,12.24-12.85,18.56-41.2,18.56-38.75,0-48.95-14.07-48.95-57.72M810.74,172.86c27.74,0,34.26-7.14,34.26-30.8v-14.89c0-23.86-6.52-31-34.26-31-29.16,0-34.47,6.53-34.47,38.34s5.3,38.34,34.47,38.34" }), _jsx("path", { fill: "currentColor", d: "M891.3,134.72c0-44.05,11.22-57.92,55.27-57.92,41.4,0,53.43,12.64,53.43,52.82v12.24h-87.49c0,25.7,5.51,32.22,35.49,32.22,23.05,0,30.8-4.28,30.8-19.58v-.82h21.01v1.63c0,27.74-13.87,37.12-52.01,37.12-45.27,0-56.49-13.46-56.49-57.72M978.79,123.71v-1.63c0-20.8-6.12-27.12-32.22-27.12s-33.65,6.32-34.06,28.76h66.28Z" }), _jsx("path", { fill: mono ? "currentColor" : "url(#dtl-52)", d: "M63.36,0c-3.41.01-6.82,1.32-9.43,3.93L5.81,52.05c-5.24,5.24-5.24,13.73,0,18.97l48.12,48.12c5.24,5.24,13.73,5.24,18.97,0l48.12-48.12c5.24-5.24,5.24-13.73,0-18.97L72.9,3.93c-2.61-2.61-6.02-3.92-9.43-3.93h-.11Z" }), _jsx("path", { fill: mono ? "currentColor" : "url(#dtl-53)", d: "M128.42,78.42l-48.12,48.12c-5.24,5.24-5.24,13.73,0,18.97l48.12,48.12c5.24,5.24,13.73,5.24,18.97,0l48.12-48.12c5.24-5.24,5.24-13.73,0-18.97l-48.12-48.12c-2.62-2.62-6.05-3.93-9.49-3.93s-6.87,1.31-9.49,3.93" }), _jsx("path", { fill: mono ? "currentColor" : "url(#dtl-54)", d: "M53.93,152.92L5.81,201.04c-5.24,5.24-5.24,13.73,0,18.97l48.12,48.12c2.6,2.6,6.01,3.91,9.43,3.93h.11c3.41-.01,6.82-1.32,9.43-3.93l48.12-48.12c5.24-5.24,5.24-13.73,0-18.97l-48.12-48.12c-2.62-2.62-6.05-3.93-9.49-3.93s-6.87,1.31-9.49,3.93" }), _jsx("path", { fill: "currentColor", d: "M47.42,128.02l-7.73-7.73c-.17-.17-.35-.31-.54-.44l-19.8-19.8c-1.43-1.43-3.74-1.43-5.17,0l-13.11,13.11c-1.43,1.43-1.43,3.74,0,5.17l17.71,17.71L1.07,153.74c-1.43,1.43-1.43,3.74,0,5.17l13.11,13.11c1.43,1.43,3.74,1.43,5.17,0l28.07-28.07c4.4-4.4,4.4-11.54,0-15.94" })] }) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as RadixCheckbox from "@radix-ui/react-checkbox";
|
|
2
|
+
export type CheckboxProps = React.ComponentProps<typeof RadixCheckbox.Root> & {
|
|
3
|
+
/** Texto ao lado da caixa; clicar nele também marca. */
|
|
4
|
+
label?: React.ReactNode;
|
|
5
|
+
invalid?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Caixa de seleção para escolhas que dependem de "Salvar". Para efeito imediato, use Switch. */
|
|
8
|
+
export declare function Checkbox({ label, invalid, className, id, ...props }: CheckboxProps): import("react").JSX.Element;
|
package/dist/checkbox.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as RadixCheckbox from "@radix-ui/react-checkbox";
|
|
4
|
+
import { cn } from "./cn.js";
|
|
5
|
+
import { Icon } from "./icon.js";
|
|
6
|
+
/** Caixa de seleção para escolhas que dependem de "Salvar". Para efeito imediato, use Switch. */
|
|
7
|
+
export function Checkbox({ label, invalid, className, id, ...props }) {
|
|
8
|
+
const box = (_jsx(RadixCheckbox.Root, { id: id, "aria-invalid": invalid || undefined, className: cn("peer grid size-[18px] flex-none place-items-center rounded-[5px] border border-border-strong bg-surface text-white transition-[background-color,border-color,box-shadow]", "hover:border-neutro-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus dark:hover:border-white/30", "data-[state=checked]:border-magenta-600 data-[state=checked]:bg-magenta-600 data-[state=indeterminate]:border-magenta-600 data-[state=indeterminate]:bg-magenta-600", "disabled:cursor-not-allowed disabled:opacity-50", invalid && "border-danger", !label && className), ...props, children: _jsx(RadixCheckbox.Indicator, { className: "grid place-items-center", children: _jsx(Icon, { name: props.checked === "indeterminate" ? "remove" : "check", size: 15, className: "animate-pop [font-variation-settings:'FILL'_1,'wght'_700]" }) }) }));
|
|
9
|
+
if (!label)
|
|
10
|
+
return box;
|
|
11
|
+
return (_jsxs("label", { htmlFor: id, className: cn("flex cursor-pointer items-start gap-2.5 text-sm text-fg has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-60", className), children: [_jsx("span", { className: "mt-px", children: box }), _jsx("span", { children: label })] }));
|
|
12
|
+
}
|
package/dist/dialog.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as RadixDialog from "@radix-ui/react-dialog";
|
|
2
|
+
export declare const Dialog: import("react").FC<RadixDialog.DialogProps>;
|
|
3
|
+
export declare const DialogTrigger: import("react").ForwardRefExoticComponent<RadixDialog.DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
4
|
+
export declare const DialogClose: import("react").ForwardRefExoticComponent<RadixDialog.DialogCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
5
|
+
/** Fundo escurecido das janelas sobrepostas (Dialog e Sheet). */
|
|
6
|
+
export declare const overlayClasses = "fixed inset-0 z-50 animate-fade-in bg-roxo-950/50 backdrop-blur-[2px]";
|
|
7
|
+
declare const widths: {
|
|
8
|
+
sm: string;
|
|
9
|
+
md: string;
|
|
10
|
+
lg: string;
|
|
11
|
+
};
|
|
12
|
+
export type DialogContentProps = Omit<React.ComponentProps<typeof RadixDialog.Content>, "title"> & {
|
|
13
|
+
/** Título anunciado ao abrir (obrigatório para leitores de tela). */
|
|
14
|
+
title: React.ReactNode;
|
|
15
|
+
description?: React.ReactNode;
|
|
16
|
+
size?: keyof typeof widths;
|
|
17
|
+
/** Esconde o botão de fechar do canto (Esc e clique fora continuam fechando). */
|
|
18
|
+
hideClose?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/** Janela modal: foco preso, Esc fecha, título e descrição ligados ao diálogo. */
|
|
21
|
+
export declare function DialogContent({ title, description, size, hideClose, className, children, ...props }: DialogContentProps): import("react").JSX.Element;
|
|
22
|
+
/** Ações do rodapé: empilhadas no celular, à direita a partir de 640px. A principal vem por último. */
|
|
23
|
+
export declare function DialogFooter({ className, ...props }: React.ComponentProps<"div">): import("react").JSX.Element;
|
|
24
|
+
export {};
|
package/dist/dialog.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as RadixDialog from "@radix-ui/react-dialog";
|
|
4
|
+
import { cn } from "./cn.js";
|
|
5
|
+
import { Icon } from "./icon.js";
|
|
6
|
+
export const Dialog = RadixDialog.Root;
|
|
7
|
+
export const DialogTrigger = RadixDialog.Trigger;
|
|
8
|
+
export const DialogClose = RadixDialog.Close;
|
|
9
|
+
/** Fundo escurecido das janelas sobrepostas (Dialog e Sheet). */
|
|
10
|
+
export const overlayClasses = "fixed inset-0 z-50 animate-fade-in bg-roxo-950/50 backdrop-blur-[2px]";
|
|
11
|
+
const widths = { sm: "max-w-[400px]", md: "max-w-[520px]", lg: "max-w-[720px]" };
|
|
12
|
+
/** Janela modal: foco preso, Esc fecha, título e descrição ligados ao diálogo. */
|
|
13
|
+
export function DialogContent({ title, description, size = "md", hideClose, className, children, ...props }) {
|
|
14
|
+
return (_jsxs(RadixDialog.Portal, { children: [_jsx(RadixDialog.Overlay, { className: overlayClasses }), _jsxs(RadixDialog.Content
|
|
15
|
+
// Sem descrição, o Radix pede aria-describedby explícito para não avisar no console.
|
|
16
|
+
, { ...(description ? {} : { "aria-describedby": undefined }), className: cn("fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-32px)] w-[calc(100%-32px)] -translate-x-1/2 -translate-y-1/2 animate-scale-in gap-5 overflow-y-auto", "rounded-xl border border-border bg-surface p-6 text-fg shadow-4 focus:outline-none", widths[size], className), ...props, children: [_jsxs("div", { className: cn("grid gap-1.5", !hideClose && "pr-10"), children: [_jsx(RadixDialog.Title, { className: "font-display text-xl leading-snug font-semibold", children: title }), description && _jsx(RadixDialog.Description, { className: "text-[15px] text-fg-2", children: description })] }), children, !hideClose && (_jsx(RadixDialog.Close, { "aria-label": "Fechar", className: "absolute top-4 right-4 grid size-9 place-items-center rounded-md text-muted transition-colors hover:bg-surface-subtle hover:text-fg", children: _jsx(Icon, { name: "close", size: 20 }) }))] })] }));
|
|
17
|
+
}
|
|
18
|
+
/** Ações do rodapé: empilhadas no celular, à direita a partir de 640px. A principal vem por último. */
|
|
19
|
+
export function DialogFooter({ className, ...props }) {
|
|
20
|
+
return _jsx("div", { className: cn("flex flex-col-reverse gap-2 pt-1 sm:flex-row sm:justify-end", className), ...props });
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as RadixMenu from "@radix-ui/react-dropdown-menu";
|
|
2
|
+
export declare const DropdownMenu: import("react").FC<RadixMenu.DropdownMenuProps>;
|
|
3
|
+
export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<RadixMenu.DropdownMenuTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
4
|
+
export declare const DropdownMenuGroup: import("react").ForwardRefExoticComponent<RadixMenu.DropdownMenuGroupProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
5
|
+
/** Lista suspensa de ações (menu da conta, notificações, "mais ações"). */
|
|
6
|
+
export declare function DropdownMenuContent({ className, sideOffset, align, ...props }: React.ComponentProps<typeof RadixMenu.Content>): import("react").JSX.Element;
|
|
7
|
+
export declare function DropdownMenuItem({ icon, tone, className, children, ...props }: React.ComponentProps<typeof RadixMenu.Item> & {
|
|
8
|
+
icon?: string;
|
|
9
|
+
tone?: "danger";
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
export declare function DropdownMenuLabel({ className, ...props }: React.ComponentProps<typeof RadixMenu.Label>): import("react").JSX.Element;
|
|
12
|
+
export declare function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof RadixMenu.Separator>): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as RadixMenu from "@radix-ui/react-dropdown-menu";
|
|
4
|
+
import { cn } from "./cn.js";
|
|
5
|
+
import { Icon } from "./icon.js";
|
|
6
|
+
export const DropdownMenu = RadixMenu.Root;
|
|
7
|
+
export const DropdownMenuTrigger = RadixMenu.Trigger;
|
|
8
|
+
export const DropdownMenuGroup = RadixMenu.Group;
|
|
9
|
+
/** Lista suspensa de ações (menu da conta, notificações, "mais ações"). */
|
|
10
|
+
export function DropdownMenuContent({ className, sideOffset = 6, align = "end", ...props }) {
|
|
11
|
+
return (_jsx(RadixMenu.Portal, { children: _jsx(RadixMenu.Content, { sideOffset: sideOffset, align: align, className: cn("z-50 min-w-[200px] origin-(--radix-dropdown-menu-content-transform-origin) animate-scale-in overflow-hidden rounded-lg border border-border bg-surface p-1 text-fg shadow-4", className), ...props }) }));
|
|
12
|
+
}
|
|
13
|
+
export function DropdownMenuItem({ icon, tone, className, children, ...props }) {
|
|
14
|
+
return (_jsxs(RadixMenu.Item, { className: cn("flex cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-2 text-sm outline-none select-none", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-surface-subtle", tone === "danger" ? "text-danger" : "text-fg", className), ...props, children: [icon && _jsx(Icon, { name: icon, size: 18, className: tone === "danger" ? undefined : "text-fg-2" }), children] }));
|
|
15
|
+
}
|
|
16
|
+
export function DropdownMenuLabel({ className, ...props }) {
|
|
17
|
+
return _jsx(RadixMenu.Label, { className: cn("px-2.5 pt-2 pb-1 text-xs font-semibold text-muted", className), ...props });
|
|
18
|
+
}
|
|
19
|
+
export function DropdownMenuSeparator({ className, ...props }) {
|
|
20
|
+
return _jsx(RadixMenu.Separator, { className: cn("-mx-1 my-1 h-px bg-border", className), ...props });
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type EmptyStateProps = {
|
|
2
|
+
title: React.ReactNode;
|
|
3
|
+
description?: React.ReactNode;
|
|
4
|
+
/** Material Symbol. Padrão: "inbox" (vazio) ou "error" (erro). */
|
|
5
|
+
icon?: string;
|
|
6
|
+
/** "error" para quando a lista não carregou. */
|
|
7
|
+
tone?: "empty" | "error";
|
|
8
|
+
/** Ação principal, por exemplo um <Button>. */
|
|
9
|
+
action?: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
/** Tela ou bloco sem conteúdo: diz o que aconteceu e o que fazer em seguida. */
|
|
13
|
+
export declare function EmptyState({ title, description, icon, tone, action, className }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "./cn.js";
|
|
3
|
+
import { Icon } from "./icon.js";
|
|
4
|
+
/** Tela ou bloco sem conteúdo: diz o que aconteceu e o que fazer em seguida. */
|
|
5
|
+
export function EmptyState({ title, description, icon, tone = "empty", action, className }) {
|
|
6
|
+
const error = tone === "error";
|
|
7
|
+
return (_jsxs("div", { role: error ? "alert" : undefined, className: cn("grid justify-items-center gap-3 rounded-lg border border-dashed border-border-strong px-6 py-12 text-center", className), children: [_jsx("span", { className: cn("grid size-12 place-items-center rounded-[14px]", error ? "bg-danger-bg text-danger-fg" : "bg-tint text-tint-fg"), children: _jsx(Icon, { name: icon ?? (error ? "error" : "inbox"), size: 24 }) }), _jsxs("div", { className: "grid max-w-[46ch] gap-1", children: [_jsx("strong", { className: "font-display text-base font-semibold text-fg", children: title }), description && _jsx("p", { className: "text-sm text-fg-2", children: description })] }), action && _jsx("div", { className: "mt-1", children: action })] }));
|
|
8
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
export * from "./alert.js";
|
|
2
|
+
export * from "./animated-symbol.js";
|
|
3
|
+
export * from "./avatar.js";
|
|
2
4
|
export * from "./badge.js";
|
|
5
|
+
export * from "./brand.js";
|
|
3
6
|
export * from "./button.js";
|
|
4
7
|
export * from "./card.js";
|
|
8
|
+
export * from "./checkbox.js";
|
|
9
|
+
export * from "./dialog.js";
|
|
10
|
+
export * from "./dropdown-menu.js";
|
|
11
|
+
export * from "./empty-state.js";
|
|
5
12
|
export * from "./icon.js";
|
|
6
13
|
export * from "./input.js";
|
|
7
14
|
export * from "./loader.js";
|
|
15
|
+
export * from "./login-template.js";
|
|
8
16
|
export * from "./segmented.js";
|
|
17
|
+
export * from "./select.js";
|
|
18
|
+
export * from "./sheet.js";
|
|
19
|
+
export * from "./skeleton.js";
|
|
9
20
|
export * from "./switch.js";
|
|
21
|
+
export * from "./table.js";
|
|
10
22
|
export * from "./tabs.js";
|
|
23
|
+
export * from "./textarea.js";
|
|
24
|
+
export * from "./theme.js";
|
|
25
|
+
export * from "./toast.js";
|
|
26
|
+
export { initials } from "./initials.js";
|
|
27
|
+
export { themeScript } from "./theme-script.js";
|
|
11
28
|
export * from "./cn.js";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
export * from "./alert.js";
|
|
2
|
+
export * from "./animated-symbol.js";
|
|
3
|
+
export * from "./avatar.js";
|
|
2
4
|
export * from "./badge.js";
|
|
5
|
+
export * from "./brand.js";
|
|
3
6
|
export * from "./button.js";
|
|
4
7
|
export * from "./card.js";
|
|
8
|
+
export * from "./checkbox.js";
|
|
9
|
+
export * from "./dialog.js";
|
|
10
|
+
export * from "./dropdown-menu.js";
|
|
11
|
+
export * from "./empty-state.js";
|
|
5
12
|
export * from "./icon.js";
|
|
6
13
|
export * from "./input.js";
|
|
7
14
|
export * from "./loader.js";
|
|
15
|
+
export * from "./login-template.js";
|
|
8
16
|
export * from "./segmented.js";
|
|
17
|
+
export * from "./select.js";
|
|
18
|
+
export * from "./sheet.js";
|
|
19
|
+
export * from "./skeleton.js";
|
|
9
20
|
export * from "./switch.js";
|
|
21
|
+
export * from "./table.js";
|
|
10
22
|
export * from "./tabs.js";
|
|
23
|
+
export * from "./textarea.js";
|
|
24
|
+
export * from "./theme.js";
|
|
25
|
+
export * from "./toast.js";
|
|
26
|
+
export { initials } from "./initials.js";
|
|
27
|
+
export { themeScript } from "./theme-script.js";
|
|
11
28
|
export * from "./cn.js";
|
package/dist/initials.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Fora do Avatar ("use client") para poder ser usado também no servidor.
|
|
2
|
+
/** Até duas iniciais: primeiro e último nome ("Ana Paula Souza" → "AS"). */
|
|
3
|
+
export function initials(name) {
|
|
4
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
5
|
+
if (!parts.length)
|
|
6
|
+
return "?";
|
|
7
|
+
const first = parts[0][0];
|
|
8
|
+
const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
|
|
9
|
+
return (first + last).toUpperCase();
|
|
10
|
+
}
|
package/dist/input.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ export type InputProps = React.ComponentProps<"input"> & {
|
|
|
2
2
|
icon?: string;
|
|
3
3
|
invalid?: boolean;
|
|
4
4
|
};
|
|
5
|
+
/** Borda, foco magenta, erro e desabilitado dos campos (Input, Select, Textarea). */
|
|
6
|
+
export declare function controlClasses({ invalid, className }?: {
|
|
7
|
+
invalid?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
}): string;
|
|
5
10
|
export declare function Input({ icon, invalid, className, ...props }: InputProps): import("react").JSX.Element;
|
|
6
11
|
type FieldProps = {
|
|
7
12
|
label: React.ReactNode;
|
package/dist/input.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "./cn.js";
|
|
3
3
|
import { Icon } from "./icon.js";
|
|
4
|
+
/** Borda, foco magenta, erro e desabilitado dos campos (Input, Select, Textarea). */
|
|
5
|
+
export function controlClasses({ invalid, className } = {}) {
|
|
6
|
+
return cn("w-full rounded-md border bg-surface px-3 text-sm text-fg placeholder:text-muted", "border-border-strong transition-[border-color,box-shadow,background-color] duration-200 hover:border-neutro-400 dark:hover:border-white/30", "focus:border-magenta-500 focus:shadow-[0_0_0_3px_rgb(239_47_177/.18)] focus:outline-none", "disabled:cursor-not-allowed disabled:bg-surface-sunken disabled:opacity-70", invalid && "border-danger focus:border-danger focus:shadow-[0_0_0_3px_rgb(209_42_31/.18)]", className);
|
|
7
|
+
}
|
|
4
8
|
export function Input({ icon, invalid, className, ...props }) {
|
|
5
|
-
const input = (_jsx("input", { "aria-invalid": invalid || undefined, className:
|
|
9
|
+
const input = (_jsx("input", { "aria-invalid": invalid || undefined, className: controlClasses({ invalid, className: cn("peer h-10", icon && "pl-9", className) }), ...props }));
|
|
6
10
|
if (!icon)
|
|
7
11
|
return input;
|
|
8
12
|
return (_jsxs("div", { className: "relative", children: [input, _jsx(Icon, { name: icon, size: 18, className: "pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-muted transition-colors peer-focus:text-accent" })] }));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type LoginValues = {
|
|
3
|
+
email: string;
|
|
4
|
+
password: string;
|
|
5
|
+
};
|
|
6
|
+
export type LoginFieldErrors = Partial<Record<keyof LoginValues, string>>;
|
|
7
|
+
/** Botão de entrar com um provedor (Google, Microsoft, SSO da empresa). */
|
|
8
|
+
export type LoginProvider = {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
/** Logo do provedor (SVG) ou nome de um Material Symbol. */
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
/** Link de início do login (ex.: /api/auth/signin/google). O botão mostra o carregamento ao clicar. */
|
|
14
|
+
href?: string;
|
|
15
|
+
onClick?: () => void | Promise<void>;
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type LoginTemplateProps = {
|
|
19
|
+
/** Recebe e-mail e senha validados. Opcional com ssoOnly. */
|
|
20
|
+
onSubmit?: (values: LoginValues) => void | Promise<void>;
|
|
21
|
+
/** Botões de provedor, acima do formulário. */
|
|
22
|
+
providers?: LoginProvider[];
|
|
23
|
+
/** Só os provedores: esconde e-mail e senha. */
|
|
24
|
+
ssoOnly?: boolean;
|
|
25
|
+
/** Aviso acima do formulário, por exemplo <Alert tone="success">Senha redefinida.</Alert>. */
|
|
26
|
+
notice?: ReactNode;
|
|
27
|
+
error?: ReactNode;
|
|
28
|
+
fieldErrors?: LoginFieldErrors;
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
eyebrow?: ReactNode;
|
|
31
|
+
title?: ReactNode;
|
|
32
|
+
description?: ReactNode;
|
|
33
|
+
brand?: ReactNode;
|
|
34
|
+
quote?: {
|
|
35
|
+
text: string;
|
|
36
|
+
author?: string;
|
|
37
|
+
} | null;
|
|
38
|
+
emailPlaceholder?: string;
|
|
39
|
+
forgotPasswordHref?: string;
|
|
40
|
+
submitLabel?: string;
|
|
41
|
+
footer?: ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function LoginTemplate({ onSubmit, providers, ssoOnly, notice, error, fieldErrors, loading, eyebrow, title, description, brand, quote, emailPlaceholder, forgotPasswordHref, submitLabel, footer, className, }: LoginTemplateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Alert } from "./alert.js";
|
|
5
|
+
import { AnimatedSymbol } from "./animated-symbol.js";
|
|
6
|
+
import { Button, buttonClasses } from "./button.js";
|
|
7
|
+
import { Icon } from "./icon.js";
|
|
8
|
+
import { Field, Input } from "./input.js";
|
|
9
|
+
import { Loader } from "./loader.js";
|
|
10
|
+
const DEFAULT_QUOTE = { text: "Um só sistema para todos os times entregarem com a mesma qualidade.", author: "dtcode" };
|
|
11
|
+
function validate({ email, password }) {
|
|
12
|
+
const errors = {};
|
|
13
|
+
if (!email)
|
|
14
|
+
errors.email = "Informe seu e-mail.";
|
|
15
|
+
else if (!/^\S+@\S+\.\S+$/.test(email))
|
|
16
|
+
errors.email = "Confira o e-mail.";
|
|
17
|
+
if (!password)
|
|
18
|
+
errors.password = "Informe sua senha.";
|
|
19
|
+
return errors;
|
|
20
|
+
}
|
|
21
|
+
function ProviderButton({ provider, disabled }) {
|
|
22
|
+
const [pending, setPending] = useState(false);
|
|
23
|
+
const busy = provider.loading || pending;
|
|
24
|
+
const icon = typeof provider.icon === "string" ? _jsx(Icon, { name: provider.icon, size: 20 }) : provider.icon;
|
|
25
|
+
const content = (_jsxs(_Fragment, { children: [busy ? _jsx(Loader, { size: "sm", label: `${provider.label}, carregando` }) : icon, provider.label] }));
|
|
26
|
+
const cls = buttonClasses({ variant: "secondary", size: "lg", block: true, loading: busy });
|
|
27
|
+
if (provider.href) {
|
|
28
|
+
return (_jsx("a", { href: provider.href, onClick: () => setPending(true), "aria-disabled": busy || disabled || undefined, className: cls, children: content }));
|
|
29
|
+
}
|
|
30
|
+
return (_jsx("button", { type: "button", disabled: disabled || busy, "aria-busy": busy || undefined, className: cls, onClick: async () => {
|
|
31
|
+
setPending(true);
|
|
32
|
+
try {
|
|
33
|
+
await provider.onClick?.();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
setPending(false);
|
|
37
|
+
}
|
|
38
|
+
}, children: content }));
|
|
39
|
+
}
|
|
40
|
+
export function LoginTemplate({ onSubmit, providers = [], ssoOnly, notice, error, fieldErrors, loading, eyebrow = "dtcode", title = "Boas-vindas", description = "Entre com sua conta para continuar.", brand, quote = DEFAULT_QUOTE, emailPlaceholder = "voce@dtcode.com.br", forgotPasswordHref, submitLabel = "Entrar", footer, className = "min-h-dvh", }) {
|
|
41
|
+
const [localErrors, setLocalErrors] = useState({});
|
|
42
|
+
const [submitting, setSubmitting] = useState(false);
|
|
43
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
44
|
+
const [capsLock, setCapsLock] = useState(false);
|
|
45
|
+
const errors = { ...fieldErrors, ...localErrors };
|
|
46
|
+
const busy = loading || submitting;
|
|
47
|
+
const handleSubmit = async (e) => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
const form = new FormData(e.currentTarget);
|
|
50
|
+
const values = { email: String(form.get("email") ?? "").trim(), password: String(form.get("password") ?? "") };
|
|
51
|
+
const found = validate(values);
|
|
52
|
+
setLocalErrors(found);
|
|
53
|
+
if (Object.keys(found).length) {
|
|
54
|
+
e.currentTarget.querySelector(`#${found.email ? "email" : "password"}`)?.focus();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setSubmitting(true);
|
|
58
|
+
try {
|
|
59
|
+
await onSubmit?.(values);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
setSubmitting(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const onPasswordKey = (e) => setCapsLock(e.getModifierState("CapsLock"));
|
|
66
|
+
return (_jsx("div", { className: `@container bg-bg text-fg ${className}`, children: _jsxs("div", { className: "grid h-full min-h-[inherit] grid-rows-[auto_1fr] @4xl:grid-cols-2 @4xl:grid-rows-none", children: [_jsxs("aside", { className: "relative flex min-h-56 flex-col overflow-hidden bg-[image:var(--grad-hero)] p-6 text-white @lg:p-10", children: [_jsx("div", { "aria-hidden": true, className: "absolute inset-0 bg-[radial-gradient(rgb(255_255_255/.14)_1px,transparent_1.2px)] bg-[size:18px_18px] [mask-image:linear-gradient(90deg,transparent_30%,#000)]" }), _jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute -right-24 -bottom-20 size-80 rotate-45 rounded-[28%] border-2 border-white/15" }), _jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute right-36 bottom-28 size-44 rotate-45 rounded-[28%] border-2 border-white/15" }), _jsx("div", { className: "relative z-10 grid flex-1 place-items-center", children: brand ?? _jsx(AnimatedSymbol, { className: "size-32 @4xl:size-44" }) }), quote && (_jsxs("blockquote", { className: "relative z-10 hidden max-w-md font-display text-2xl leading-snug font-medium @4xl:block", children: ["\u201C", quote.text, "\u201D", quote.author && _jsx("span", { className: "mt-3 block font-sans text-sm font-normal text-white/65", children: quote.author })] }))] }), _jsx("main", { className: "grid place-items-center px-6 py-12 @lg:px-10", children: _jsxs("div", { className: "grid w-full max-w-[400px] gap-8", children: [_jsxs("header", { className: "grid gap-3", children: [_jsxs("p", { className: "font-display text-xs font-semibold tracking-[.12em] text-fg-2 uppercase", children: [_jsx("span", { className: "text-accent", children: "\u203A" }), " ", eyebrow] }), _jsx("h1", { className: "font-display text-3xl leading-tight font-bold", children: title }), _jsx("div", { className: "text-[15px] text-fg-2", children: description })] }), notice, error && _jsx(Alert, { tone: "danger", children: error }), providers.length > 0 && (_jsx("div", { className: "grid gap-3", children: providers.map((p) => (_jsx(ProviderButton, { provider: p, disabled: busy }, p.id))) })), providers.length > 0 && !ssoOnly && (_jsxs("div", { className: "flex items-center gap-3 text-xs font-semibold tracking-[.08em] text-muted uppercase", children: [_jsx("span", { className: "h-px flex-1 bg-border" }), "ou com e-mail", _jsx("span", { className: "h-px flex-1 bg-border" })] })), !ssoOnly && (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-5", noValidate: true, "aria-busy": busy, children: [_jsx(Field, { label: "E-mail", htmlFor: "email", error: errors.email, children: _jsx(Input, { id: "email", name: "email", type: "email", icon: "mail", autoComplete: "email", placeholder: emailPlaceholder, invalid: !!errors.email, "aria-describedby": errors.email ? "email-error" : undefined, className: "h-12 text-[15px]", required: true }) }), _jsx(Field, { label: "Senha", htmlFor: "password", error: errors.password, action: forgotPasswordHref && (_jsx("a", { href: forgotPasswordHref, className: "text-[13px] font-semibold text-link hover:underline", children: "Esqueci minha senha" })), hint: capsLock && (_jsxs("span", { className: "flex items-center gap-1 font-medium text-warning", children: [_jsx(Icon, { name: "warning", size: 14 }), " Caps Lock est\u00E1 ativado"] })), children: _jsxs("div", { className: "relative", children: [_jsx(Input, { id: "password", name: "password", type: showPassword ? "text" : "password", icon: "lock", autoComplete: "current-password", invalid: !!errors.password, "aria-describedby": errors.password ? "password-error" : capsLock ? "password-hint" : undefined, className: "h-12 pr-12 text-[15px]", onKeyUp: onPasswordKey, onKeyDown: onPasswordKey, required: true }), _jsx("button", { type: "button", onClick: () => setShowPassword((s) => !s), "aria-label": showPassword ? "Ocultar senha" : "Mostrar senha", "aria-pressed": showPassword, "aria-controls": "password", className: "absolute top-1/2 right-1.5 grid size-9 -translate-y-1/2 place-items-center rounded-md text-muted transition-colors hover:bg-surface-subtle hover:text-fg", children: _jsx(Icon, { name: showPassword ? "visibility_off" : "visibility", size: 20 }) })] }) }), _jsx(Button, { type: "submit", size: "lg", block: true, loading: busy, iconRight: "arrow_forward", className: "mt-1", children: submitLabel })] })), footer && _jsx("div", { className: "border-t border-border pt-5 text-[13px] text-muted", children: footer })] }) })] }) }));
|
|
67
|
+
}
|
package/dist/select.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type SelectProps = React.ComponentProps<"select"> & {
|
|
2
|
+
invalid?: boolean;
|
|
3
|
+
/** Primeira opção vazia, mostrada enquanto nada foi escolhido (ex.: "Selecione"). */
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
};
|
|
6
|
+
/** Campo de seleção nativo, com o visual do Input. Use dentro de <Field> para rótulo, dica e erro. */
|
|
7
|
+
export declare function Select({ invalid, placeholder, className, children, ...props }: SelectProps): import("react").JSX.Element;
|
package/dist/select.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "./cn.js";
|
|
3
|
+
import { Icon } from "./icon.js";
|
|
4
|
+
import { controlClasses } from "./input.js";
|
|
5
|
+
/** Campo de seleção nativo, com o visual do Input. Use dentro de <Field> para rótulo, dica e erro. */
|
|
6
|
+
export function Select({ invalid, placeholder, className, children, ...props }) {
|
|
7
|
+
return (_jsxs("div", { className: "relative", children: [_jsxs("select", { "aria-invalid": invalid || undefined, className: controlClasses({ invalid, className: cn("peer h-10 cursor-pointer appearance-none pr-10", className) }), ...props, children: [placeholder !== undefined && (_jsx("option", { value: "", disabled: props.required, children: placeholder })), children] }), _jsx(Icon, { name: "expand_more", size: 20, className: "pointer-events-none absolute top-1/2 right-2.5 -translate-y-1/2 text-muted transition-colors peer-focus:text-accent" })] }));
|
|
8
|
+
}
|
package/dist/sheet.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as RadixDialog from "@radix-ui/react-dialog";
|
|
2
|
+
export declare const Sheet: import("react").FC<RadixDialog.DialogProps>;
|
|
3
|
+
export declare const SheetTrigger: import("react").ForwardRefExoticComponent<RadixDialog.DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
4
|
+
export declare const SheetClose: import("react").ForwardRefExoticComponent<RadixDialog.DialogCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
5
|
+
export type SheetContentProps = Omit<React.ComponentProps<typeof RadixDialog.Content>, "title"> & {
|
|
6
|
+
title: React.ReactNode;
|
|
7
|
+
description?: React.ReactNode;
|
|
8
|
+
/** Ações fixas no rodapé do painel (ex.: botões Salvar e Cancelar). */
|
|
9
|
+
footer?: React.ReactNode;
|
|
10
|
+
};
|
|
11
|
+
/** Painel lateral à direita (até 520px) para ver ou editar um item sem sair da lista. */
|
|
12
|
+
export declare function SheetContent({ title, description, footer, className, children, ...props }: SheetContentProps): import("react").JSX.Element;
|
|
13
|
+
/** Bloco com título dentro do painel. */
|
|
14
|
+
export declare function SheetSection({ title, className, children }: {
|
|
15
|
+
title: React.ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
/** Lista de rótulo e valor (ex.: dados de um registro). */
|
|
20
|
+
export declare function DefinitionList({ items, className }: {
|
|
21
|
+
items: {
|
|
22
|
+
label: React.ReactNode;
|
|
23
|
+
value: React.ReactNode;
|
|
24
|
+
}[];
|
|
25
|
+
className?: string;
|
|
26
|
+
}): import("react").JSX.Element;
|