@fldev-solution/ui 0.1.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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +523 -0
- package/dist/styles.css +89 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fldev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @fldev-solution/ui
|
|
2
|
+
|
|
3
|
+
Primitivos e hooks do kernel de UI da fldev. Blocos compostos (login,
|
|
4
|
+
DataTable, LineChart…) não vêm por aqui: vêm pelo registry do shadcn, em
|
|
5
|
+
<https://ui.fldev.com.br/r/index.json>. O que todo produto usa igual vai por
|
|
6
|
+
npm; o que todo produto ajusta vai por registry.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm i @fldev-solution/ui
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Button, Alert, Field, Input, useModo } from "@fldev-solution/ui";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## CSS: a ordem importa
|
|
17
|
+
|
|
18
|
+
Os componentes não carregam cor própria: leem tokens semânticos que a marca
|
|
19
|
+
define. Sem o `tokens.css` de uma marca importado **antes**, nada tem cor.
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
@import "tailwindcss";
|
|
23
|
+
@import "@fontsource-variable/inter"; /* a fonte que a marca declara */
|
|
24
|
+
@import "./design-system/tokens/tokens.css"; /* valores — da marca */
|
|
25
|
+
@import "@fldev-solution/ui/styles.css"; /* nomes de utility — do kernel */
|
|
26
|
+
@source "../node_modules/@fldev-solution/ui/dist";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
O `@source` é obrigatório: o Tailwind v4 não varre `node_modules`, e sem ele
|
|
30
|
+
as classes usadas dentro dos componentes não entram no CSS final. Ajuste o
|
|
31
|
+
caminho relativo ao arquivo CSS.
|
|
32
|
+
|
|
33
|
+
O tema de uma marca também vem pelo registry:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx shadcn add https://ui.fldev.com.br/r/brand-demo-teal.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Modo e marca
|
|
40
|
+
|
|
41
|
+
`useModo` alterna claro/escuro. `useBrand` também troca a marca na raiz — use
|
|
42
|
+
só em white-label, e nunca em página que mostra duas marcas ao mesmo tempo:
|
|
43
|
+
escrever `data-brand` na raiz faz o documento inteiro casar com o seletor de
|
|
44
|
+
todas as marcas.
|
|
45
|
+
|
|
46
|
+
## Requisitos
|
|
47
|
+
|
|
48
|
+
React 18+ e Tailwind CSS v4.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { ClassValue } from 'clsx';
|
|
2
|
+
import * as _tanstack_react_table from '@tanstack/react-table';
|
|
3
|
+
import { RowData } from '@tanstack/react-table';
|
|
4
|
+
import * as react from 'react';
|
|
5
|
+
import { ButtonHTMLAttributes, HTMLAttributes, ReactNode, InputHTMLAttributes, ReactElement, TdHTMLAttributes, ThHTMLAttributes } from 'react';
|
|
6
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
7
|
+
import { VariantProps } from 'class-variance-authority';
|
|
8
|
+
|
|
9
|
+
declare function cn(...inputs: ClassValue[]): string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Conjunto de features do DataTable do kernel: ordenação, filtro de coluna,
|
|
13
|
+
* filtro global e paginação — nada além. Em v9 a feature precisa ser
|
|
14
|
+
* registrada para o estado dela existir; registrar o que não se usa é peso
|
|
15
|
+
* morto no bundle.
|
|
16
|
+
*
|
|
17
|
+
* Slots de row model vêm DEPOIS da feature que os requer.
|
|
18
|
+
*/
|
|
19
|
+
declare const dataTableFeatures: {
|
|
20
|
+
rowSortingFeature: _tanstack_react_table.TableFeature;
|
|
21
|
+
sortedRowModel: (table: _tanstack_react_table.Table<any, any>) => () => _tanstack_react_table.RowModel<any, any>;
|
|
22
|
+
columnFilteringFeature: _tanstack_react_table.TableFeature;
|
|
23
|
+
globalFilteringFeature: _tanstack_react_table.TableFeature;
|
|
24
|
+
filteredRowModel: (table: _tanstack_react_table.Table<any, any>) => () => _tanstack_react_table.RowModel<any, any>;
|
|
25
|
+
rowPaginationFeature: _tanstack_react_table.TableFeature;
|
|
26
|
+
paginatedRowModel: (table: _tanstack_react_table.Table<any, any>) => () => _tanstack_react_table.RowModel<any, any>;
|
|
27
|
+
};
|
|
28
|
+
type DataTableFeatures = typeof dataTableFeatures;
|
|
29
|
+
/**
|
|
30
|
+
* Column helper já amarrado às features do kernel. Sem isso, cada consumidor
|
|
31
|
+
* escreve `createColumnHelper<typeof dataTableFeatures, Linha>()` à mão e
|
|
32
|
+
* erra o primeiro parâmetro — que é justamente o que o v9 usa para saber
|
|
33
|
+
* quais APIs de coluna existem.
|
|
34
|
+
*/
|
|
35
|
+
declare function criarColumnHelper<TLinha extends RowData>(): _tanstack_react_table.ColumnHelper<{
|
|
36
|
+
rowSortingFeature: _tanstack_react_table.TableFeature;
|
|
37
|
+
sortedRowModel: (table: _tanstack_react_table.Table<any, any>) => () => _tanstack_react_table.RowModel<any, any>;
|
|
38
|
+
columnFilteringFeature: _tanstack_react_table.TableFeature;
|
|
39
|
+
globalFilteringFeature: _tanstack_react_table.TableFeature;
|
|
40
|
+
filteredRowModel: (table: _tanstack_react_table.Table<any, any>) => () => _tanstack_react_table.RowModel<any, any>;
|
|
41
|
+
rowPaginationFeature: _tanstack_react_table.TableFeature;
|
|
42
|
+
paginatedRowModel: (table: _tanstack_react_table.Table<any, any>) => () => _tanstack_react_table.RowModel<any, any>;
|
|
43
|
+
}, TLinha>;
|
|
44
|
+
|
|
45
|
+
type Modo = "light" | "dark";
|
|
46
|
+
/**
|
|
47
|
+
* Alterna claro/escuro no elemento raiz. É o que quase todo produto precisa:
|
|
48
|
+
* a marca é fixa, só o modo muda.
|
|
49
|
+
*/
|
|
50
|
+
declare function useModo(inicial?: Modo): {
|
|
51
|
+
modo: Modo;
|
|
52
|
+
setModo: react.Dispatch<react.SetStateAction<Modo>>;
|
|
53
|
+
alternarModo: () => void;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Para white-label: aplica marca E modo no elemento raiz.
|
|
57
|
+
*
|
|
58
|
+
* Cuidado: escrever `data-brand` na raiz faz o documento inteiro casar com
|
|
59
|
+
* `[data-brand="x"]`. Se a página mostra mais de uma marca ao mesmo tempo,
|
|
60
|
+
* use `useModo` e ponha `data-brand` no contêiner de cada marca — senão
|
|
61
|
+
* seletores escopados passam a casar tudo.
|
|
62
|
+
*/
|
|
63
|
+
declare function useBrand(marcaInicial: string, modoInicial?: Modo): {
|
|
64
|
+
marca: string;
|
|
65
|
+
setMarca: react.Dispatch<react.SetStateAction<string>>;
|
|
66
|
+
modo: Modo;
|
|
67
|
+
setModo: react.Dispatch<react.SetStateAction<Modo>>;
|
|
68
|
+
alternarModo: () => void;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
declare const botao: (props?: ({
|
|
72
|
+
variante?: "primary" | "secondary" | "outline" | "ghost" | "destructive" | null | undefined;
|
|
73
|
+
tamanho?: "sm" | "md" | "lg" | "icon" | null | undefined;
|
|
74
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
75
|
+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof botao> {
|
|
76
|
+
carregando?: boolean;
|
|
77
|
+
}
|
|
78
|
+
declare function Button({ className, variante, tamanho, carregando, disabled, children, ...props }: ButtonProps): react.JSX.Element;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* As quatro famílias são estruturalmente idênticas — só o token muda. Isso é
|
|
82
|
+
* de propósito: alerta que difere em layout entre níveis ensina o usuário a
|
|
83
|
+
* ler layout em vez de cor, e aí o nível deixa de comunicar.
|
|
84
|
+
*/
|
|
85
|
+
declare const alerta: (props?: ({
|
|
86
|
+
nivel?: "success" | "info" | "warning" | "danger" | null | undefined;
|
|
87
|
+
variante?: "surface" | "solid" | null | undefined;
|
|
88
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
89
|
+
interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, "title">, VariantProps<typeof alerta> {
|
|
90
|
+
titulo?: ReactNode;
|
|
91
|
+
icone?: ReactNode;
|
|
92
|
+
}
|
|
93
|
+
declare function Alert({ className, nivel, variante, titulo, icone, children, ...props }: AlertProps): react.JSX.Element;
|
|
94
|
+
|
|
95
|
+
interface DialogProps {
|
|
96
|
+
aberto: boolean;
|
|
97
|
+
onFechar: () => void;
|
|
98
|
+
titulo: ReactNode;
|
|
99
|
+
descricao?: ReactNode;
|
|
100
|
+
children?: ReactNode;
|
|
101
|
+
rodape?: ReactNode;
|
|
102
|
+
tamanho?: "sm" | "md" | "lg" | "full";
|
|
103
|
+
/** Impede fechar por Esc ou clique no backdrop. Use só em fluxo destrutivo. */
|
|
104
|
+
persistente?: boolean;
|
|
105
|
+
className?: string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Modal sobre o <dialog> nativo — sem dependência de biblioteca.
|
|
109
|
+
*
|
|
110
|
+
* O elemento nativo já entrega, correto e de graça: armadilha de foco, o
|
|
111
|
+
* resto da página inerte, Esc para fechar, camada de topo acima de qualquer
|
|
112
|
+
* z-index, e ::backdrop. Reimplementar isso em React é como a maioria das
|
|
113
|
+
* libs de modal nasce com bug de acessibilidade.
|
|
114
|
+
*
|
|
115
|
+
* O que ele NÃO entrega e está resolvido aqui: travar o scroll do body,
|
|
116
|
+
* fechar por clique no backdrop, e o modo persistente.
|
|
117
|
+
*/
|
|
118
|
+
declare function Dialog({ aberto, onFechar, titulo, descricao, children, rodape, tamanho, persistente, className, }: DialogProps): react.JSX.Element;
|
|
119
|
+
|
|
120
|
+
declare const selo: (props?: ({
|
|
121
|
+
nivel?: "brand" | "success" | "info" | "warning" | "danger" | "neutral" | null | undefined;
|
|
122
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
123
|
+
interface BadgeProps extends HTMLAttributes<HTMLSpanElement>, VariantProps<typeof selo> {
|
|
124
|
+
}
|
|
125
|
+
declare function Badge({ className, nivel, ...props }: BadgeProps): react.JSX.Element;
|
|
126
|
+
|
|
127
|
+
interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
|
|
128
|
+
label: ReactNode;
|
|
129
|
+
ajuda?: ReactNode;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Checkbox nativo com aparência própria via `appearance-none`, não um `<div>`
|
|
133
|
+
* fingindo ser checkbox. Mantém de graça: teclado, formulário, leitores de
|
|
134
|
+
* tela, autofill e estado indeterminado.
|
|
135
|
+
*/
|
|
136
|
+
declare function Checkbox({ label, ajuda, className, id: idExterno, ...props }: CheckboxProps): react.JSX.Element;
|
|
137
|
+
|
|
138
|
+
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
139
|
+
invalido?: boolean;
|
|
140
|
+
}
|
|
141
|
+
declare function Input({ className, invalido, ...props }: InputProps): react.JSX.Element;
|
|
142
|
+
|
|
143
|
+
interface FieldProps {
|
|
144
|
+
label: ReactNode;
|
|
145
|
+
/** Texto de apoio. Some quando há erro — dois textos competindo confunde. */
|
|
146
|
+
ajuda?: ReactNode;
|
|
147
|
+
erro?: ReactNode;
|
|
148
|
+
obrigatorio?: boolean;
|
|
149
|
+
className?: string;
|
|
150
|
+
/** Recebe id, aria-describedby e aria-invalid já ligados. */
|
|
151
|
+
children: (props: {
|
|
152
|
+
id: string;
|
|
153
|
+
"aria-describedby": string | undefined;
|
|
154
|
+
"aria-invalid": boolean | undefined;
|
|
155
|
+
}) => ReactElement;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Amarra label, ajuda e erro ao controle com os ids corretos. Existe porque
|
|
159
|
+
* `aria-describedby` escrito à mão em cada formulário erra — e erra silencioso.
|
|
160
|
+
*/
|
|
161
|
+
declare function Field({ label, ajuda, erro, obrigatorio, className, children }: FieldProps): react.JSX.Element;
|
|
162
|
+
|
|
163
|
+
interface PasswordInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
|
|
164
|
+
invalido?: boolean;
|
|
165
|
+
/** "new-password" em cadastro e redefinição; "current-password" no login. */
|
|
166
|
+
autoComplete?: "current-password" | "new-password";
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Campo de senha com alternância de visibilidade.
|
|
170
|
+
*
|
|
171
|
+
* O `autoComplete` é obrigatório e tipado: gerenciador de senha que não sabe
|
|
172
|
+
* distinguir login de cadastro salva a senha errada, e isso aparece como
|
|
173
|
+
* "o site quebrou meu gerenciador", nunca como bug de UI.
|
|
174
|
+
*/
|
|
175
|
+
declare function PasswordInput({ className, invalido, autoComplete, ...props }: PasswordInputProps): react.JSX.Element;
|
|
176
|
+
|
|
177
|
+
declare function Table({ className, ...props }: HTMLAttributes<HTMLTableElement>): react.JSX.Element;
|
|
178
|
+
declare function THead({ className, ...props }: HTMLAttributes<HTMLTableSectionElement>): react.JSX.Element;
|
|
179
|
+
declare function TBody({ className, ...props }: HTMLAttributes<HTMLTableSectionElement>): react.JSX.Element;
|
|
180
|
+
declare function TR({ className, selecionada, ...props }: HTMLAttributes<HTMLTableRowElement> & {
|
|
181
|
+
selecionada?: boolean;
|
|
182
|
+
}): react.JSX.Element;
|
|
183
|
+
declare function TH({ className, numerica, ...props }: ThHTMLAttributes<HTMLTableCellElement> & {
|
|
184
|
+
numerica?: boolean;
|
|
185
|
+
}): react.JSX.Element;
|
|
186
|
+
declare function TD({ className, numerica, ...props }: TdHTMLAttributes<HTMLTableCellElement> & {
|
|
187
|
+
numerica?: boolean;
|
|
188
|
+
}): react.JSX.Element;
|
|
189
|
+
|
|
190
|
+
interface EmptyStateProps {
|
|
191
|
+
titulo: ReactNode;
|
|
192
|
+
descricao?: ReactNode;
|
|
193
|
+
acao?: ReactNode;
|
|
194
|
+
icone?: ReactNode;
|
|
195
|
+
className?: string;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Estado vazio pede três coisas: dizer o que falta, por que está vazio, e o
|
|
199
|
+
* próximo passo. "Nenhum resultado" sozinho é um beco sem saída.
|
|
200
|
+
*/
|
|
201
|
+
declare function EmptyState({ titulo, descricao, acao, icone, className }: EmptyStateProps): react.JSX.Element;
|
|
202
|
+
|
|
203
|
+
interface PaginationProps {
|
|
204
|
+
pagina: number;
|
|
205
|
+
totalPaginas: number;
|
|
206
|
+
totalLinhas: number;
|
|
207
|
+
podeVoltar: boolean;
|
|
208
|
+
podeAvancar: boolean;
|
|
209
|
+
onVoltar: () => void;
|
|
210
|
+
onAvancar: () => void;
|
|
211
|
+
className?: string;
|
|
212
|
+
}
|
|
213
|
+
declare function Pagination({ pagina, totalPaginas, totalLinhas, podeVoltar, podeAvancar, onVoltar, onAvancar, className, }: PaginationProps): react.JSX.Element;
|
|
214
|
+
|
|
215
|
+
interface SparklineProps {
|
|
216
|
+
valores: number[];
|
|
217
|
+
/** Token de cor da série. Padrão: a cor primária da marca. */
|
|
218
|
+
cor?: string;
|
|
219
|
+
largura?: number;
|
|
220
|
+
altura?: number;
|
|
221
|
+
className?: string;
|
|
222
|
+
rotulo: string;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Linha de 2px com marcador de fim. Sem eixo, sem grade — sparkline é forma,
|
|
226
|
+
* não gráfico: mostra direção, não valor. O valor fica na métrica ao lado.
|
|
227
|
+
*/
|
|
228
|
+
declare function Sparkline({ valores, cor, largura, altura, className, rotulo, }: SparklineProps): react.JSX.Element | null;
|
|
229
|
+
|
|
230
|
+
declare function Spinner({ className }: {
|
|
231
|
+
className?: string;
|
|
232
|
+
}): react.JSX.Element;
|
|
233
|
+
|
|
234
|
+
type DirecaoDelta = "subiu" | "caiu" | "estavel";
|
|
235
|
+
type SentidoDelta = "bom" | "ruim" | "neutro";
|
|
236
|
+
interface StatTileProps {
|
|
237
|
+
rotulo: ReactNode;
|
|
238
|
+
valor: ReactNode;
|
|
239
|
+
unidade?: ReactNode;
|
|
240
|
+
delta?: {
|
|
241
|
+
texto: string;
|
|
242
|
+
direcao: DirecaoDelta;
|
|
243
|
+
sentido: SentidoDelta;
|
|
244
|
+
};
|
|
245
|
+
apoio?: ReactNode;
|
|
246
|
+
grafico?: ReactNode;
|
|
247
|
+
className?: string;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Métrica sem gráfico. Quando a história é UM número, gráfico é ruído — esta
|
|
251
|
+
* é a forma certa, não um gráfico de uma barra.
|
|
252
|
+
*
|
|
253
|
+
* O número usa algarismos proporcionais de propósito: tabular-nums em número
|
|
254
|
+
* grande e solto deixa "121" frouxo. Tabular só onde há alinhamento vertical.
|
|
255
|
+
*/
|
|
256
|
+
declare function StatTile({ rotulo, valor, unidade, delta, apoio, grafico, className }: StatTileProps): react.JSX.Element;
|
|
257
|
+
|
|
258
|
+
export { Alert, type AlertProps, Badge, type BadgeProps, Button, type ButtonProps, Checkbox, type CheckboxProps, type DataTableFeatures, Dialog, type DialogProps, type DirecaoDelta, EmptyState, type EmptyStateProps, Field, type FieldProps, Input, type InputProps, type Modo, Pagination, type PaginationProps, PasswordInput, type PasswordInputProps, type SentidoDelta, Sparkline, type SparklineProps, Spinner, StatTile, type StatTileProps, TBody, TD, TH, THead, TR, Table, alerta as alertVariants, selo as badgeVariants, botao as buttonVariants, cn, criarColumnHelper, dataTableFeatures, useBrand, useModo };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
import { tableFeatures, rowPaginationFeature, globalFilteringFeature, columnFilteringFeature, rowSortingFeature, createPaginatedRowModel, createFilteredRowModel, createSortedRowModel, createColumnHelper } from '@tanstack/react-table';
|
|
4
|
+
import { useState, useEffect, useCallback, useRef, useId } from 'react';
|
|
5
|
+
import { cva } from 'class-variance-authority';
|
|
6
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
// src/lib/cn.ts
|
|
9
|
+
function cn(...inputs) {
|
|
10
|
+
return twMerge(clsx(inputs));
|
|
11
|
+
}
|
|
12
|
+
var dataTableFeatures = tableFeatures({
|
|
13
|
+
rowSortingFeature,
|
|
14
|
+
sortedRowModel: createSortedRowModel(),
|
|
15
|
+
columnFilteringFeature,
|
|
16
|
+
globalFilteringFeature,
|
|
17
|
+
filteredRowModel: createFilteredRowModel(),
|
|
18
|
+
rowPaginationFeature,
|
|
19
|
+
paginatedRowModel: createPaginatedRowModel()
|
|
20
|
+
});
|
|
21
|
+
function criarColumnHelper() {
|
|
22
|
+
return createColumnHelper();
|
|
23
|
+
}
|
|
24
|
+
function useModo(inicial = "light") {
|
|
25
|
+
const [modo, setModo] = useState(inicial);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
document.documentElement.classList.toggle("dark", modo === "dark");
|
|
28
|
+
}, [modo]);
|
|
29
|
+
const alternarModo = useCallback(
|
|
30
|
+
() => setModo((m) => m === "dark" ? "light" : "dark"),
|
|
31
|
+
[]
|
|
32
|
+
);
|
|
33
|
+
return { modo, setModo, alternarModo };
|
|
34
|
+
}
|
|
35
|
+
function useBrand(marcaInicial, modoInicial = "light") {
|
|
36
|
+
const [marca, setMarca] = useState(marcaInicial);
|
|
37
|
+
const { modo, setModo, alternarModo } = useModo(modoInicial);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
document.documentElement.dataset.brand = marca;
|
|
40
|
+
}, [marca]);
|
|
41
|
+
return { marca, setMarca, modo, setModo, alternarModo };
|
|
42
|
+
}
|
|
43
|
+
function Spinner({ className }) {
|
|
44
|
+
return /* @__PURE__ */ jsx(
|
|
45
|
+
"span",
|
|
46
|
+
{
|
|
47
|
+
role: "status",
|
|
48
|
+
"aria-label": "Carregando",
|
|
49
|
+
className: cn(
|
|
50
|
+
"inline-block size-4 animate-spin rounded-full border-2 border-current border-t-transparent motion-reduce:animate-none",
|
|
51
|
+
className
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
var botao = cva(
|
|
57
|
+
// O anel de foco usa ring-ring/ring-offset-background: foco visível é
|
|
58
|
+
// requisito WCAG, não enfeite — por isso não é removível por variante.
|
|
59
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
60
|
+
{
|
|
61
|
+
variants: {
|
|
62
|
+
variante: {
|
|
63
|
+
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
64
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
65
|
+
outline: "border border-input bg-background text-foreground hover:bg-muted",
|
|
66
|
+
ghost: "text-foreground hover:bg-muted",
|
|
67
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
68
|
+
},
|
|
69
|
+
tamanho: {
|
|
70
|
+
sm: "h-8 rounded-md px-3 text-[0.8125rem]",
|
|
71
|
+
md: "h-9 rounded-md px-4 text-sm",
|
|
72
|
+
lg: "h-10 rounded-lg px-6 text-sm",
|
|
73
|
+
icon: "size-9 rounded-md"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
defaultVariants: { variante: "primary", tamanho: "md" }
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
function Button({
|
|
80
|
+
className,
|
|
81
|
+
variante,
|
|
82
|
+
tamanho,
|
|
83
|
+
carregando = false,
|
|
84
|
+
disabled,
|
|
85
|
+
children,
|
|
86
|
+
...props
|
|
87
|
+
}) {
|
|
88
|
+
return /* @__PURE__ */ jsxs(
|
|
89
|
+
"button",
|
|
90
|
+
{
|
|
91
|
+
className: cn(botao({ variante, tamanho }), className),
|
|
92
|
+
disabled: disabled ?? carregando,
|
|
93
|
+
"aria-busy": carregando || void 0,
|
|
94
|
+
...props,
|
|
95
|
+
children: [
|
|
96
|
+
carregando ? /* @__PURE__ */ jsx(Spinner, {}) : null,
|
|
97
|
+
children
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
var alerta = cva(
|
|
103
|
+
"relative flex w-full gap-3 rounded-lg border px-4 py-3 text-sm [&>svg]:mt-0.5 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
104
|
+
{
|
|
105
|
+
variants: {
|
|
106
|
+
nivel: {
|
|
107
|
+
success: "",
|
|
108
|
+
info: "",
|
|
109
|
+
warning: "",
|
|
110
|
+
danger: ""
|
|
111
|
+
},
|
|
112
|
+
variante: { surface: "", solid: "" }
|
|
113
|
+
},
|
|
114
|
+
compoundVariants: [
|
|
115
|
+
{ nivel: "success", variante: "surface", class: "bg-success border-success-border text-success-foreground" },
|
|
116
|
+
{ nivel: "info", variante: "surface", class: "bg-info border-info-border text-info-foreground" },
|
|
117
|
+
{ nivel: "warning", variante: "surface", class: "bg-warning border-warning-border text-warning-foreground" },
|
|
118
|
+
{ nivel: "danger", variante: "surface", class: "bg-danger border-danger-border text-danger-foreground" },
|
|
119
|
+
{ nivel: "success", variante: "solid", class: "bg-success-solid border-transparent text-success-solid-foreground" },
|
|
120
|
+
{ nivel: "info", variante: "solid", class: "bg-info-solid border-transparent text-info-solid-foreground" },
|
|
121
|
+
{ nivel: "warning", variante: "solid", class: "bg-warning-solid border-transparent text-warning-solid-foreground" },
|
|
122
|
+
{ nivel: "danger", variante: "solid", class: "bg-danger-solid border-transparent text-danger-solid-foreground" }
|
|
123
|
+
],
|
|
124
|
+
defaultVariants: { nivel: "info", variante: "surface" }
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
function Alert({ className, nivel, variante, titulo, icone, children, ...props }) {
|
|
128
|
+
return /* @__PURE__ */ jsxs(
|
|
129
|
+
"div",
|
|
130
|
+
{
|
|
131
|
+
role: nivel === "danger" ? "alert" : "status",
|
|
132
|
+
className: cn(alerta({ nivel, variante }), className),
|
|
133
|
+
...props,
|
|
134
|
+
children: [
|
|
135
|
+
icone,
|
|
136
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
137
|
+
titulo ? /* @__PURE__ */ jsx("p", { className: "font-semibold", children: titulo }) : null,
|
|
138
|
+
children ? /* @__PURE__ */ jsx("div", { className: cn(titulo && "mt-1 opacity-90"), children }) : null
|
|
139
|
+
] })
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
var TAMANHO = {
|
|
145
|
+
sm: "max-w-sm",
|
|
146
|
+
md: "max-w-lg",
|
|
147
|
+
lg: "max-w-3xl",
|
|
148
|
+
full: "max-w-[min(96vw,80rem)] h-[min(92vh,60rem)]"
|
|
149
|
+
};
|
|
150
|
+
function Dialog({
|
|
151
|
+
aberto,
|
|
152
|
+
onFechar,
|
|
153
|
+
titulo,
|
|
154
|
+
descricao,
|
|
155
|
+
children,
|
|
156
|
+
rodape,
|
|
157
|
+
tamanho = "md",
|
|
158
|
+
persistente = false,
|
|
159
|
+
className
|
|
160
|
+
}) {
|
|
161
|
+
const ref = useRef(null);
|
|
162
|
+
const id = useId();
|
|
163
|
+
const idTitulo = `${id}-titulo`;
|
|
164
|
+
const idDescricao = `${id}-descricao`;
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
const el = ref.current;
|
|
167
|
+
if (!el) return;
|
|
168
|
+
if (aberto && !el.open) el.showModal();
|
|
169
|
+
if (!aberto && el.open) el.close();
|
|
170
|
+
}, [aberto]);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!aberto) return;
|
|
173
|
+
const anterior = document.body.style.overflow;
|
|
174
|
+
document.body.style.overflow = "hidden";
|
|
175
|
+
return () => {
|
|
176
|
+
document.body.style.overflow = anterior;
|
|
177
|
+
};
|
|
178
|
+
}, [aberto]);
|
|
179
|
+
return /* @__PURE__ */ jsxs(
|
|
180
|
+
"dialog",
|
|
181
|
+
{
|
|
182
|
+
ref,
|
|
183
|
+
"aria-labelledby": idTitulo,
|
|
184
|
+
"aria-describedby": descricao ? idDescricao : void 0,
|
|
185
|
+
onCancel: (e) => {
|
|
186
|
+
if (persistente) e.preventDefault();
|
|
187
|
+
else onFechar();
|
|
188
|
+
},
|
|
189
|
+
onClose: () => {
|
|
190
|
+
if (aberto) onFechar();
|
|
191
|
+
},
|
|
192
|
+
onClick: (e) => {
|
|
193
|
+
if (!persistente && e.target === ref.current) onFechar();
|
|
194
|
+
},
|
|
195
|
+
className: cn(
|
|
196
|
+
"m-auto w-[calc(100vw-2rem)] rounded-lg border border-border bg-card p-0 text-card-foreground shadow-lg",
|
|
197
|
+
"backdrop:bg-overlay backdrop:backdrop-blur-[2px]",
|
|
198
|
+
TAMANHO[tamanho],
|
|
199
|
+
// open:flex, não flex. Um <dialog> fechado é display:none pela folha
|
|
200
|
+
// do navegador; uma classe com display:flex sobrescreve isso e o modal
|
|
201
|
+
// passa a renderizar no fluxo da página o tempo todo.
|
|
202
|
+
tamanho === "full" && "open:flex open:flex-col",
|
|
203
|
+
className
|
|
204
|
+
),
|
|
205
|
+
children: [
|
|
206
|
+
/* @__PURE__ */ jsxs("header", { className: "flex items-start justify-between gap-4 border-b border-border px-5 py-3", children: [
|
|
207
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
208
|
+
/* @__PURE__ */ jsx("h2", { id: idTitulo, className: "text-base font-semibold", children: titulo }),
|
|
209
|
+
descricao ? /* @__PURE__ */ jsx("p", { id: idDescricao, className: "mt-0.5 text-sm text-muted-foreground", children: descricao }) : null
|
|
210
|
+
] }),
|
|
211
|
+
persistente ? null : /* @__PURE__ */ jsx(Button, { variante: "ghost", tamanho: "icon", onClick: onFechar, "aria-label": "Fechar", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 20 20", className: "size-4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", children: /* @__PURE__ */ jsx("path", { d: "M5 5l10 10M15 5L5 15", strokeLinecap: "round" }) }) })
|
|
212
|
+
] }),
|
|
213
|
+
/* @__PURE__ */ jsx("div", { className: cn("px-5 py-4", tamanho === "full" && "min-h-0 flex-1 overflow-auto"), children }),
|
|
214
|
+
rodape ? /* @__PURE__ */ jsx("footer", { className: "flex justify-end gap-2 border-t border-border px-5 py-3", children: rodape }) : null
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
var selo = cva(
|
|
220
|
+
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap",
|
|
221
|
+
{
|
|
222
|
+
variants: {
|
|
223
|
+
nivel: {
|
|
224
|
+
neutral: "bg-muted border-border text-muted-foreground",
|
|
225
|
+
brand: "bg-accent border-transparent text-accent-foreground",
|
|
226
|
+
success: "bg-success border-success-border text-success-foreground",
|
|
227
|
+
info: "bg-info border-info-border text-info-foreground",
|
|
228
|
+
warning: "bg-warning border-warning-border text-warning-foreground",
|
|
229
|
+
danger: "bg-danger border-danger-border text-danger-foreground"
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
defaultVariants: { nivel: "neutral" }
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
function Badge({ className, nivel, ...props }) {
|
|
236
|
+
return /* @__PURE__ */ jsx("span", { className: cn(selo({ nivel }), className), ...props });
|
|
237
|
+
}
|
|
238
|
+
function Checkbox({ label, ajuda, className, id: idExterno, ...props }) {
|
|
239
|
+
const gerado = useId();
|
|
240
|
+
const id = idExterno ?? gerado;
|
|
241
|
+
const idAjuda = `${id}-ajuda`;
|
|
242
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex gap-2", className), children: [
|
|
243
|
+
/* @__PURE__ */ jsxs("span", { className: "relative mt-0.5 inline-grid shrink-0 place-items-center", children: [
|
|
244
|
+
/* @__PURE__ */ jsx(
|
|
245
|
+
"input",
|
|
246
|
+
{
|
|
247
|
+
type: "checkbox",
|
|
248
|
+
id,
|
|
249
|
+
"aria-describedby": ajuda ? idAjuda : void 0,
|
|
250
|
+
className: cn(
|
|
251
|
+
"peer size-4 appearance-none rounded border border-input bg-background",
|
|
252
|
+
"checked:border-primary checked:bg-primary",
|
|
253
|
+
"indeterminate:border-primary indeterminate:bg-primary",
|
|
254
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
255
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
256
|
+
),
|
|
257
|
+
...props
|
|
258
|
+
}
|
|
259
|
+
),
|
|
260
|
+
/* @__PURE__ */ jsx(
|
|
261
|
+
"svg",
|
|
262
|
+
{
|
|
263
|
+
viewBox: "0 0 16 16",
|
|
264
|
+
"aria-hidden": "true",
|
|
265
|
+
className: "pointer-events-none absolute size-3 text-primary-foreground opacity-0 peer-checked:opacity-100",
|
|
266
|
+
fill: "none",
|
|
267
|
+
stroke: "currentColor",
|
|
268
|
+
strokeWidth: "2.5",
|
|
269
|
+
strokeLinecap: "round",
|
|
270
|
+
strokeLinejoin: "round",
|
|
271
|
+
children: /* @__PURE__ */ jsx("path", { d: "M3 8.5l3.5 3.5L13 5" })
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
] }),
|
|
275
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
276
|
+
/* @__PURE__ */ jsx("label", { htmlFor: id, className: "text-sm text-foreground", children: label }),
|
|
277
|
+
ajuda ? /* @__PURE__ */ jsx("p", { id: idAjuda, className: "text-xs text-muted-foreground", children: ajuda }) : null
|
|
278
|
+
] })
|
|
279
|
+
] });
|
|
280
|
+
}
|
|
281
|
+
function Input({ className, invalido, ...props }) {
|
|
282
|
+
return /* @__PURE__ */ jsx(
|
|
283
|
+
"input",
|
|
284
|
+
{
|
|
285
|
+
className: cn(
|
|
286
|
+
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm text-foreground transition-colors",
|
|
287
|
+
"placeholder:text-muted-foreground",
|
|
288
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
289
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
290
|
+
invalido && "border-danger-solid ring-danger-solid/20",
|
|
291
|
+
className
|
|
292
|
+
),
|
|
293
|
+
"aria-invalid": invalido || void 0,
|
|
294
|
+
...props
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
function Field({ label, ajuda, erro, obrigatorio, className, children }) {
|
|
299
|
+
const id = useId();
|
|
300
|
+
const idAjuda = `${id}-ajuda`;
|
|
301
|
+
const idErro = `${id}-erro`;
|
|
302
|
+
const descritores = [erro ? idErro : null, ajuda && !erro ? idAjuda : null].filter(Boolean).join(" ");
|
|
303
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-1.5", className), children: [
|
|
304
|
+
/* @__PURE__ */ jsxs("label", { htmlFor: id, className: "block text-sm font-medium text-foreground", children: [
|
|
305
|
+
label,
|
|
306
|
+
obrigatorio ? /* @__PURE__ */ jsx("span", { className: "ml-0.5 text-danger-solid", "aria-hidden": "true", children: "*" }) : null
|
|
307
|
+
] }),
|
|
308
|
+
children({
|
|
309
|
+
id,
|
|
310
|
+
"aria-describedby": descritores || void 0,
|
|
311
|
+
"aria-invalid": erro ? true : void 0
|
|
312
|
+
}),
|
|
313
|
+
erro ? /* @__PURE__ */ jsx("p", { id: idErro, className: "text-xs text-danger-foreground", children: erro }) : ajuda ? /* @__PURE__ */ jsx("p", { id: idAjuda, className: "text-xs text-muted-foreground", children: ajuda }) : null
|
|
314
|
+
] });
|
|
315
|
+
}
|
|
316
|
+
var OLHO = "M2 10s3-5.5 8-5.5S18 10 18 10s-3 5.5-8 5.5S2 10 2 10Z";
|
|
317
|
+
var RISCO = "M4 16 16 4";
|
|
318
|
+
function PasswordInput({ className, invalido, autoComplete, ...props }) {
|
|
319
|
+
const [visivel, setVisivel] = useState(false);
|
|
320
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
321
|
+
/* @__PURE__ */ jsx(
|
|
322
|
+
"input",
|
|
323
|
+
{
|
|
324
|
+
type: visivel ? "text" : "password",
|
|
325
|
+
autoComplete,
|
|
326
|
+
"aria-invalid": invalido || void 0,
|
|
327
|
+
className: cn(
|
|
328
|
+
"flex h-9 w-full rounded-md border border-input bg-background py-1 pr-10 pl-3 text-sm text-foreground transition-colors",
|
|
329
|
+
"placeholder:text-muted-foreground",
|
|
330
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
331
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
332
|
+
invalido && "border-danger-solid",
|
|
333
|
+
className
|
|
334
|
+
),
|
|
335
|
+
...props
|
|
336
|
+
}
|
|
337
|
+
),
|
|
338
|
+
/* @__PURE__ */ jsx(
|
|
339
|
+
"button",
|
|
340
|
+
{
|
|
341
|
+
type: "button",
|
|
342
|
+
onClick: () => setVisivel((v) => !v),
|
|
343
|
+
"aria-pressed": visivel,
|
|
344
|
+
"aria-label": visivel ? "Ocultar senha" : "Mostrar senha",
|
|
345
|
+
className: cn(
|
|
346
|
+
"absolute top-1/2 right-1 -translate-y-1/2 rounded p-1.5 text-muted-foreground",
|
|
347
|
+
"hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
348
|
+
),
|
|
349
|
+
children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 20 20", className: "size-4", fill: "none", stroke: "currentColor", strokeWidth: "1.6", "aria-hidden": "true", children: [
|
|
350
|
+
/* @__PURE__ */ jsx("path", { d: OLHO }),
|
|
351
|
+
/* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "2.2" }),
|
|
352
|
+
visivel ? /* @__PURE__ */ jsx("path", { d: RISCO, strokeLinecap: "round" }) : null
|
|
353
|
+
] })
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
] });
|
|
357
|
+
}
|
|
358
|
+
function Table({ className, ...props }) {
|
|
359
|
+
return /* @__PURE__ */ jsx("div", { className: "relative w-full overflow-x-auto", children: /* @__PURE__ */ jsx("table", { className: cn("w-full caption-bottom border-collapse text-sm", className), ...props }) });
|
|
360
|
+
}
|
|
361
|
+
function THead({ className, ...props }) {
|
|
362
|
+
return /* @__PURE__ */ jsx("thead", { className: cn("[&_tr]:border-b [&_tr]:border-border", className), ...props });
|
|
363
|
+
}
|
|
364
|
+
function TBody({ className, ...props }) {
|
|
365
|
+
return /* @__PURE__ */ jsx("tbody", { className: cn("[&_tr:last-child]:border-0", className), ...props });
|
|
366
|
+
}
|
|
367
|
+
function TR({
|
|
368
|
+
className,
|
|
369
|
+
selecionada,
|
|
370
|
+
...props
|
|
371
|
+
}) {
|
|
372
|
+
return /* @__PURE__ */ jsx(
|
|
373
|
+
"tr",
|
|
374
|
+
{
|
|
375
|
+
"data-selecionada": selecionada || void 0,
|
|
376
|
+
"aria-selected": selecionada,
|
|
377
|
+
className: cn(
|
|
378
|
+
"border-b border-border transition-colors hover:bg-muted/60",
|
|
379
|
+
selecionada && "bg-accent/60",
|
|
380
|
+
className
|
|
381
|
+
),
|
|
382
|
+
...props
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
function TH({
|
|
387
|
+
className,
|
|
388
|
+
numerica,
|
|
389
|
+
...props
|
|
390
|
+
}) {
|
|
391
|
+
return /* @__PURE__ */ jsx(
|
|
392
|
+
"th",
|
|
393
|
+
{
|
|
394
|
+
className: cn(
|
|
395
|
+
"h-9 px-3 text-left align-middle text-xs font-semibold text-muted-foreground",
|
|
396
|
+
numerica && "text-right",
|
|
397
|
+
className
|
|
398
|
+
),
|
|
399
|
+
...props
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
function TD({
|
|
404
|
+
className,
|
|
405
|
+
numerica,
|
|
406
|
+
...props
|
|
407
|
+
}) {
|
|
408
|
+
return /* @__PURE__ */ jsx(
|
|
409
|
+
"td",
|
|
410
|
+
{
|
|
411
|
+
className: cn(
|
|
412
|
+
"px-3 py-2 align-middle leading-[1.4] text-foreground",
|
|
413
|
+
numerica && "text-right tabular-nums",
|
|
414
|
+
className
|
|
415
|
+
),
|
|
416
|
+
...props
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
function EmptyState({ titulo, descricao, acao, icone, className }) {
|
|
421
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col items-center justify-center gap-2 px-6 py-12 text-center", className), children: [
|
|
422
|
+
icone ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground", children: icone }) : null,
|
|
423
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: titulo }),
|
|
424
|
+
descricao ? /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: descricao }) : null,
|
|
425
|
+
acao ? /* @__PURE__ */ jsx("div", { className: "mt-2", children: acao }) : null
|
|
426
|
+
] });
|
|
427
|
+
}
|
|
428
|
+
function Pagination({
|
|
429
|
+
pagina,
|
|
430
|
+
totalPaginas,
|
|
431
|
+
totalLinhas,
|
|
432
|
+
podeVoltar,
|
|
433
|
+
podeAvancar,
|
|
434
|
+
onVoltar,
|
|
435
|
+
onAvancar,
|
|
436
|
+
className
|
|
437
|
+
}) {
|
|
438
|
+
return /* @__PURE__ */ jsxs(
|
|
439
|
+
"nav",
|
|
440
|
+
{
|
|
441
|
+
"aria-label": "Pagina\xE7\xE3o",
|
|
442
|
+
className: cn("flex items-center justify-between gap-4 border-t border-border px-3 py-2", className),
|
|
443
|
+
children: [
|
|
444
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground tabular-nums", "aria-live": "polite", children: [
|
|
445
|
+
"P\xE1gina ",
|
|
446
|
+
pagina,
|
|
447
|
+
" de ",
|
|
448
|
+
totalPaginas,
|
|
449
|
+
" \xB7 ",
|
|
450
|
+
totalLinhas,
|
|
451
|
+
" registros"
|
|
452
|
+
] }),
|
|
453
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
454
|
+
/* @__PURE__ */ jsx(Button, { variante: "outline", tamanho: "sm", onClick: onVoltar, disabled: !podeVoltar, children: "Anterior" }),
|
|
455
|
+
/* @__PURE__ */ jsx(Button, { variante: "outline", tamanho: "sm", onClick: onAvancar, disabled: !podeAvancar, children: "Pr\xF3xima" })
|
|
456
|
+
] })
|
|
457
|
+
]
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
function Sparkline({
|
|
462
|
+
valores,
|
|
463
|
+
cor = "var(--primary)",
|
|
464
|
+
largura = 120,
|
|
465
|
+
altura = 32,
|
|
466
|
+
className,
|
|
467
|
+
rotulo
|
|
468
|
+
}) {
|
|
469
|
+
const id = useId();
|
|
470
|
+
if (valores.length < 2) return null;
|
|
471
|
+
const min = Math.min(...valores);
|
|
472
|
+
const max = Math.max(...valores);
|
|
473
|
+
const span = max - min || 1;
|
|
474
|
+
const pad = 4;
|
|
475
|
+
const x = (i) => pad + i / (valores.length - 1) * (largura - pad * 2);
|
|
476
|
+
const y = (v) => altura - pad - (v - min) / span * (altura - pad * 2);
|
|
477
|
+
const d = valores.map((v, i) => `${i ? "L" : "M"}${x(i).toFixed(2)},${y(v).toFixed(2)}`).join(" ");
|
|
478
|
+
const ultimoX = x(valores.length - 1);
|
|
479
|
+
const ultimoY = y(valores[valores.length - 1]);
|
|
480
|
+
return /* @__PURE__ */ jsxs(
|
|
481
|
+
"svg",
|
|
482
|
+
{
|
|
483
|
+
viewBox: `0 0 ${largura} ${altura}`,
|
|
484
|
+
width: largura,
|
|
485
|
+
height: altura,
|
|
486
|
+
className: cn("overflow-visible", className),
|
|
487
|
+
role: "img",
|
|
488
|
+
"aria-labelledby": id,
|
|
489
|
+
children: [
|
|
490
|
+
/* @__PURE__ */ jsx("title", { id, children: rotulo }),
|
|
491
|
+
/* @__PURE__ */ jsx("path", { d, fill: "none", stroke: cor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }),
|
|
492
|
+
/* @__PURE__ */ jsx("circle", { cx: ultimoX, cy: ultimoY, r: "4", fill: cor, stroke: "var(--card)", strokeWidth: "2" })
|
|
493
|
+
]
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
var SETA = { subiu: "M10 4v12M10 4l-5 5M10 4l5 5", caiu: "M10 16V4M10 16l-5-5M10 16l5-5", estavel: "M4 10h12" };
|
|
498
|
+
function StatTile({ rotulo, valor, unidade, delta, apoio, grafico, className }) {
|
|
499
|
+
const corDelta = delta?.sentido === "bom" ? "text-success-foreground" : delta?.sentido === "ruim" ? "text-danger-foreground" : "text-muted-foreground";
|
|
500
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("rounded-lg border border-border bg-card p-4", className), children: [
|
|
501
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: rotulo }),
|
|
502
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-end justify-between gap-3", children: [
|
|
503
|
+
/* @__PURE__ */ jsxs("p", { className: "text-3xl leading-none font-semibold text-foreground", children: [
|
|
504
|
+
valor,
|
|
505
|
+
unidade ? /* @__PURE__ */ jsx("span", { className: "ml-1 text-base font-normal text-muted-foreground", children: unidade }) : null
|
|
506
|
+
] }),
|
|
507
|
+
grafico
|
|
508
|
+
] }),
|
|
509
|
+
delta || apoio ? /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center gap-2 text-xs", children: [
|
|
510
|
+
delta ? (
|
|
511
|
+
/* Ícone + texto, nunca cor sozinha: quem não distingue verde de
|
|
512
|
+
vermelho precisa da seta e da palavra. */
|
|
513
|
+
/* @__PURE__ */ jsxs("span", { className: cn("inline-flex items-center gap-1 font-medium", corDelta), children: [
|
|
514
|
+
/* @__PURE__ */ jsx("svg", { viewBox: "0 0 20 20", className: "size-3.5", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: SETA[delta.direcao] }) }),
|
|
515
|
+
delta.texto
|
|
516
|
+
] })
|
|
517
|
+
) : null,
|
|
518
|
+
apoio ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: apoio }) : null
|
|
519
|
+
] }) : null
|
|
520
|
+
] });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export { Alert, Badge, Button, Checkbox, Dialog, EmptyState, Field, Input, Pagination, PasswordInput, Sparkline, Spinner, StatTile, TBody, TD, TH, THead, TR, Table, alerta as alertVariants, selo as badgeVariants, botao as buttonVariants, cn, criarColumnHelper, dataTableFeatures, useBrand, useModo };
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* Ponte entre os tokens semânticos (que o brand-forge escreve em :root) e as
|
|
2
|
+
utilities do Tailwind v4. Divisão de responsabilidade: o brand-forge é dono
|
|
3
|
+
dos VALORES, o kernel é dono dos NOMES de utility que seus componentes usam.
|
|
4
|
+
|
|
5
|
+
Importe DEPOIS do tokens.css da marca. */
|
|
6
|
+
|
|
7
|
+
@theme inline {
|
|
8
|
+
--color-background: var(--background);
|
|
9
|
+
--color-foreground: var(--foreground);
|
|
10
|
+
--color-card: var(--card);
|
|
11
|
+
--color-card-foreground: var(--card-foreground);
|
|
12
|
+
--color-popover: var(--popover);
|
|
13
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
14
|
+
--color-primary: var(--primary);
|
|
15
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
16
|
+
--color-secondary: var(--secondary);
|
|
17
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
18
|
+
--color-muted: var(--muted);
|
|
19
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
20
|
+
--color-accent: var(--accent);
|
|
21
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
22
|
+
--color-destructive: var(--destructive);
|
|
23
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
24
|
+
--color-border: var(--border);
|
|
25
|
+
--color-input: var(--input);
|
|
26
|
+
--color-ring: var(--ring);
|
|
27
|
+
|
|
28
|
+
--color-success: var(--success);
|
|
29
|
+
--color-success-border: var(--success-border);
|
|
30
|
+
--color-success-foreground: var(--success-foreground);
|
|
31
|
+
--color-success-solid: var(--success-solid);
|
|
32
|
+
--color-success-solid-foreground: var(--success-solid-foreground);
|
|
33
|
+
--color-info: var(--info);
|
|
34
|
+
--color-info-border: var(--info-border);
|
|
35
|
+
--color-info-foreground: var(--info-foreground);
|
|
36
|
+
--color-info-solid: var(--info-solid);
|
|
37
|
+
--color-info-solid-foreground: var(--info-solid-foreground);
|
|
38
|
+
--color-warning: var(--warning);
|
|
39
|
+
--color-warning-border: var(--warning-border);
|
|
40
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
41
|
+
--color-warning-solid: var(--warning-solid);
|
|
42
|
+
--color-warning-solid-foreground: var(--warning-solid-foreground);
|
|
43
|
+
--color-danger: var(--danger);
|
|
44
|
+
--color-danger-border: var(--danger-border);
|
|
45
|
+
--color-danger-foreground: var(--danger-foreground);
|
|
46
|
+
--color-danger-solid: var(--danger-solid);
|
|
47
|
+
--color-danger-solid-foreground: var(--danger-solid-foreground);
|
|
48
|
+
|
|
49
|
+
/* Véu atrás de modal. A marca pode emitir --overlay; enquanto não emite, o
|
|
50
|
+
fallback é preto a 50% — véu escurece nos dois modos, então não segue
|
|
51
|
+
--background nem --foreground. Fica aqui, na ponte, e não no componente. */
|
|
52
|
+
--color-overlay: var(--overlay, oklch(0% 0 0 / 0.5));
|
|
53
|
+
|
|
54
|
+
--color-chart-1: var(--chart-1);
|
|
55
|
+
--color-chart-2: var(--chart-2);
|
|
56
|
+
--color-chart-3: var(--chart-3);
|
|
57
|
+
--color-chart-4: var(--chart-4);
|
|
58
|
+
--color-chart-5: var(--chart-5);
|
|
59
|
+
|
|
60
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
61
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
62
|
+
--radius-lg: var(--radius);
|
|
63
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@layer base {
|
|
67
|
+
* {
|
|
68
|
+
border-color: var(--border);
|
|
69
|
+
}
|
|
70
|
+
body {
|
|
71
|
+
background-color: var(--background);
|
|
72
|
+
color: var(--foreground);
|
|
73
|
+
-webkit-font-smoothing: antialiased;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* A família precisa ser reaplicada em cada contêiner de marca, não só no
|
|
77
|
+
body: em página multi-marca a marca vive abaixo do body, e font-family
|
|
78
|
+
herdada já vem resolvida — o token da marca nunca chegaria. */
|
|
79
|
+
/* Guarda contra o mesmo erro em qualquer dialog do produto: nenhuma
|
|
80
|
+
utility de display deve conseguir revelar um dialog fechado. */
|
|
81
|
+
dialog:not([open]) {
|
|
82
|
+
display: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
body,
|
|
86
|
+
[data-brand] {
|
|
87
|
+
font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif);
|
|
88
|
+
}
|
|
89
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fldev-solution/ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kernel de UI compartilhado entre os SaaS da fldev. Primitivos, hooks e utilitários; blocos compostos vão pelo registry.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://ui.fldev.com.br",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/FLDEV-Solution/fldev-ui.git",
|
|
10
|
+
"directory": "packages/ui"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"react",
|
|
14
|
+
"tailwindcss",
|
|
15
|
+
"design-system",
|
|
16
|
+
"shadcn",
|
|
17
|
+
"multi-brand"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./styles.css": "./dist/styles.css"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"prepublishOnly": "pnpm build"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": ">=18",
|
|
41
|
+
"react-dom": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@tanstack/react-table": "^9.2.4",
|
|
45
|
+
"class-variance-authority": "^0.7.1",
|
|
46
|
+
"clsx": "^2.1.1",
|
|
47
|
+
"tailwind-merge": "^3.4.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/react": "^19.3.0",
|
|
51
|
+
"@types/react-dom": "^19.3.0",
|
|
52
|
+
"react": "^19.3.0",
|
|
53
|
+
"react-dom": "^19.3.0",
|
|
54
|
+
"tsup": "^8.5.0",
|
|
55
|
+
"typescript": "^5.9.3"
|
|
56
|
+
}
|
|
57
|
+
}
|