ads-nuxt 0.1.0 → 0.1.2
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 -21
- package/README.md +567 -1
- package/app/app.config.ts +25 -25
- package/app/assets/ads/checkbox-check.svg +5 -5
- package/app/assets/ads/checkbox-minus.svg +5 -5
- package/app/assets/brand/farmarcas.svg +6 -6
- package/app/assets/brand/radar.svg +9 -9
- package/app/assets/css/component-tokens.css +403 -403
- package/app/assets/css/main.css +617 -617
- package/app/components/AdsBadge.vue +48 -48
- package/app/components/AdsBroadcast.vue +45 -45
- package/app/components/AdsButton.vue +569 -569
- package/app/components/AdsButtonGroup.vue +39 -39
- package/app/components/AdsButtonMenu.vue +48 -48
- package/app/components/AdsCard.vue +117 -117
- package/app/components/AdsCharttip.vue +78 -78
- package/app/components/AdsCheckbox.vue +77 -77
- package/app/components/AdsDatePicker.vue +473 -473
- package/app/components/AdsDropdownMenu.vue +19 -19
- package/app/components/AdsEmptyState.vue +58 -58
- package/app/components/AdsFarmarcasPill.vue +74 -74
- package/app/components/AdsFeatureCard.vue +217 -217
- package/app/components/AdsFileCard.vue +101 -101
- package/app/components/AdsFilterDropdown.vue +174 -174
- package/app/components/AdsFilterOption.vue +72 -72
- package/app/components/AdsFilterSelect.vue +202 -202
- package/app/components/AdsFooter.vue +25 -25
- package/app/components/AdsInput.vue +143 -143
- package/app/components/AdsListDropdown.vue +44 -44
- package/app/components/AdsLoading.vue +109 -109
- package/app/components/AdsMenuBar.vue +103 -103
- package/app/components/AdsMetricCard.vue +119 -119
- package/app/components/AdsMiniPill.vue +40 -40
- package/app/components/AdsModal.vue +157 -157
- package/app/components/AdsMonthPicker.vue +80 -80
- package/app/components/AdsNavbarApp.vue +67 -67
- package/app/components/AdsNavbarDesk.vue +62 -62
- package/app/components/AdsNotabilityPill.vue +27 -27
- package/app/components/AdsNumberInput.vue +166 -166
- package/app/components/AdsPagination.vue +91 -91
- package/app/components/AdsProductTag.vue +61 -61
- package/app/components/AdsRadio.vue +70 -70
- package/app/components/AdsSearchInput.vue +113 -113
- package/app/components/AdsSegmentedTabs.vue +28 -28
- package/app/components/AdsSideMenu.vue +101 -101
- package/app/components/AdsSmalltip.vue +44 -44
- package/app/components/AdsSteptip.vue +101 -101
- package/app/components/AdsSwitch.vue +59 -59
- package/app/components/AdsTable.vue +100 -100
- package/app/components/AdsTableToolbar.vue +44 -44
- package/app/components/AdsTabs.vue +51 -51
- package/app/components/AdsTextarea.vue +109 -109
- package/app/components/AdsToast.vue +181 -181
- package/app/components/AdsTooltip.vue +66 -66
- package/app/components/InvertedTooltip.vue +40 -40
- package/app/components/OverlayLoading.vue +173 -173
- package/app/components/ProgressBarLoading.vue +256 -256
- package/app/composables/useOverlayLoading.ts +66 -66
- package/app/locale/pt-br.ts +6 -6
- package/app/plugins/ads-toast.client.ts +24 -24
- package/app/theme/ads-theme.ts +370 -370
- package/app/theme/component-tokens.json +402 -402
- package/app/theme/component-tokens.ts +815 -815
- package/app/types/overlay-loading.types.ts +26 -26
- package/app/utils/ads-filter-select.ts +172 -172
- package/app/utils/ads-table.ts +204 -204
- package/app/utils/calendar-date.ts +128 -128
- package/app/utils/category-role.ts +45 -45
- package/app/utils/money.ts +77 -77
- package/app/utils/notability.ts +8 -8
- package/app/utils/parser.ts +134 -134
- package/app/utils/percentage.ts +54 -54
- package/nuxt.config.ts +31 -32
- package/package.json +48 -46
package/app/utils/ads-table.ts
CHANGED
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Padrão visual das tabelas ADS sobre o UTable.
|
|
3
|
-
* Duas variantes (lista e grade) com a mesma casca: cabeçalho neutral-100,
|
|
4
|
-
* célula surface-card, borda subtle, raio md. A grade só acrescenta a zebra.
|
|
5
|
-
* Cabeçalho e célula usam o mesmo tamanho: --ads-font-size-xxs. O cabeçalho é bold.
|
|
6
|
-
* As células têm fundo próprio para a coluna fixa e a zebra não vazarem no scroll.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** 14px, entrelinha de leitura. Igual no cabeçalho e na célula; o negrito não muda o tamanho. */
|
|
10
|
-
export const ADS_TABLE_CELL_TEXT =
|
|
11
|
-
"text-[length:var(--ads-font-size-xxs)] leading-[var(--ads-line-height-md)]";
|
|
12
|
-
|
|
13
|
-
/** O mesmo 14px da célula. O bold separa o cabeçalho. */
|
|
14
|
-
export const ADS_TABLE_HEADER_TEXT = `${ADS_TABLE_CELL_TEXT} font-bold`;
|
|
15
|
-
|
|
16
|
-
export type AdsTableDensity = "md" | "sm";
|
|
17
|
-
|
|
18
|
-
/** lines: só a divisória horizontal. grid: divisória horizontal e vertical. */
|
|
19
|
-
export type AdsTableBorders = "lines" | "grid";
|
|
20
|
-
|
|
21
|
-
export interface AdsTableLook {
|
|
22
|
-
/** Só o respiro da célula. A fonte não muda. */
|
|
23
|
-
density?: AdsTableDensity;
|
|
24
|
-
/** Zebra só em grade numérica larga, para seguir a linha. */
|
|
25
|
-
striped?: boolean;
|
|
26
|
-
/** Borda e raio próprios. Dentro de AdsCard, false. */
|
|
27
|
-
framed?: boolean;
|
|
28
|
-
/** lines é o padrão. grid desenha a borda entre as células. */
|
|
29
|
-
borders?: AdsTableBorders;
|
|
30
|
-
/** Fundo por nível: neutral-200, neutral-100 e surface-main. Desligado, a linha fica surface-card. */
|
|
31
|
-
depth?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type AdsTableUi = {
|
|
35
|
-
root: string;
|
|
36
|
-
base: string;
|
|
37
|
-
thead: string;
|
|
38
|
-
tbody: string;
|
|
39
|
-
tfoot: string;
|
|
40
|
-
tr: string;
|
|
41
|
-
th: string;
|
|
42
|
-
td: string;
|
|
43
|
-
separator: string;
|
|
44
|
-
empty: string;
|
|
45
|
-
loading: string;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const CELL_PADDING: Record<AdsTableDensity, string> = {
|
|
49
|
-
md: "px-4 py-3",
|
|
50
|
-
sm: "px-3 py-2",
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const GRID_BORDER = "border-r border-[var(--ads-border-subtle)] last:border-r-0";
|
|
54
|
-
|
|
55
|
-
export const buildAdsTableUi = ({
|
|
56
|
-
density = "md",
|
|
57
|
-
striped = false,
|
|
58
|
-
framed = true,
|
|
59
|
-
borders = "lines",
|
|
60
|
-
depth = false,
|
|
61
|
-
}: AdsTableLook = {}): AdsTableUi => {
|
|
62
|
-
const padding = CELL_PADDING[density];
|
|
63
|
-
const grid = borders === "grid" ? GRID_BORDER : "";
|
|
64
|
-
// Na grade a borda da direita já fecha a coluna fixa da esquerda; a da direita desenha a sua
|
|
65
|
-
// por fora, em cima da borda da vizinha, para não somar 2px.
|
|
66
|
-
const pinnedEdge =
|
|
67
|
-
borders === "grid"
|
|
68
|
-
? "data-[pinned=right]:shadow-[-1px_0_0_var(--ads-border-subtle)]"
|
|
69
|
-
: "data-[pinned=left]:shadow-[inset_-1px_0_0_var(--ads-border-subtle)] data-[pinned=right]:shadow-[inset_1px_0_0_var(--ads-border-subtle)]";
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
root: [
|
|
73
|
-
"relative overflow-auto bg-[var(--ads-surface-card)]",
|
|
74
|
-
"[scrollbar-color:var(--ads-border-subtle)_transparent] [scrollbar-width:thin]",
|
|
75
|
-
framed ? "rounded-[var(--ads-radius-md)] border border-[var(--ads-border-subtle)]" : "",
|
|
76
|
-
].join(" "),
|
|
77
|
-
base: "min-w-full border-separate border-spacing-0",
|
|
78
|
-
thead: "z-[2] bg-neutral-100 backdrop-blur-none",
|
|
79
|
-
tbody: [
|
|
80
|
-
"divide-y-0 [&>tr:last-child>td]:border-b-0",
|
|
81
|
-
striped ? "[&>tr:nth-child(even)>td:not(:has([data-cell-fill]))]:bg-[var(--ads-surface-main)]" : "",
|
|
82
|
-
"[&>tr[data-selectable=true]]:cursor-pointer [&>tr[data-selectable=true]:hover>td]:bg-neutral-100",
|
|
83
|
-
"[&>tr[data-selected=true]>td]:bg-[var(--ads-button-secondary)]",
|
|
84
|
-
depth
|
|
85
|
-
? "[&>tr:has([data-depth='0'])>td:not(:has([data-cell-fill]))]:!bg-neutral-200 [&>tr:has([data-depth='1'])>td:not(:has([data-cell-fill]))]:!bg-neutral-100 [&>tr:has([data-depth='2'])>td:not(:has([data-cell-fill]))]:!bg-[var(--ads-surface-main)]"
|
|
86
|
-
: "",
|
|
87
|
-
].join(" "),
|
|
88
|
-
tfoot: [
|
|
89
|
-
"z-[2] bg-neutral-100 backdrop-blur-none [&>tr[data-slot=tr]~tr[data-slot=tr]]:hidden",
|
|
90
|
-
"[&_th]:border-t [&_th]:border-b-0 [&_th]:border-[var(--ads-border-subtle)] [&_th]:text-[var(--ads-text-primary)]",
|
|
91
|
-
"[&_td]:border-t [&_td]:border-b-0 [&_td]:border-[var(--ads-border-subtle)]",
|
|
92
|
-
].join(" "),
|
|
93
|
-
tr: "",
|
|
94
|
-
th: [
|
|
95
|
-
padding,
|
|
96
|
-
"border-b border-[var(--ads-border-subtle)] bg-neutral-100 text-left align-middle whitespace-normal text-[var(--ads-text-secondary)] has-data-cell-fill:!h-px has-data-cell-fill:!p-0",
|
|
97
|
-
grid,
|
|
98
|
-
ADS_TABLE_HEADER_TEXT,
|
|
99
|
-
"[&[colspan]]:text-center [&[colspan]]:text-[var(--ads-text-primary)]",
|
|
100
|
-
"data-[pinned=left]:z-[3] data-[pinned=right]:z-[3]",
|
|
101
|
-
pinnedEdge,
|
|
102
|
-
].join(" "),
|
|
103
|
-
td: [
|
|
104
|
-
padding,
|
|
105
|
-
"border-b border-[var(--ads-border-subtle)] bg-[var(--ads-surface-card)] align-middle whitespace-normal text-[var(--ads-text-secondary)] transition-colors duration-150 ease-out has-data-cell-fill:!h-px has-data-cell-fill:!p-0",
|
|
106
|
-
grid,
|
|
107
|
-
ADS_TABLE_CELL_TEXT,
|
|
108
|
-
pinnedEdge,
|
|
109
|
-
].join(" "),
|
|
110
|
-
separator: "hidden",
|
|
111
|
-
empty: `py-10 text-center text-[var(--ads-text-secondary)] ${ADS_TABLE_CELL_TEXT}`,
|
|
112
|
-
loading: "py-10 text-center",
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/** `meta` pronto para cada papel de coluna. */
|
|
117
|
-
export const adsTableColumn = {
|
|
118
|
-
/** Nome da linha: texto primário em negrito. */
|
|
119
|
-
identity: { class: { td: "font-bold text-[var(--ads-text-primary)]" } },
|
|
120
|
-
/** Valor, preço, percentual e índice. */
|
|
121
|
-
numeric: { class: { th: "text-right", td: "text-right tabular-nums" } },
|
|
122
|
-
center: { class: { th: "text-center", td: "text-center" } },
|
|
123
|
-
/** Checkbox de seleção. */
|
|
124
|
-
select: { class: { th: "w-px pe-0", td: "w-px pe-0" } },
|
|
125
|
-
/** Ação da linha, sempre à direita. */
|
|
126
|
-
action: { class: { th: "w-px", td: "w-px text-right" } },
|
|
127
|
-
/** Texto longo que trunca com InvertedTooltip. */
|
|
128
|
-
truncate: { class: { td: "max-w-72 truncate" } },
|
|
129
|
-
} as const;
|
|
130
|
-
|
|
131
|
-
type AdsTableColumnMeta = { class?: { th?: string; td?: string } };
|
|
132
|
-
|
|
133
|
-
const PIN_WIDTH_CLASS = {
|
|
134
|
-
48: "min-w-12",
|
|
135
|
-
96: "min-w-24",
|
|
136
|
-
120: "min-w-30",
|
|
137
|
-
160: "min-w-40",
|
|
138
|
-
256: "min-w-64",
|
|
139
|
-
} as const;
|
|
140
|
-
|
|
141
|
-
export type AdsTablePinWidth = keyof typeof PIN_WIDTH_CLASS;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Coluna fixa (column-pinning). O UTable desloca a coluna pelo `size` do TanStack,
|
|
145
|
-
* então a largura da célula precisa ser a mesma do `size`.
|
|
146
|
-
*/
|
|
147
|
-
export const adsTablePinned = (width: AdsTablePinWidth, meta: AdsTableColumnMeta = {}) => ({
|
|
148
|
-
size: width,
|
|
149
|
-
meta: {
|
|
150
|
-
class: {
|
|
151
|
-
th: `${PIN_WIDTH_CLASS[width]} ${meta.class?.th ?? ""}`.trim(),
|
|
152
|
-
td: `${PIN_WIDTH_CLASS[width]} ${meta.class?.td ?? ""}`.trim(),
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
export type AdsTableGroupTone = "brand" | "success" | "innovation" | "neutral";
|
|
158
|
-
|
|
159
|
-
const GROUP_UNDERLINE: Record<AdsTableGroupTone, string> = {
|
|
160
|
-
brand: "border-b-2 border-b-[var(--ads-button-primary)]",
|
|
161
|
-
success: "border-b-2 border-b-[var(--ads-status-success-500)]",
|
|
162
|
-
innovation: "border-b-2 border-b-[var(--ads-status-innovation-500)]",
|
|
163
|
-
neutral: "",
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
/** Grupo de colunas (Meus dados, Concorrentes, Mercado): risco de 2px no tom da fonte. */
|
|
167
|
-
export const adsTableGroup = (tone: AdsTableGroupTone = "neutral") => ({
|
|
168
|
-
class: { th: GROUP_UNDERLINE[tone] },
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
/** Variação: sinal e cor no tom 500, o mesmo do badge. Zero fica neutro. */
|
|
172
|
-
export const adsTableVariationClass = (value: number | null | undefined) => {
|
|
173
|
-
if (value == null || value === 0) return "text-[var(--ads-text-secondary)]";
|
|
174
|
-
return value > 0
|
|
175
|
-
? "font-bold text-[var(--ads-status-success-500)]"
|
|
176
|
-
: "font-bold text-[var(--ads-status-warning-500)]";
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
/** Marca o nível da linha para o degradê. Acima de 2 fica no tom mais claro. */
|
|
180
|
-
export const adsTableDepthAttr = (depth: number) => Math.min(Math.max(depth, 0), 2);
|
|
181
|
-
|
|
182
|
-
export type AdsTableHeat = "high" | "low" | "muted" | "empty";
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Preenche a célula inteira. O td zera o respiro quando encontra `data-cell-fill` e mantém
|
|
186
|
-
* o fundo opaco, para a coluna fixa não deixar ver o que rola por baixo.
|
|
187
|
-
* Fundo 100 e texto 800 para passar de 4.5:1.
|
|
188
|
-
*/
|
|
189
|
-
export const adsTableCellFill = (tone: AdsTableHeat) =>
|
|
190
|
-
`flex h-full min-h-11 w-full items-center justify-center px-4 py-3 text-center ${ADS_TABLE_HEAT_CLASS[tone]}`;
|
|
191
|
-
|
|
192
|
-
/** Faixa de IC. Fundo 100 e texto 800 para passar de 4.5:1. */
|
|
193
|
-
export const ADS_TABLE_HEAT_CLASS: Record<AdsTableHeat, string> = {
|
|
194
|
-
high: "bg-[var(--ads-button-secondary)] text-[var(--ads-button-primary-pressed)]",
|
|
195
|
-
low: "bg-[var(--ads-status-warning-100)] text-[var(--ads-status-warning-800)]",
|
|
196
|
-
muted: "bg-neutral-100 text-[var(--ads-text-tertiary)]",
|
|
197
|
-
empty: "text-[var(--ads-text-tertiary)]",
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
/** Recuo da árvore, por profundidade. */
|
|
201
|
-
export const ADS_TABLE_TREE_INDENT = ["ps-0", "ps-5", "ps-10", "ps-15", "ps-20"] as const;
|
|
202
|
-
|
|
203
|
-
export const adsTableTreeIndent = (depth: number) =>
|
|
204
|
-
ADS_TABLE_TREE_INDENT[Math.min(depth, ADS_TABLE_TREE_INDENT.length - 1)];
|
|
1
|
+
/**
|
|
2
|
+
* Padrão visual das tabelas ADS sobre o UTable.
|
|
3
|
+
* Duas variantes (lista e grade) com a mesma casca: cabeçalho neutral-100,
|
|
4
|
+
* célula surface-card, borda subtle, raio md. A grade só acrescenta a zebra.
|
|
5
|
+
* Cabeçalho e célula usam o mesmo tamanho: --ads-font-size-xxs. O cabeçalho é bold.
|
|
6
|
+
* As células têm fundo próprio para a coluna fixa e a zebra não vazarem no scroll.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 14px, entrelinha de leitura. Igual no cabeçalho e na célula; o negrito não muda o tamanho. */
|
|
10
|
+
export const ADS_TABLE_CELL_TEXT =
|
|
11
|
+
"text-[length:var(--ads-font-size-xxs)] leading-[var(--ads-line-height-md)]";
|
|
12
|
+
|
|
13
|
+
/** O mesmo 14px da célula. O bold separa o cabeçalho. */
|
|
14
|
+
export const ADS_TABLE_HEADER_TEXT = `${ADS_TABLE_CELL_TEXT} font-bold`;
|
|
15
|
+
|
|
16
|
+
export type AdsTableDensity = "md" | "sm";
|
|
17
|
+
|
|
18
|
+
/** lines: só a divisória horizontal. grid: divisória horizontal e vertical. */
|
|
19
|
+
export type AdsTableBorders = "lines" | "grid";
|
|
20
|
+
|
|
21
|
+
export interface AdsTableLook {
|
|
22
|
+
/** Só o respiro da célula. A fonte não muda. */
|
|
23
|
+
density?: AdsTableDensity;
|
|
24
|
+
/** Zebra só em grade numérica larga, para seguir a linha. */
|
|
25
|
+
striped?: boolean;
|
|
26
|
+
/** Borda e raio próprios. Dentro de AdsCard, false. */
|
|
27
|
+
framed?: boolean;
|
|
28
|
+
/** lines é o padrão. grid desenha a borda entre as células. */
|
|
29
|
+
borders?: AdsTableBorders;
|
|
30
|
+
/** Fundo por nível: neutral-200, neutral-100 e surface-main. Desligado, a linha fica surface-card. */
|
|
31
|
+
depth?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type AdsTableUi = {
|
|
35
|
+
root: string;
|
|
36
|
+
base: string;
|
|
37
|
+
thead: string;
|
|
38
|
+
tbody: string;
|
|
39
|
+
tfoot: string;
|
|
40
|
+
tr: string;
|
|
41
|
+
th: string;
|
|
42
|
+
td: string;
|
|
43
|
+
separator: string;
|
|
44
|
+
empty: string;
|
|
45
|
+
loading: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const CELL_PADDING: Record<AdsTableDensity, string> = {
|
|
49
|
+
md: "px-4 py-3",
|
|
50
|
+
sm: "px-3 py-2",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const GRID_BORDER = "border-r border-[var(--ads-border-subtle)] last:border-r-0";
|
|
54
|
+
|
|
55
|
+
export const buildAdsTableUi = ({
|
|
56
|
+
density = "md",
|
|
57
|
+
striped = false,
|
|
58
|
+
framed = true,
|
|
59
|
+
borders = "lines",
|
|
60
|
+
depth = false,
|
|
61
|
+
}: AdsTableLook = {}): AdsTableUi => {
|
|
62
|
+
const padding = CELL_PADDING[density];
|
|
63
|
+
const grid = borders === "grid" ? GRID_BORDER : "";
|
|
64
|
+
// Na grade a borda da direita já fecha a coluna fixa da esquerda; a da direita desenha a sua
|
|
65
|
+
// por fora, em cima da borda da vizinha, para não somar 2px.
|
|
66
|
+
const pinnedEdge =
|
|
67
|
+
borders === "grid"
|
|
68
|
+
? "data-[pinned=right]:shadow-[-1px_0_0_var(--ads-border-subtle)]"
|
|
69
|
+
: "data-[pinned=left]:shadow-[inset_-1px_0_0_var(--ads-border-subtle)] data-[pinned=right]:shadow-[inset_1px_0_0_var(--ads-border-subtle)]";
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
root: [
|
|
73
|
+
"relative overflow-auto bg-[var(--ads-surface-card)]",
|
|
74
|
+
"[scrollbar-color:var(--ads-border-subtle)_transparent] [scrollbar-width:thin]",
|
|
75
|
+
framed ? "rounded-[var(--ads-radius-md)] border border-[var(--ads-border-subtle)]" : "",
|
|
76
|
+
].join(" "),
|
|
77
|
+
base: "min-w-full border-separate border-spacing-0",
|
|
78
|
+
thead: "z-[2] bg-neutral-100 backdrop-blur-none",
|
|
79
|
+
tbody: [
|
|
80
|
+
"divide-y-0 [&>tr:last-child>td]:border-b-0",
|
|
81
|
+
striped ? "[&>tr:nth-child(even)>td:not(:has([data-cell-fill]))]:bg-[var(--ads-surface-main)]" : "",
|
|
82
|
+
"[&>tr[data-selectable=true]]:cursor-pointer [&>tr[data-selectable=true]:hover>td]:bg-neutral-100",
|
|
83
|
+
"[&>tr[data-selected=true]>td]:bg-[var(--ads-button-secondary)]",
|
|
84
|
+
depth
|
|
85
|
+
? "[&>tr:has([data-depth='0'])>td:not(:has([data-cell-fill]))]:!bg-neutral-200 [&>tr:has([data-depth='1'])>td:not(:has([data-cell-fill]))]:!bg-neutral-100 [&>tr:has([data-depth='2'])>td:not(:has([data-cell-fill]))]:!bg-[var(--ads-surface-main)]"
|
|
86
|
+
: "",
|
|
87
|
+
].join(" "),
|
|
88
|
+
tfoot: [
|
|
89
|
+
"z-[2] bg-neutral-100 backdrop-blur-none [&>tr[data-slot=tr]~tr[data-slot=tr]]:hidden",
|
|
90
|
+
"[&_th]:border-t [&_th]:border-b-0 [&_th]:border-[var(--ads-border-subtle)] [&_th]:text-[var(--ads-text-primary)]",
|
|
91
|
+
"[&_td]:border-t [&_td]:border-b-0 [&_td]:border-[var(--ads-border-subtle)]",
|
|
92
|
+
].join(" "),
|
|
93
|
+
tr: "",
|
|
94
|
+
th: [
|
|
95
|
+
padding,
|
|
96
|
+
"border-b border-[var(--ads-border-subtle)] bg-neutral-100 text-left align-middle whitespace-normal text-[var(--ads-text-secondary)] has-data-cell-fill:!h-px has-data-cell-fill:!p-0",
|
|
97
|
+
grid,
|
|
98
|
+
ADS_TABLE_HEADER_TEXT,
|
|
99
|
+
"[&[colspan]]:text-center [&[colspan]]:text-[var(--ads-text-primary)]",
|
|
100
|
+
"data-[pinned=left]:z-[3] data-[pinned=right]:z-[3]",
|
|
101
|
+
pinnedEdge,
|
|
102
|
+
].join(" "),
|
|
103
|
+
td: [
|
|
104
|
+
padding,
|
|
105
|
+
"border-b border-[var(--ads-border-subtle)] bg-[var(--ads-surface-card)] align-middle whitespace-normal text-[var(--ads-text-secondary)] transition-colors duration-150 ease-out has-data-cell-fill:!h-px has-data-cell-fill:!p-0",
|
|
106
|
+
grid,
|
|
107
|
+
ADS_TABLE_CELL_TEXT,
|
|
108
|
+
pinnedEdge,
|
|
109
|
+
].join(" "),
|
|
110
|
+
separator: "hidden",
|
|
111
|
+
empty: `py-10 text-center text-[var(--ads-text-secondary)] ${ADS_TABLE_CELL_TEXT}`,
|
|
112
|
+
loading: "py-10 text-center",
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** `meta` pronto para cada papel de coluna. */
|
|
117
|
+
export const adsTableColumn = {
|
|
118
|
+
/** Nome da linha: texto primário em negrito. */
|
|
119
|
+
identity: { class: { td: "font-bold text-[var(--ads-text-primary)]" } },
|
|
120
|
+
/** Valor, preço, percentual e índice. */
|
|
121
|
+
numeric: { class: { th: "text-right", td: "text-right tabular-nums" } },
|
|
122
|
+
center: { class: { th: "text-center", td: "text-center" } },
|
|
123
|
+
/** Checkbox de seleção. */
|
|
124
|
+
select: { class: { th: "w-px pe-0", td: "w-px pe-0" } },
|
|
125
|
+
/** Ação da linha, sempre à direita. */
|
|
126
|
+
action: { class: { th: "w-px", td: "w-px text-right" } },
|
|
127
|
+
/** Texto longo que trunca com InvertedTooltip. */
|
|
128
|
+
truncate: { class: { td: "max-w-72 truncate" } },
|
|
129
|
+
} as const;
|
|
130
|
+
|
|
131
|
+
type AdsTableColumnMeta = { class?: { th?: string; td?: string } };
|
|
132
|
+
|
|
133
|
+
const PIN_WIDTH_CLASS = {
|
|
134
|
+
48: "min-w-12",
|
|
135
|
+
96: "min-w-24",
|
|
136
|
+
120: "min-w-30",
|
|
137
|
+
160: "min-w-40",
|
|
138
|
+
256: "min-w-64",
|
|
139
|
+
} as const;
|
|
140
|
+
|
|
141
|
+
export type AdsTablePinWidth = keyof typeof PIN_WIDTH_CLASS;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Coluna fixa (column-pinning). O UTable desloca a coluna pelo `size` do TanStack,
|
|
145
|
+
* então a largura da célula precisa ser a mesma do `size`.
|
|
146
|
+
*/
|
|
147
|
+
export const adsTablePinned = (width: AdsTablePinWidth, meta: AdsTableColumnMeta = {}) => ({
|
|
148
|
+
size: width,
|
|
149
|
+
meta: {
|
|
150
|
+
class: {
|
|
151
|
+
th: `${PIN_WIDTH_CLASS[width]} ${meta.class?.th ?? ""}`.trim(),
|
|
152
|
+
td: `${PIN_WIDTH_CLASS[width]} ${meta.class?.td ?? ""}`.trim(),
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export type AdsTableGroupTone = "brand" | "success" | "innovation" | "neutral";
|
|
158
|
+
|
|
159
|
+
const GROUP_UNDERLINE: Record<AdsTableGroupTone, string> = {
|
|
160
|
+
brand: "border-b-2 border-b-[var(--ads-button-primary)]",
|
|
161
|
+
success: "border-b-2 border-b-[var(--ads-status-success-500)]",
|
|
162
|
+
innovation: "border-b-2 border-b-[var(--ads-status-innovation-500)]",
|
|
163
|
+
neutral: "",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Grupo de colunas (Meus dados, Concorrentes, Mercado): risco de 2px no tom da fonte. */
|
|
167
|
+
export const adsTableGroup = (tone: AdsTableGroupTone = "neutral") => ({
|
|
168
|
+
class: { th: GROUP_UNDERLINE[tone] },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/** Variação: sinal e cor no tom 500, o mesmo do badge. Zero fica neutro. */
|
|
172
|
+
export const adsTableVariationClass = (value: number | null | undefined) => {
|
|
173
|
+
if (value == null || value === 0) return "text-[var(--ads-text-secondary)]";
|
|
174
|
+
return value > 0
|
|
175
|
+
? "font-bold text-[var(--ads-status-success-500)]"
|
|
176
|
+
: "font-bold text-[var(--ads-status-warning-500)]";
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** Marca o nível da linha para o degradê. Acima de 2 fica no tom mais claro. */
|
|
180
|
+
export const adsTableDepthAttr = (depth: number) => Math.min(Math.max(depth, 0), 2);
|
|
181
|
+
|
|
182
|
+
export type AdsTableHeat = "high" | "low" | "muted" | "empty";
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Preenche a célula inteira. O td zera o respiro quando encontra `data-cell-fill` e mantém
|
|
186
|
+
* o fundo opaco, para a coluna fixa não deixar ver o que rola por baixo.
|
|
187
|
+
* Fundo 100 e texto 800 para passar de 4.5:1.
|
|
188
|
+
*/
|
|
189
|
+
export const adsTableCellFill = (tone: AdsTableHeat) =>
|
|
190
|
+
`flex h-full min-h-11 w-full items-center justify-center px-4 py-3 text-center ${ADS_TABLE_HEAT_CLASS[tone]}`;
|
|
191
|
+
|
|
192
|
+
/** Faixa de IC. Fundo 100 e texto 800 para passar de 4.5:1. */
|
|
193
|
+
export const ADS_TABLE_HEAT_CLASS: Record<AdsTableHeat, string> = {
|
|
194
|
+
high: "bg-[var(--ads-button-secondary)] text-[var(--ads-button-primary-pressed)]",
|
|
195
|
+
low: "bg-[var(--ads-status-warning-100)] text-[var(--ads-status-warning-800)]",
|
|
196
|
+
muted: "bg-neutral-100 text-[var(--ads-text-tertiary)]",
|
|
197
|
+
empty: "text-[var(--ads-text-tertiary)]",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** Recuo da árvore, por profundidade. */
|
|
201
|
+
export const ADS_TABLE_TREE_INDENT = ["ps-0", "ps-5", "ps-10", "ps-15", "ps-20"] as const;
|
|
202
|
+
|
|
203
|
+
export const adsTableTreeIndent = (depth: number) =>
|
|
204
|
+
ADS_TABLE_TREE_INDENT[Math.min(depth, ADS_TABLE_TREE_INDENT.length - 1)];
|
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
import { CalendarDate, isEqualMonth, parseDate } from "@internationalized/date";
|
|
2
|
-
|
|
3
|
-
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
|
-
|
|
5
|
-
export type CalendarDateRangeValue = {
|
|
6
|
-
start: CalendarDate;
|
|
7
|
-
end?: CalendarDate;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export function dateStringToCalendarDate(
|
|
11
|
-
value: string,
|
|
12
|
-
): CalendarDate | undefined {
|
|
13
|
-
if (!DATE_ONLY_REGEX.test(value)) return undefined;
|
|
14
|
-
return parseDate(value);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function calendarDateToDateString(value: CalendarDate): string {
|
|
18
|
-
return value.toString();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function dateStringsToCalendarRange(
|
|
22
|
-
start: string,
|
|
23
|
-
end: string,
|
|
24
|
-
): CalendarDateRangeValue | undefined {
|
|
25
|
-
const startDate = dateStringToCalendarDate(start);
|
|
26
|
-
if (!startDate) return undefined;
|
|
27
|
-
|
|
28
|
-
const endDate = dateStringToCalendarDate(end);
|
|
29
|
-
if (!endDate) return { start: startDate };
|
|
30
|
-
|
|
31
|
-
return { start: startDate, end: endDate };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function calendarRangeToDateStrings(
|
|
35
|
-
range: CalendarDateRangeValue,
|
|
36
|
-
): { start: string; end: string } {
|
|
37
|
-
return {
|
|
38
|
-
start: calendarDateToDateString(range.start),
|
|
39
|
-
end: range.end ? calendarDateToDateString(range.end) : "",
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function isCalendarDateRange(
|
|
44
|
-
value: CalendarDate | CalendarDateRangeValue | undefined,
|
|
45
|
-
): value is CalendarDateRangeValue {
|
|
46
|
-
return Boolean(value && typeof value === "object" && "start" in value);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* A seleção emite a data clicada como placeholder. Se ela já está
|
|
51
|
-
* num dos dois meses visíveis, o par permanece. Só troca a janela
|
|
52
|
-
* quando a data cai fora dela.
|
|
53
|
-
*/
|
|
54
|
-
export function visibleMonthAfterPlaceholder(
|
|
55
|
-
currentLeft: CalendarDate,
|
|
56
|
-
incoming: CalendarDate,
|
|
57
|
-
): CalendarDate {
|
|
58
|
-
const left = currentLeft.set({ day: 1 });
|
|
59
|
-
const right = left.add({ months: 1 });
|
|
60
|
-
if (isEqualMonth(incoming, left) || isEqualMonth(incoming, right)) return left;
|
|
61
|
-
return incoming.set({ day: 1 });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function shiftVisibleMonth(
|
|
65
|
-
currentLeft: CalendarDate,
|
|
66
|
-
delta: -1 | 1,
|
|
67
|
-
min?: CalendarDate,
|
|
68
|
-
max?: CalendarDate,
|
|
69
|
-
): CalendarDate {
|
|
70
|
-
const left = currentLeft.set({ day: 1 });
|
|
71
|
-
const next = left.add({ months: delta });
|
|
72
|
-
if (delta < 0 && min) {
|
|
73
|
-
const lastOfTarget = next.add({ months: 1 }).subtract({ days: 1 });
|
|
74
|
-
if (lastOfTarget.compare(min) < 0) return left;
|
|
75
|
-
}
|
|
76
|
-
if (delta > 0 && max && next.add({ months: 1 }).compare(max) > 0) return left;
|
|
77
|
-
return next;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function isOutsideMaximumWindow(
|
|
81
|
-
date: CalendarDate,
|
|
82
|
-
anchor: CalendarDate,
|
|
83
|
-
maximumDays: number,
|
|
84
|
-
): boolean {
|
|
85
|
-
const earliest = anchor.subtract({ days: maximumDays - 1 });
|
|
86
|
-
const latest = anchor.add({ days: maximumDays - 1 });
|
|
87
|
-
return date.compare(earliest) < 0 || date.compare(latest) > 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function inclusiveCalendarDays(
|
|
91
|
-
start: CalendarDate,
|
|
92
|
-
end: CalendarDate,
|
|
93
|
-
): number {
|
|
94
|
-
const [from, to] = start.compare(end) <= 0 ? [start, end] : [end, start];
|
|
95
|
-
const milliseconds = to.toDate("UTC").getTime() - from.toDate("UTC").getTime();
|
|
96
|
-
return Math.round(milliseconds / 86_400_000) + 1;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function calendarMonthTitle(
|
|
100
|
-
date: CalendarDate,
|
|
101
|
-
locale = "pt-BR",
|
|
102
|
-
): string {
|
|
103
|
-
const label = new Intl.DateTimeFormat(locale, {
|
|
104
|
-
month: "long",
|
|
105
|
-
timeZone: "UTC",
|
|
106
|
-
}).format(date.toDate("UTC"));
|
|
107
|
-
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function calendarRangesEqual(
|
|
111
|
-
left: CalendarDate | CalendarDateRangeValue | undefined,
|
|
112
|
-
right: CalendarDate | CalendarDateRangeValue | undefined,
|
|
113
|
-
): boolean {
|
|
114
|
-
if (left === right) return true;
|
|
115
|
-
if (!left || !right) return !left && !right;
|
|
116
|
-
|
|
117
|
-
if (isCalendarDateRange(left) && isCalendarDateRange(right)) {
|
|
118
|
-
const leftEnd = left.end?.toString() ?? "";
|
|
119
|
-
const rightEnd = right.end?.toString() ?? "";
|
|
120
|
-
return left.start.toString() === right.start.toString() && leftEnd === rightEnd;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!isCalendarDateRange(left) && !isCalendarDateRange(right)) {
|
|
124
|
-
return left.toString() === right.toString();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
1
|
+
import { CalendarDate, isEqualMonth, parseDate } from "@internationalized/date";
|
|
2
|
+
|
|
3
|
+
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
|
+
|
|
5
|
+
export type CalendarDateRangeValue = {
|
|
6
|
+
start: CalendarDate;
|
|
7
|
+
end?: CalendarDate;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function dateStringToCalendarDate(
|
|
11
|
+
value: string,
|
|
12
|
+
): CalendarDate | undefined {
|
|
13
|
+
if (!DATE_ONLY_REGEX.test(value)) return undefined;
|
|
14
|
+
return parseDate(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function calendarDateToDateString(value: CalendarDate): string {
|
|
18
|
+
return value.toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function dateStringsToCalendarRange(
|
|
22
|
+
start: string,
|
|
23
|
+
end: string,
|
|
24
|
+
): CalendarDateRangeValue | undefined {
|
|
25
|
+
const startDate = dateStringToCalendarDate(start);
|
|
26
|
+
if (!startDate) return undefined;
|
|
27
|
+
|
|
28
|
+
const endDate = dateStringToCalendarDate(end);
|
|
29
|
+
if (!endDate) return { start: startDate };
|
|
30
|
+
|
|
31
|
+
return { start: startDate, end: endDate };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function calendarRangeToDateStrings(
|
|
35
|
+
range: CalendarDateRangeValue,
|
|
36
|
+
): { start: string; end: string } {
|
|
37
|
+
return {
|
|
38
|
+
start: calendarDateToDateString(range.start),
|
|
39
|
+
end: range.end ? calendarDateToDateString(range.end) : "",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isCalendarDateRange(
|
|
44
|
+
value: CalendarDate | CalendarDateRangeValue | undefined,
|
|
45
|
+
): value is CalendarDateRangeValue {
|
|
46
|
+
return Boolean(value && typeof value === "object" && "start" in value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A seleção emite a data clicada como placeholder. Se ela já está
|
|
51
|
+
* num dos dois meses visíveis, o par permanece. Só troca a janela
|
|
52
|
+
* quando a data cai fora dela.
|
|
53
|
+
*/
|
|
54
|
+
export function visibleMonthAfterPlaceholder(
|
|
55
|
+
currentLeft: CalendarDate,
|
|
56
|
+
incoming: CalendarDate,
|
|
57
|
+
): CalendarDate {
|
|
58
|
+
const left = currentLeft.set({ day: 1 });
|
|
59
|
+
const right = left.add({ months: 1 });
|
|
60
|
+
if (isEqualMonth(incoming, left) || isEqualMonth(incoming, right)) return left;
|
|
61
|
+
return incoming.set({ day: 1 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function shiftVisibleMonth(
|
|
65
|
+
currentLeft: CalendarDate,
|
|
66
|
+
delta: -1 | 1,
|
|
67
|
+
min?: CalendarDate,
|
|
68
|
+
max?: CalendarDate,
|
|
69
|
+
): CalendarDate {
|
|
70
|
+
const left = currentLeft.set({ day: 1 });
|
|
71
|
+
const next = left.add({ months: delta });
|
|
72
|
+
if (delta < 0 && min) {
|
|
73
|
+
const lastOfTarget = next.add({ months: 1 }).subtract({ days: 1 });
|
|
74
|
+
if (lastOfTarget.compare(min) < 0) return left;
|
|
75
|
+
}
|
|
76
|
+
if (delta > 0 && max && next.add({ months: 1 }).compare(max) > 0) return left;
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isOutsideMaximumWindow(
|
|
81
|
+
date: CalendarDate,
|
|
82
|
+
anchor: CalendarDate,
|
|
83
|
+
maximumDays: number,
|
|
84
|
+
): boolean {
|
|
85
|
+
const earliest = anchor.subtract({ days: maximumDays - 1 });
|
|
86
|
+
const latest = anchor.add({ days: maximumDays - 1 });
|
|
87
|
+
return date.compare(earliest) < 0 || date.compare(latest) > 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function inclusiveCalendarDays(
|
|
91
|
+
start: CalendarDate,
|
|
92
|
+
end: CalendarDate,
|
|
93
|
+
): number {
|
|
94
|
+
const [from, to] = start.compare(end) <= 0 ? [start, end] : [end, start];
|
|
95
|
+
const milliseconds = to.toDate("UTC").getTime() - from.toDate("UTC").getTime();
|
|
96
|
+
return Math.round(milliseconds / 86_400_000) + 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function calendarMonthTitle(
|
|
100
|
+
date: CalendarDate,
|
|
101
|
+
locale = "pt-BR",
|
|
102
|
+
): string {
|
|
103
|
+
const label = new Intl.DateTimeFormat(locale, {
|
|
104
|
+
month: "long",
|
|
105
|
+
timeZone: "UTC",
|
|
106
|
+
}).format(date.toDate("UTC"));
|
|
107
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function calendarRangesEqual(
|
|
111
|
+
left: CalendarDate | CalendarDateRangeValue | undefined,
|
|
112
|
+
right: CalendarDate | CalendarDateRangeValue | undefined,
|
|
113
|
+
): boolean {
|
|
114
|
+
if (left === right) return true;
|
|
115
|
+
if (!left || !right) return !left && !right;
|
|
116
|
+
|
|
117
|
+
if (isCalendarDateRange(left) && isCalendarDateRange(right)) {
|
|
118
|
+
const leftEnd = left.end?.toString() ?? "";
|
|
119
|
+
const rightEnd = right.end?.toString() ?? "";
|
|
120
|
+
return left.start.toString() === right.start.toString() && leftEnd === rightEnd;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!isCalendarDateRange(left) && !isCalendarDateRange(right)) {
|
|
124
|
+
return left.toString() === right.toString();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false;
|
|
128
|
+
}
|