@snksergio/design-system 0.61.0 → 0.62.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/dist-lib/ai/componentes/AlertModal.md +47 -0
- package/dist-lib/ai/componentes/AppShell.md +117 -0
- package/dist-lib/ai/componentes/Breadcrumb.md +187 -0
- package/dist-lib/ai/componentes/Button.md +116 -0
- package/dist-lib/ai/componentes/ButtonGroup.md +162 -0
- package/dist-lib/ai/componentes/CardCheckbox.md +99 -0
- package/dist-lib/ai/componentes/CardOption.md +133 -0
- package/dist-lib/ai/componentes/Chart.md +93 -0
- package/dist-lib/ai/componentes/Chip.md +68 -0
- package/dist-lib/ai/componentes/ChoroplethMap.md +118 -0
- package/dist-lib/ai/componentes/ColorPicker.md +70 -0
- package/dist-lib/ai/componentes/Combobox.md +51 -0
- package/dist-lib/ai/componentes/ConversationListItem.md +90 -0
- package/dist-lib/ai/componentes/DataList.md +111 -0
- package/dist-lib/ai/componentes/DataTable.md +867 -0
- package/dist-lib/ai/componentes/DatePicker.md +84 -0
- package/dist-lib/ai/componentes/DateSeparatorChip.md +59 -0
- package/dist-lib/ai/componentes/EmptyState.md +72 -0
- package/dist-lib/ai/componentes/FileUploadField.md +95 -0
- package/dist-lib/ai/componentes/FloatingPanel.md +118 -0
- package/dist-lib/ai/componentes/FooterTable.md +62 -0
- package/dist-lib/ai/componentes/FormField.md +110 -0
- package/dist-lib/ai/componentes/Gantt.md +552 -0
- package/dist-lib/ai/componentes/Header.md +98 -0
- package/dist-lib/ai/componentes/Icon.md +65 -0
- package/dist-lib/ai/componentes/Kanban.md +343 -0
- package/dist-lib/ai/componentes/Kpi.md +103 -0
- package/dist-lib/ai/componentes/List.md +61 -0
- package/dist-lib/ai/componentes/MarkdownText.md +59 -0
- package/dist-lib/ai/componentes/MenuSidebar.md +128 -0
- package/dist-lib/ai/componentes/MessageAck.md +53 -0
- package/dist-lib/ai/componentes/MessageBubble.md +115 -0
- package/dist-lib/ai/componentes/MessageComposer.md +80 -0
- package/dist-lib/ai/componentes/MessageVariablesPicker.md +104 -0
- package/dist-lib/ai/componentes/Modal.md +88 -0
- package/dist-lib/ai/componentes/MonthYearPicker.md +49 -0
- package/dist-lib/ai/componentes/PageHeader.md +129 -0
- package/dist-lib/ai/componentes/Panel.md +84 -0
- package/dist-lib/ai/componentes/Scheduler.md +421 -0
- package/dist-lib/ai/componentes/ScreenLoader.md +60 -0
- package/dist-lib/ai/componentes/SingleMenuSidebar.md +171 -0
- package/dist-lib/ai/componentes/Spinner.md +52 -0
- package/dist-lib/ai/componentes/Table.md +192 -0
- package/dist-lib/ai/componentes/TableToolbar.md +87 -0
- package/dist-lib/ai/componentes/TabsNavigation.md +152 -0
- package/dist-lib/ai/componentes/Toast.md +49 -0
- package/dist-lib/ai/componentes/_primitivos.md +74 -0
- package/dist-lib/ai/componentes/avatar-ig.md +181 -0
- package/dist-lib/ai/componentes/indice.json +49 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-brazil-map.ts +33 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-screen.tsx +1110 -0
- package/dist-lib/ai/exemplos/dashboard/index.ts +1 -0
- package/dist-lib/ai/global/componentes.md +217 -0
- package/dist-lib/ai/global/composicao.md +182 -0
- package/dist-lib/ai/indice.json +177 -0
- package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
- package/dist-lib/ai/manifest.json +16 -0
- package/dist-lib/ai/regras/design.md +88 -0
- package/dist-lib/ai/regras/temas.md +192 -0
- package/dist-lib/ai/regras-por-componente.json +103 -0
- package/dist-lib/ai/roteiros/dashboard/blueprint.md +47 -0
- package/dist-lib/ai/roteiros/dashboard/entrevista.md +62 -0
- package/dist-lib/ai/roteiros/dashboard/geracao.md +88 -0
- package/dist-lib/ai/roteiros/dashboard/roteiro.md +88 -0
- package/package.json +4 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# ScreenLoader
|
|
2
|
+
|
|
3
|
+
<!-- ds:regras
|
|
4
|
+
- omita `size`: `md` é o calibrado pro slot do AppShell; `lg` não é "pra dar destaque"
|
|
5
|
+
- omita `skeletonLayout`: `page` serve pra qualquer tela — só mude se ela TEM KPIs no topo
|
|
6
|
+
- o pai precisa ter altura, senão a variante spinner colapsa no topo
|
|
7
|
+
- loading inline (botão, célula) → `Spinner` direto; overlay é composição do consumidor
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
**Categoria:** iGreen (tv()). Estado de **carregamento de página/área** — preenche o container pai e mostra um indicador enquanto o conteúdo processa. Irmão do `EmptyState` (mesma família de "estado de área").
|
|
11
|
+
|
|
12
|
+
## Quando usar
|
|
13
|
+
|
|
14
|
+
- Conteúdo de uma página inteira carregando dentro do slot de conteúdo do AppShell.
|
|
15
|
+
- Área grande (card, section, painel) aguardando primeira carga de dados.
|
|
16
|
+
- **Não** é pra loading inline (botão, célula) — aí use `Spinner` direto.
|
|
17
|
+
- **Não** é overlay sobre conteúdo já renderizado — cobre o slot vazio; sobrepor é composição do consumidor.
|
|
18
|
+
|
|
19
|
+
## Props essenciais
|
|
20
|
+
|
|
21
|
+
| Prop | Tipo | Default | Descrição |
|
|
22
|
+
|------|------|---------|-----------|
|
|
23
|
+
| `variant` | `"spinner" \| "skeleton"` | `"spinner"` | `spinner` = Spinner centrado + título + descrição. `skeleton` = silhueta genérica de página, sem prever o layout final. |
|
|
24
|
+
| `skeletonLayout` | `"page" \| "dashboard" \| "kpis"` | `"page"` | Blocos da silhueta (só `skeleton`): `page` = header + conteúdo · `dashboard` = header + linha de 4 KPIs + conteúdo · `kpis` = KPIs + conteúdo, sem header. |
|
|
25
|
+
| `title` | `string` | `"Carregando…"` | Visível no `spinner`; sr-only no `skeleton` (leitores anunciam via `role="status"`). |
|
|
26
|
+
| `description` | `string` | — | Linha auxiliar sob o título (só `spinner`). |
|
|
27
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Escala spinner + tipografia do título. Sem efeito no `skeleton`. **`md` é o padrão de uso** — não passe `size` a menos que a área destoe (ver Gotchas). |
|
|
28
|
+
| `color` | cores do Spinner | `"brand"` | Repassada ao Spinner (só `spinner`). |
|
|
29
|
+
| `className` | `string` | — | Overrides no root (aceita `ref` de `HTMLDivElement`). |
|
|
30
|
+
|
|
31
|
+
## Exemplo mínimo
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { ScreenLoader } from "@snksergio/design-system";
|
|
35
|
+
|
|
36
|
+
// dentro do slot de conteúdo (o pai precisa ter altura)
|
|
37
|
+
{isLoading ? (
|
|
38
|
+
<ScreenLoader title="Carregando clientes" description="Buscando os dados mais recentes…" />
|
|
39
|
+
) : (
|
|
40
|
+
<ClientesPage />
|
|
41
|
+
)}
|
|
42
|
+
|
|
43
|
+
// variação skeleton — silhueta genérica em vez de spinner
|
|
44
|
+
{isLoading ? <ScreenLoader variant="skeleton" /> : <ClientesPage />}
|
|
45
|
+
|
|
46
|
+
// tela de painel: skeleton com linha de KPIs (com ou sem header)
|
|
47
|
+
{isLoading ? <ScreenLoader variant="skeleton" skeletonLayout="dashboard" /> : <Dashboard />}
|
|
48
|
+
{isLoading ? <ScreenLoader variant="skeleton" skeletonLayout="kpis" /> : <KpiSection />}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Gotchas
|
|
52
|
+
|
|
53
|
+
- **Padrão de uso: omita `size` (= `md`).** É o calibrado pro slot de conteúdo do AppShell. `sm` só pra área comprovadamente pequena (card baixo, painel lateral); `lg` só pra tela cheia vazia sem chrome. Não escolha `lg` "pra dar destaque".
|
|
54
|
+
- **Padrão do skeleton: omita `skeletonLayout` (= `page`, header + conteúdo).** É a silhueta genérica que serve pra qualquer tela — só saia dela quando a tela alvo COMPROVADAMENTE tem KPIs no topo: `dashboard` (com header de página) ou `kpis` (quando o header já está renderizado fora da área que carrega). Não invente combinação além dessas três: layout mais específico que isso = compor `Skeleton` na mão.
|
|
55
|
+
|
|
56
|
+
- **O pai precisa ter altura.** O componente preenche o container (`h-full flex-1`) — num pai sem altura definida, a variante `spinner` colapsa no topo (o `skeleton` tem `min-h` próprio de fallback). No AppShell o slot de conteúdo já tem altura.
|
|
57
|
+
- **Nunca `position: fixed`** — cobrir o viewport inteiro (splash, auth guard) é composição do consumidor, de propósito.
|
|
58
|
+
- **Skeleton genérico de propósito**: quando o layout final é conhecido (tabela, lista de cards), componha `<Skeleton>` na mão desenhando a silhueta real — o `DataTable`/`DataList` já trazem os próprios skeletons; não empilhe este por cima.
|
|
59
|
+
- **A11y sem duplicação**: o root tem `role="status"` + `aria-live="polite"`; o Spinner interno vai `aria-hidden`. Não embrulhe em outro `role="status"`.
|
|
60
|
+
- A rotação do Spinner para sob `prefers-reduced-motion` (`motion-reduce:animate-none`); o pulse do Skeleton (só opacidade, sem deslocamento) **não** é gated — é o comportamento do `shadcn/skeleton.tsx`.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# SingleMenuSidebar
|
|
2
|
+
|
|
3
|
+
<!-- ds:regras
|
|
4
|
+
- NÃO passe `logo`: o default é a marca iGreen. Só com marca própria pedida explicitamente
|
|
5
|
+
- `title` = nome do projeto (vai à direita da logo) — pergunte, não invente
|
|
6
|
+
- é a escolha quando NÃO há divisão em áreas: `showSearch={false}`, sem `module`/`modules`
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
**O que é** — Sidebar de navegação de **nível único**: categoria → sub-itens em
|
|
10
|
+
accordion (1 aberto por vez). Categoria sem `items` é link simples.
|
|
11
|
+
**Categoria**: Templates / App-level (mesma família de `MenuSidebar`, `Header`, `AppShell`).
|
|
12
|
+
|
|
13
|
+
**Quando usar** — App com navegação plana, sem múltiplos contextos/rail. É a
|
|
14
|
+
alternativa **enxuta** ao `MenuSidebar` (que tem rail + painel + sections +
|
|
15
|
+
badges). Sem variantes — escolha o componente pela necessidade, não por props.
|
|
16
|
+
Precisa de rail + contextos + bookmarks/chats? → use `MenuSidebar`.
|
|
17
|
+
|
|
18
|
+
**Qual das duas** — o critério é *o sistema tem áreas grandes e separadas, cada uma com o
|
|
19
|
+
menu dela?* Sim → `MenuSidebar` (as áreas ficam todas visíveis no rail). Não → esta, na
|
|
20
|
+
variação **"Sem módulo / sem busca"** (omita `module`/`modules`, passe `showSearch={false}`).
|
|
21
|
+
Ela **também** aceita módulos, mas aí eles ficam atrás de um botão acima da busca — por isso
|
|
22
|
+
não é a recomendação quando a divisão existe.
|
|
23
|
+
|
|
24
|
+
**A logo é a da iGreen por default** — `logo` é opcional desde a 0.46.0 e cai em
|
|
25
|
+
`SidebarBrandMark`. Só passe quando o app tem marca própria; `title` é o **nome do projeto**,
|
|
26
|
+
exibido à direita dela.
|
|
27
|
+
|
|
28
|
+
> ⚠️ Até a 0.46.0 `logo` era **obrigatória e sem fallback**, enquanto o `brand` do `MenuSidebar`
|
|
29
|
+
> sempre foi opcional. Trocar pra esta sidebar portanto *forçava* quem montava a inventar uma
|
|
30
|
+
> logo — e foi assim que a marca da iGreen sumiu num app de consumidor (2026-08-22).
|
|
31
|
+
|
|
32
|
+
## Props essenciais
|
|
33
|
+
|
|
34
|
+
| Prop | Tipo | Default | Obrigatório |
|
|
35
|
+
| ------------------------------- | ----------------------------- | ------- | ----------- |
|
|
36
|
+
| `logo` | `ReactNode` | **marca iGreen** | — (omita p/ ficar com a marca) |
|
|
37
|
+
| `title` | `string` | — | ✅ |
|
|
38
|
+
| `user` | `SingleMenuUser` | — | ✅ |
|
|
39
|
+
| `categories` | `SingleMenuCategory[]` | — | opcional se usar `modules` |
|
|
40
|
+
| `modules` | `SingleMenuModuleConfig[]` | — | |
|
|
41
|
+
| `activeModuleId` / `defaultModuleId` / `onModuleChange` | multi-módulo (controlado / inicial / callback) | — | |
|
|
42
|
+
| `module` | `SingleMenuModule` | — | **ignorado se `modules`** |
|
|
43
|
+
| `showSearch` | `boolean` | `true` | |
|
|
44
|
+
| `searchCommand` | `ReactNode` | — | |
|
|
45
|
+
| `searchPlaceholder` | `string` | — | |
|
|
46
|
+
| `activeItemId` | `string` | — | |
|
|
47
|
+
| `onItemClick` | `(id: string) => void` | — | |
|
|
48
|
+
| `defaultExpanded` | `boolean` | `true` | |
|
|
49
|
+
| `expanded` / `onExpandedChange` | toggle controlado | — | |
|
|
50
|
+
| `showToggleIndicator` | `boolean` | `false` | |
|
|
51
|
+
|
|
52
|
+
⚠️ **A busca NÃO é input controlado.** `searchValue`/`onSearchChange`/`searchRef` **não
|
|
53
|
+
existem** nesta API (estavam documentados aqui e nunca foram props do componente). A busca
|
|
54
|
+
abre um `CommandDialog`: você passa o **conteúdo** dele em `searchCommand` e, se quiser, o
|
|
55
|
+
texto do placeholder em `searchPlaceholder`. As props `value`/`onChange`/`inputRef` existem
|
|
56
|
+
em `SingleMenuSearchProps`, que é subcomponente interno.
|
|
57
|
+
|
|
58
|
+
### Multi-módulo
|
|
59
|
+
|
|
60
|
+
`modules` é a API pra sidebar que troca de contexto: cada `SingleMenuModuleConfig` traz suas
|
|
61
|
+
próprias `categories`, e trocar de módulo **sobrepõe** as categorias exibidas. Quando
|
|
62
|
+
`modules` é passado, `module` (singular) é ignorado e `categories` no nível raiz vira
|
|
63
|
+
opcional.
|
|
64
|
+
|
|
65
|
+
## Data model
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
SingleMenuCategory = { id, icon, label, href?, items?, active? }
|
|
69
|
+
SingleMenuSubItem = { id, label, href? }
|
|
70
|
+
SingleMenuModule = { icon, title, subtitle, options?, onModuleChange? }
|
|
71
|
+
SingleMenuUser = { name, email, avatar?, actions?, onAction? }
|
|
72
|
+
SingleMenuUserAction= { id, label, icon?, variant?: "default" | "destructive" }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Navegação — `href` e `renderLink`
|
|
76
|
+
|
|
77
|
+
Item **com `href`** vira `<a href>`: ctrl/cmd+clique abre em nova aba, "copiar endereço do
|
|
78
|
+
link" funciona, e o leitor de tela anuncia **link** (com `aria-current="page"` no ativo).
|
|
79
|
+
Sem `href`, continua `<button>`. Vale pro sub-item e pra **categoria-folha** (sem `items`).
|
|
80
|
+
|
|
81
|
+
Pra integrar o router do consumidor, use **`renderLink`** — render-prop, não componente:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<SingleMenuSidebar
|
|
85
|
+
categories={categories}
|
|
86
|
+
onItemClick={(id) => setAtivo(id)}
|
|
87
|
+
renderLink={({ href, ...rest }) => <Link to={href} {...rest} />}
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
⚠️ **Sem `renderLink`**, o clique cancela a navegação nativa quando há handler — **exceto**
|
|
92
|
+
em clique modificado, `target="_blank"`, href externo (`https:`, `mailto:`…) e **href de
|
|
93
|
+
hash** (`#/rota`). A regra e o porquê de cada exceção estão em `@/utils/nav-link`, que é a
|
|
94
|
+
MESMA lógica do `MenuSidebar` (util compartilhada, não cópia).
|
|
95
|
+
|
|
96
|
+
> ⚠️ Até 2026-08-18 o `href` acima **não fazia nada**: o tipo o aceitava, este USAGE o
|
|
97
|
+
> documentava, e o componente renderizava `<button>` sempre. Se você escreveu código
|
|
98
|
+
> contando com navegação por `href` aqui, ele nunca navegou — agora navega.
|
|
99
|
+
|
|
100
|
+
## Exemplo mínimo
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { SingleMenuSidebar } from "@/components/ui/SingleMenuSidebar";
|
|
104
|
+
|
|
105
|
+
<SingleMenuSidebar
|
|
106
|
+
logo={<Logo />}
|
|
107
|
+
title="Sólis iGreen"
|
|
108
|
+
module={{
|
|
109
|
+
icon: <Zap />,
|
|
110
|
+
title: "Créditos",
|
|
111
|
+
subtitle: "MÓDULO ATIVO",
|
|
112
|
+
options,
|
|
113
|
+
}}
|
|
114
|
+
categories={[
|
|
115
|
+
{ id: "dashboard", icon: <LayoutGrid />, label: "Dashboard", active: true },
|
|
116
|
+
{
|
|
117
|
+
id: "instalacoes",
|
|
118
|
+
icon: <Zap />,
|
|
119
|
+
label: "Instalações",
|
|
120
|
+
items: [
|
|
121
|
+
{ id: "contratos", label: "Contratos" },
|
|
122
|
+
{ id: "vistorias", label: "Vistorias" },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
]}
|
|
126
|
+
user={{ name: "Sérgio", email: "sergio@igreen.com.br", actions }}
|
|
127
|
+
activeItemId={activeItemId}
|
|
128
|
+
onItemClick={setActiveItemId}
|
|
129
|
+
/>;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Comportamentos
|
|
133
|
+
|
|
134
|
+
- **Accordion** — apenas 1 categoria aberta por vez. Definir `activeItemId` abre
|
|
135
|
+
automaticamente a categoria que contém o item.
|
|
136
|
+
- **Seleção única** — sempre 1 item marcado: folha ativa OU pai aberto (abrir um
|
|
137
|
+
pai o marca e suprime a folha) OU pai que contém o sub-item ativo. Clicar numa
|
|
138
|
+
folha fecha o pai aberto e assume a marca.
|
|
139
|
+
- **Toggle** — botão no header trava/destrava o estado expandido. Após recolher
|
|
140
|
+
manualmente, o hover-expand fica suprimido ~500ms (não "pisca" com o mouse por cima).
|
|
141
|
+
- **Hover-to-expand** — recolhida, o hover sobre a sidebar a expande
|
|
142
|
+
temporariamente; sai o mouse, recolhe (~200ms). Categorias mostram tooltip.
|
|
143
|
+
⚠️ **A espiada FLUTUA sobre o conteúdo — não empurra** (v0.43.0+). No desktop o
|
|
144
|
+
painel vira `absolute z-40` e o `<aside>` continua ocupando só a largura do rail,
|
|
145
|
+
então o que está ao lado não se mexe. **Só o clique** (travar aberta) ocupa espaço
|
|
146
|
+
no fluxo e empurra — é decisão de layout do usuário, e quem está com o menu
|
|
147
|
+
recolhido escolheu maximizar a área de conteúdo.
|
|
148
|
+
Antes disso a espiada animava a largura **dentro** do fluxo: numa tela com
|
|
149
|
+
`DataTable` isso disparava 5 recálculos completos de largura de coluna por gesto,
|
|
150
|
+
~100ms de travada e **CLS 0,117**. É o mesmo desenho que o `MenuSidebar` sempre
|
|
151
|
+
teve (painel flutuante), agora aqui. Se você depende do empurrão no hover, use
|
|
152
|
+
`expanded` controlado e trave aberta.
|
|
153
|
+
- **Controlado/não-controlado** — `expanded` + `onExpandedChange` (controlado) ou
|
|
154
|
+
`defaultExpanded` (não-controlado).
|
|
155
|
+
- **Responsivo (mobile)** — abaixo de `md` (768px) a sidebar ocupa **100% da
|
|
156
|
+
largura** (pronta pra drawer); no desktop mantém a largura fixa (280px/80px). A
|
|
157
|
+
sidebar é **dumb**: exibir/ocultar no mobile é responsabilidade do consumidor
|
|
158
|
+
(um toggle/drawer controlado pelo seu app — veja o exemplo "Responsivo (mobile)").
|
|
159
|
+
|
|
160
|
+
## Gotchas
|
|
161
|
+
|
|
162
|
+
- **Sem variantes (por design).** Não há `variant`/`size`. Mudança visual = editar
|
|
163
|
+
`single-menu-sidebar.styles.ts`. Outra forma de navegação = outro componente.
|
|
164
|
+
- **Dá altura ao container.** O `<aside>` é `h-full` — o pai precisa ter altura
|
|
165
|
+
(ex.: `h-[680px]` ou `flex-1 min-h-0` num pai `h-full`).
|
|
166
|
+
- **`logo` e `avatar` são ReactNode** — você controla o tamanho; o slot do logo só
|
|
167
|
+
faz `shrink-0`. Passe um elemento já dimensionado (ex.: caixa `size-form-lg`).
|
|
168
|
+
- **Cores 100% via tokens DS.** Estado marcado = `fg-brand` + `bg-sidebar-accent`;
|
|
169
|
+
rodapé/hover = `bg-sidebar-accent`. Não usa palette própria `sidebar-*`.
|
|
170
|
+
- **`<TooltipProvider>` embutido** — o componente já envolve a árvore; não precisa
|
|
171
|
+
de provider externo só pra ele.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Spinner
|
|
2
|
+
|
|
3
|
+
**Categoria:** iGreen (tv()). Indicador de **loading** (SVG que gira).
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
|
|
7
|
+
- Estado de carregamento inline: dentro de botões (`Salvando…`), ao lado de um label, em placeholders de área que ainda vai popular.
|
|
8
|
+
- Para um bloco de página inteiro carregando, componha com o seu próprio layout (ex.: centralizar `<Spinner size="lg" />`).
|
|
9
|
+
|
|
10
|
+
Não é barra de progresso determinística — para isso use `Progress`.
|
|
11
|
+
|
|
12
|
+
## Props essenciais
|
|
13
|
+
|
|
14
|
+
| Prop | Tipo | Default | Descrição |
|
|
15
|
+
|------|------|---------|-----------|
|
|
16
|
+
| `size` | `"sm" \| "md" \| "lg" \| "xl" \| "2xl"` | `"md"` | Tamanho via `size-icon-*` (16/20/24/32/40px). `xl`/`2xl` são pra loading de área/página (é o que o `ScreenLoader` usa). |
|
|
17
|
+
| `color` | `"current" \| "default" \| "muted" \| "brand" \| "on-brand"` | `"muted"` | Cor via `text-fg-*`. `current` herda a cor do texto do pai. |
|
|
18
|
+
| `label` | `string` | `"Carregando"` | Rótulo acessível (`role="status"`). |
|
|
19
|
+
| `className` | `string` | — | Overrides (o SVG é o próprio nó — aceita `ref` para `SVGSVGElement`). |
|
|
20
|
+
|
|
21
|
+
Aceita qualquer prop de `<svg>` (menos `color`, sobrescrita pela variante).
|
|
22
|
+
|
|
23
|
+
## Variantes
|
|
24
|
+
|
|
25
|
+
| Variante | Valores |
|
|
26
|
+
|----------|---------|
|
|
27
|
+
| `size` | `sm` · `md` · `lg` · `xl` · `2xl` |
|
|
28
|
+
| `color` | `current` · `default` · `muted` · `brand` · `on-brand` |
|
|
29
|
+
|
|
30
|
+
## Exemplo mínimo
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Spinner } from "@snksergio/design-system";
|
|
34
|
+
|
|
35
|
+
// standalone, neutro
|
|
36
|
+
<Spinner />
|
|
37
|
+
|
|
38
|
+
// dentro de um botão primário (herda o branco do texto do botão)
|
|
39
|
+
<Button disabled>
|
|
40
|
+
<Spinner size="sm" color="current" aria-hidden />
|
|
41
|
+
Salvando…
|
|
42
|
+
</Button>
|
|
43
|
+
|
|
44
|
+
// destaque de marca, maior
|
|
45
|
+
<Spinner size="lg" color="brand" label="Carregando ranking" />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Gotchas
|
|
49
|
+
|
|
50
|
+
- **Respeita `prefers-reduced-motion`**: quando o usuário pede menos movimento, a rotação para (`motion-reduce:animate-none`) — o spinner fica estático mas ainda comunica "carregando" via `role="status"`.
|
|
51
|
+
- **Decorativo dentro de botões**: passe `aria-hidden` — aí o `label`/`role="status"` são omitidos (o texto do botão já anuncia o estado) e evita anúncio duplicado no leitor de tela.
|
|
52
|
+
- O traço usa `currentColor`; `color="current"` é o caminho para casar com a cor do container.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Table — primitiva burra de grid
|
|
2
|
+
|
|
3
|
+
`Table` é a **primitiva de baixo nível** que renderiza estrutura visual de grid: head, body, rows, cells. Sem lógica de dados. Use-a quando precisar de uma tabela leve sem o overhead do `DataTable` (smart wrapper que orquestra filtro/sort/pagination/virtualization).
|
|
4
|
+
|
|
5
|
+
## Anatomia
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Table density="standard" cellBorders ariaLabel="Lista de itens">
|
|
9
|
+
<TableHead>
|
|
10
|
+
<TableHeadCell field="id" sortable sortDirection="asc" icon={Hash} width={120}>
|
|
11
|
+
ID
|
|
12
|
+
</TableHeadCell>
|
|
13
|
+
<TableHeadCell field="name" sortable resizable width={240}>
|
|
14
|
+
Nome
|
|
15
|
+
</TableHeadCell>
|
|
16
|
+
</TableHead>
|
|
17
|
+
|
|
18
|
+
<TableBody>
|
|
19
|
+
{rows.map((row) => (
|
|
20
|
+
<TableRow key={row.id} selected={isSelected(row)} onClick={() => open(row)}>
|
|
21
|
+
<TableCell field="id" width={120}>{row.id}</TableCell>
|
|
22
|
+
<TableCell field="name" width={240} ellipsis>{row.name}</TableCell>
|
|
23
|
+
</TableRow>
|
|
24
|
+
))}
|
|
25
|
+
</TableBody>
|
|
26
|
+
</Table>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Componentes
|
|
30
|
+
|
|
31
|
+
| Componente | Responsabilidade |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `Table` | Root grid container. Aceita `scrollRef` externa (pra virtualization). Expõe `cardBreakpoint?: number \| false` (default `768`) — hoje **no-op** no Table puro (ver Pattern 4). |
|
|
34
|
+
| `TableHead` | Sticky row group de headers. `sticky` toggle. `rootProps` pra HTMLAttributes. |
|
|
35
|
+
| `TableHeadCell` | Cell de header. `forwardRef`. Suporta sort, resize, icon, headMenu slot. |
|
|
36
|
+
| `TableBody` | Row group de body. `virtualized={{ totalHeight }}` ativa modo absolute layout. |
|
|
37
|
+
| `TableRow` | `forwardRef`. `selected`/`open`/`focused`/`clickable` variants. |
|
|
38
|
+
| `TableCell` | `forwardRef`. Cell de dados. `purpose="selection"` remove padding (checkbox). |
|
|
39
|
+
| `TableCardRow` | Modo card (mobile) — substitui TableRow inteira por card vertical. Pra auto-switch baseado em viewport, prefira `<DataTable cardBreakpoint={...}>` (faz o mapeamento automaticamente). |
|
|
40
|
+
|
|
41
|
+
## Hooks expostos
|
|
42
|
+
|
|
43
|
+
| Hook | Pra que serve |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `useColumnWidths(columns)` | Calcula `widths` efetivos + `offsets` cumulativos pra colunas pinned. |
|
|
46
|
+
| `useColumnResize({ currentWidth, onResize, onResizeEnd, onResizeLiveDOM })` | Drag-to-resize handle. 3 callbacks: live DOM (síncrono), onResize (mousemove), onResizeEnd (mouseup). |
|
|
47
|
+
|
|
48
|
+
## Constants
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { SELECTION_COLUMN_WIDTH, TABLE_HEADER_HEIGHT } from "@/components/ui/Table";
|
|
52
|
+
|
|
53
|
+
// SELECTION_COLUMN_WIDTH = 56 — usar em TableHeadCell/TableCell width
|
|
54
|
+
// TABLE_HEADER_HEIGHT = 42 — alinhar skeletons, totalizers, group headers
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Patterns
|
|
58
|
+
|
|
59
|
+
### 1. Coluna de seleção (checkbox)
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { SELECTION_COLUMN_WIDTH } from "@/components/ui/Table";
|
|
63
|
+
|
|
64
|
+
<TableHeadCell width={SELECTION_COLUMN_WIDTH} purpose="selection">
|
|
65
|
+
<Checkbox checked={...} onCheckedChange={...} />
|
|
66
|
+
</TableHeadCell>
|
|
67
|
+
|
|
68
|
+
<TableCell width={SELECTION_COLUMN_WIDTH} purpose="selection">
|
|
69
|
+
<Checkbox checked={...} onCheckedChange={...} />
|
|
70
|
+
</TableCell>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`purpose="selection"` é o **padrão pra coluna de checkbox**: remove o padding interno **e centraliza o checkbox** (header + body) automaticamente nos 56px — **não precisa** passar `align="center"`. Vale tanto no `<TableHeadCell>` quanto no `<TableCell>`. Use `width={SELECTION_COLUMN_WIDTH}` (56px) pra largura.
|
|
74
|
+
|
|
75
|
+
### 2. Resize column com state externo
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
const [colWidths, setColWidths] = useState<Record<string, number>>({});
|
|
79
|
+
|
|
80
|
+
<TableHeadCell
|
|
81
|
+
field="name"
|
|
82
|
+
width={colWidths.name ?? 240}
|
|
83
|
+
resizable
|
|
84
|
+
// mousemove → side-effect (live DOM já é feito internamente pelo TableHeadCell)
|
|
85
|
+
onResize={(w) => console.log("dragging:", w)}
|
|
86
|
+
// mouseup → commit no state
|
|
87
|
+
onResizeEnd={(w) => setColWidths((prev) => ({ ...prev, name: w }))}
|
|
88
|
+
>
|
|
89
|
+
Nome
|
|
90
|
+
</TableHeadCell>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Virtualization (`@tanstack/react-virtual`)
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
97
|
+
const virtualizer = useVirtualizer({
|
|
98
|
+
count: rows.length,
|
|
99
|
+
getScrollElement: () => scrollRef.current,
|
|
100
|
+
estimateSize: () => 56,
|
|
101
|
+
overscan: 10,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
<Table scrollRef={scrollRef}>
|
|
105
|
+
<TableHead>{/* ... */}</TableHead>
|
|
106
|
+
<TableBody virtualized={{ totalHeight: virtualizer.getTotalSize() }}>
|
|
107
|
+
{virtualizer.getVirtualItems().map((vi) => (
|
|
108
|
+
<TableRow
|
|
109
|
+
key={rows[vi.index].id}
|
|
110
|
+
rootProps={{
|
|
111
|
+
style: {
|
|
112
|
+
position: "absolute",
|
|
113
|
+
top: 0,
|
|
114
|
+
left: 0,
|
|
115
|
+
width: "100%",
|
|
116
|
+
height: `${vi.size}px`,
|
|
117
|
+
transform: `translateY(${vi.start}px)`,
|
|
118
|
+
},
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{/* cells */}
|
|
122
|
+
</TableRow>
|
|
123
|
+
))}
|
|
124
|
+
</TableBody>
|
|
125
|
+
</Table>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 4. Card mode manual (TableCardRow + matchMedia)
|
|
129
|
+
|
|
130
|
+
Pro `<Table>` puro, o auto-switch row→card NÃO é automático — você decide via `matchMedia` ou ResizeObserver:
|
|
131
|
+
|
|
132
|
+
> ⚠️ `TableProps` expõe `cardBreakpoint?: number | false` (default `768`), mas no `<Table>` puro a prop é **no-op**: ela só escreve a CSS var `--table-card-bp` no root — nenhuma regra CSS/`@container` consome essa var hoje. Não passe `cardBreakpoint` no `<Table>` esperando card mode automático; o auto-switch real é o do `<DataTable cardBreakpoint={...}>`, implementado via JS.
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { useMediaQuery } from "@/components/ui/MenuSidebar/use-media-query";
|
|
136
|
+
import { TableCardRow } from "@/components/ui/Table";
|
|
137
|
+
|
|
138
|
+
const isMobile = useMediaQuery("(max-width: 767px)");
|
|
139
|
+
|
|
140
|
+
return isMobile ? (
|
|
141
|
+
<div className="flex flex-col gap-gp-md">
|
|
142
|
+
{rows.map((row) => (
|
|
143
|
+
<TableCardRow
|
|
144
|
+
key={row.id}
|
|
145
|
+
header={<strong>{row.name}</strong>}
|
|
146
|
+
items={[
|
|
147
|
+
{ label: "Email", value: row.email },
|
|
148
|
+
{ label: "Status", value: row.status },
|
|
149
|
+
]}
|
|
150
|
+
/>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
) : (
|
|
154
|
+
<Table>{/* ... */}</Table>
|
|
155
|
+
);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
> Pra evitar o trabalho manual, use `<DataTable cardBreakpoint={768}>` — ele mapeia automaticamente as colunas (selection + primary + actions + items) e renderiza `<TableCardRow>` abaixo do breakpoint.
|
|
159
|
+
|
|
160
|
+
### 5. Sticky pinned columns
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
const cols = [
|
|
164
|
+
{ field: "id", width: 120, pinned: "left" as const },
|
|
165
|
+
{ field: "name", width: 240, pinned: "left" as const },
|
|
166
|
+
{ field: "email", width: 240 },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const { widths, offsets } = useColumnWidths(cols);
|
|
170
|
+
|
|
171
|
+
<TableHeadCell field="id" width={widths.id} pinned="left" pinOffset={offsets.id}>
|
|
172
|
+
ID
|
|
173
|
+
</TableHeadCell>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Table vs DataTable — quando usar qual
|
|
177
|
+
|
|
178
|
+
| Use **Table** quando | Use **DataTable** quando |
|
|
179
|
+
|---|---|
|
|
180
|
+
| Tabela estática sem filtros/sort/paginação | CRM/admin com filtros, sort, etc |
|
|
181
|
+
| Lista simples controlada por estado próprio | Server mode ou client mode com mock |
|
|
182
|
+
| Quer composição máxima manual | Quer setup rápido via prop config |
|
|
183
|
+
| Tabela dentro de modal/drawer pequeno | Página inteira com toolbar + footer |
|
|
184
|
+
|
|
185
|
+
## Princípio dumb
|
|
186
|
+
|
|
187
|
+
`Table` **não armazena estado de dados**. Tudo é controlado externamente:
|
|
188
|
+
- Selection → consumer mantém `Set<id>` e passa pra cada `TableRow`
|
|
189
|
+
- Sort → consumer decide `sortDirection` por cell e implementa onClick
|
|
190
|
+
- Pagination → consumer fatia rows antes de mapear
|
|
191
|
+
|
|
192
|
+
O Table guarda **apenas** estado visual interno (resize hover, scroll detection) — via `TableContext` privado, nunca exposto.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# TableToolbar — toolbar opinativa de tabela
|
|
2
|
+
|
|
3
|
+
Toolbar **padrão** (e única) do DataTable. Layout OPINATIVO: slots semânticos em
|
|
4
|
+
ordem fixa — o consumer não monta a ordem, impossível montar errado.
|
|
5
|
+
|
|
6
|
+
## Quando usar
|
|
7
|
+
|
|
8
|
+
- Você está montando uma toolbar de tabela custom (fora do DataTable) e quer o
|
|
9
|
+
visual/UX canônico do DS.
|
|
10
|
+
- Dentro do DataTable é automático — não precisa instanciar à mão.
|
|
11
|
+
|
|
12
|
+
## Ordem renderizada (fixa)
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Esquerda: viewToggle · ⟨divider⟩ · savedViews
|
|
16
|
+
Direita: refresh · actions · search · filter · settings · more
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Em `< md`: o `refresh` some (o menu Configurações cobre o resto).
|
|
20
|
+
|
|
21
|
+
## Slots (props)
|
|
22
|
+
|
|
23
|
+
| Slot | O que vai aqui |
|
|
24
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `viewToggle` | Toggle Kanban/Lista (`<ToolbarSegmented>`) |
|
|
26
|
+
| `savedViews` | Abas de visões + adicionar (`<TableToolbarViews>` + `<ViewsPopover>`). `<TableToolbarViews allowCreate={false}>` (v0.23.0) esconde o "+" e o modal de nova visão → só as visões pré-definidas (read-only). No DataTable use a prop `allowCreateView`. **`maxTabs`** (default `3`, **incluindo a aba "Default"**) limita quantas viram aba — o excedente é cortado por `.slice()`, sem overflow — mas em DEV sai um `console.warn` nomeando as visões engolidas e o valor que resolve (v0.43.1); no DataTable a prop equivalente é `maxViewTabs`. |
|
|
27
|
+
| `refresh` | Botão atualizar (`<ToolbarToolButton>`) |
|
|
28
|
+
| `search` | Campo de busca (`<ToolbarSearch>`) |
|
|
29
|
+
| `filter` | Filtro simples: use `<ToolbarFilterButton onClick isActive hasIndicator />` (funil, icon-only — **mesmo botão** no DataTable e DataList) → abre `<ToolbarSimpleFilterDrawer>` |
|
|
30
|
+
| `actions` | Ações custom: `<ToolbarActions actions={[...]} />` (`button`/`dropdown`/`input`). Inline no desktop; **colapsam no ⋯ no mobile** (passe `extraItems` p/ absorver o ⋯ existente) |
|
|
31
|
+
| `settings` | Configurações: sliders → drill-down (`<ToolbarSettingsMenu>` com Ordenação · Colunas · Filtros avançados · Densidade) |
|
|
32
|
+
| `more` | Menu "⋯" (`<MoreMenu>`) — export + ações |
|
|
33
|
+
| `bulkBar` | Substitui a toolbar inteira pela barra de ações em massa |
|
|
34
|
+
| `className` | — |
|
|
35
|
+
|
|
36
|
+
## Exemplo mínimo
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import {
|
|
40
|
+
TableToolbar,
|
|
41
|
+
ToolbarSearch,
|
|
42
|
+
ToolbarToolButton,
|
|
43
|
+
ToolbarSettingsMenu,
|
|
44
|
+
ToolbarSegmented,
|
|
45
|
+
ToolbarApplied,
|
|
46
|
+
MoreMenu,
|
|
47
|
+
SortPanel,
|
|
48
|
+
ColsPanel,
|
|
49
|
+
FilterPanel,
|
|
50
|
+
} from "@/components/ui/TableToolbar";
|
|
51
|
+
|
|
52
|
+
<TableToolbar
|
|
53
|
+
search={<ToolbarSearch value={q} onChange={(e) => setQ(e.target.value)} />}
|
|
54
|
+
settings={
|
|
55
|
+
<ToolbarSettingsMenu
|
|
56
|
+
trigger={<ToolbarToolButton icon={<SlidersHorizontal />} aria-label="Configurações da tabela" />}
|
|
57
|
+
sortPanel={(onBack) => <SortPanel ... onBack={onBack} />}
|
|
58
|
+
colsPanel={(onBack) => <ColsPanel ... onBack={onBack} />}
|
|
59
|
+
filterPanel={(onBack) => <FilterPanel ... onBack={onBack} />}
|
|
60
|
+
density={<ToolbarSegmented items={DENSITY} ... />}
|
|
61
|
+
/>
|
|
62
|
+
}
|
|
63
|
+
more={<MoreMenu ... />}
|
|
64
|
+
/>
|
|
65
|
+
{/* Chips dos filtros aplicados LOGO ABAIXO */}
|
|
66
|
+
<ToolbarApplied filters={appliedFilters} onRemove={...} />
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Gotchas
|
|
70
|
+
|
|
71
|
+
- **Ordem é fixa** — não dá (nem precisa) reordenar. Cada controle tem seu slot semântico.
|
|
72
|
+
- **Mobile**: o `refresh` é escondido em `< md` via `hidden md:contents`; o
|
|
73
|
+
`<ToolbarSettingsMenu>` expõe Visualização/Visões inline dentro do menu nesse breakpoint.
|
|
74
|
+
- **`mobileDisplayToggle?: ReactNode`** (ToolbarSettingsMenu): slot pro toggle
|
|
75
|
+
**"Exibição" (Linhas/Cards)** — só renderiza em `< md`. O DataTable passa um
|
|
76
|
+
`<ToolbarSegmented>` aqui quando a tela cabe em card mode; permite o usuário
|
|
77
|
+
forçar tabela mesmo abaixo do `cardBreakpoint` (default mobile = **tabela**).
|
|
78
|
+
- **bulkBar substitui tudo**: passe `<BulkActionsBar>` (auto-some quando `count=0`)
|
|
79
|
+
condicionalmente — `selectedIds.size > 0 ? <BulkActionsBar/> : undefined`.
|
|
80
|
+
- **Chips ficam fora** da toolbar: renderize `<ToolbarApplied>` como irmão abaixo.
|
|
81
|
+
|
|
82
|
+
## Acoplamento DataTable ↔ TableToolbar
|
|
83
|
+
|
|
84
|
+
O DataTable consome os parts do TableToolbar via composição e orquestra os slots
|
|
85
|
+
na ordem fixa. Mudar a ordem/conteúdo dos controles do DataTable é edição **no
|
|
86
|
+
DataTable**, não aqui. Coupling reverso (DataTable → TableToolbar para
|
|
87
|
+
`columnTypeRegistry`/`FilterModel`) é aceito; o inverso é proibido.
|