@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,129 @@
|
|
|
1
|
+
# PageHeader — USAGE
|
|
2
|
+
|
|
3
|
+
Bloco de título reutilizável renderizado dentro do body do `<AppShell>`. Cobre o pattern comum de "header de página" do CRM iGreen: **título + descrição + badge + actions**, com slot opcional pra conteúdo extra abaixo (tabs, filtros).
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
- Páginas listagem (CRUD, dashboards) que precisam de título + CTA principal
|
|
7
|
+
- Páginas com tabs/filtros abaixo do header
|
|
8
|
+
- Qualquer tela que reaproveite a estrutura "title/description/badge + actions"
|
|
9
|
+
|
|
10
|
+
Quando **não** usar:
|
|
11
|
+
- Telas chat-like (ChatV2/ChatShowcase) — o header global do AppShell já basta
|
|
12
|
+
- Detail pages onde o header é parte de um card/drawer (use composição direta)
|
|
13
|
+
|
|
14
|
+
## Import
|
|
15
|
+
```tsx
|
|
16
|
+
import { PageHeader } from "@/components/ui/PageHeader";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Props essenciais
|
|
20
|
+
|
|
21
|
+
| Prop | Tipo | Default | Função |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| `title` | string | — | Título h1 (text-title-lg) |
|
|
24
|
+
| `description` | string | — | Sub-linha (text-body-md fg-subtle) |
|
|
25
|
+
| `badge` | ReactNode | — | Inline ao lado do title (geralmente `<Chip>`) |
|
|
26
|
+
| `actions` | ReactNode | — | Bloco de ações à direita (Buttons, dropdowns) |
|
|
27
|
+
| `children` | ReactNode | — | Conteúdo extra abaixo (tabs, filtros) |
|
|
28
|
+
| `hideTextOnMobile` | boolean | `true` | Em mobile (<md), esconde title/description/badge |
|
|
29
|
+
| `fluidPrimaryOnMobile` | boolean | `true` | Em mobile, último filho do `actions` vira fluid (full-width CTA) |
|
|
30
|
+
| `className` | string | — | className extra no `<header>` root |
|
|
31
|
+
|
|
32
|
+
## Exemplos
|
|
33
|
+
|
|
34
|
+
### Mínimo (só título)
|
|
35
|
+
```tsx
|
|
36
|
+
<PageHeader title="Configurações" />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Pattern CRUD (replica o header da página de Clientes)
|
|
40
|
+
```tsx
|
|
41
|
+
<PageHeader
|
|
42
|
+
title="Clientes"
|
|
43
|
+
description="Gerencie sua base de clientes, acompanhe status e abra atendimentos pelo WhatsApp."
|
|
44
|
+
badge={
|
|
45
|
+
<Chip color="primary" variant="soft" size="sm" shape="rounded">
|
|
46
|
+
87 registros
|
|
47
|
+
</Chip>
|
|
48
|
+
}
|
|
49
|
+
actions={
|
|
50
|
+
<>
|
|
51
|
+
<Button variant="outline" color="secondary" size="icon-md" aria-label="Mais ações">
|
|
52
|
+
<MoreHorizontal />
|
|
53
|
+
</Button>
|
|
54
|
+
<Button variant="filled" color="primary" iconLeft={<Plus />}>
|
|
55
|
+
Novo cliente
|
|
56
|
+
</Button>
|
|
57
|
+
</>
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Com tabs abaixo do header
|
|
63
|
+
```tsx
|
|
64
|
+
<PageHeader
|
|
65
|
+
title="Relatórios"
|
|
66
|
+
description="Análise consolidada de vendas e leads."
|
|
67
|
+
actions={<Button>Exportar</Button>}
|
|
68
|
+
>
|
|
69
|
+
<Tabs value={tab} onValueChange={setTab}>
|
|
70
|
+
<TabsList>
|
|
71
|
+
<TabsTrigger value="day">Hoje</TabsTrigger>
|
|
72
|
+
<TabsTrigger value="week">Semana</TabsTrigger>
|
|
73
|
+
<TabsTrigger value="month">Mês</TabsTrigger>
|
|
74
|
+
</TabsList>
|
|
75
|
+
</Tabs>
|
|
76
|
+
</PageHeader>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Mantendo título visível no mobile
|
|
80
|
+
```tsx
|
|
81
|
+
<PageHeader
|
|
82
|
+
title="Termos de uso"
|
|
83
|
+
description="Última atualização: jan/2026"
|
|
84
|
+
hideTextOnMobile={false}
|
|
85
|
+
/>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Sem actions, só conteúdo
|
|
89
|
+
```tsx
|
|
90
|
+
<PageHeader title="FAQ">
|
|
91
|
+
<SearchBar />
|
|
92
|
+
</PageHeader>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Cuidados / Gotchas
|
|
96
|
+
|
|
97
|
+
- **`title` é renderizado como `<h1>`** — use **apenas um** `<PageHeader>` com title por página (semântica HTML).
|
|
98
|
+
- **Mobile fluid** assume que o último filho do `actions` é o CTA primary. Se a ordem for diferente, desligue via `fluidPrimaryOnMobile={false}` e controle manualmente.
|
|
99
|
+
- **`description` truncate em 1 linha** (`whitespace-nowrap overflow-hidden text-ellipsis`) — pra descrições longas, considere usar o slot `children` em vez disso.
|
|
100
|
+
- **Body do AppShell** já aplica `gap-gp-4xl` entre filhos diretos. Não envolva o `<PageHeader>` num wrapper extra — coloque direto como filho do `<AppShell>`.
|
|
101
|
+
|
|
102
|
+
## Anatomia
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
<PageHeader> (header.flex-col.gap-gp-md)
|
|
106
|
+
├─ topRow (flex.justify-between.gap-gp-2xl)
|
|
107
|
+
│ ├─ textCol [optional] (flex-col.gap-gp-xs.flex-1)
|
|
108
|
+
│ │ ├─ titleRow (flex.gap-gp-md)
|
|
109
|
+
│ │ │ ├─ <h1 title>
|
|
110
|
+
│ │ │ └─ {badge}
|
|
111
|
+
│ │ └─ <p description>
|
|
112
|
+
│ └─ actionsRow [optional] (flex.gap-gp-sm)
|
|
113
|
+
│ └─ {actions}
|
|
114
|
+
└─ extraRow [optional — children] (w-full)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Composição manual
|
|
118
|
+
|
|
119
|
+
Pra layouts que fogem do pattern (ex: title centralizado, ações em coluna), use os slots de styles direto:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { pageHeaderStyles } from "@/components/ui/PageHeader";
|
|
123
|
+
|
|
124
|
+
const s = pageHeaderStyles();
|
|
125
|
+
|
|
126
|
+
<header className={s.root()}>
|
|
127
|
+
{/* sua estrutura custom */}
|
|
128
|
+
</header>
|
|
129
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Panel — USAGE
|
|
2
|
+
|
|
3
|
+
<!-- ds:regras
|
|
4
|
+
- painel de DETALHE → siga o bloco `dsgreen-paneldetail-1` (o **padrão**). `-2` (tarefa com abas) e `-3` (com tabela) só se o usuário citar o ID
|
|
5
|
+
- header aqui é só STRING (`title`/`description`): precisa de avatar, Chip de status ou ação de ícone? → `FloatingPanel`
|
|
6
|
+
- form dentro dele → `<FormField>` + `gap-form-gap`, nunca `<label>` cru
|
|
7
|
+
- aba dentro dele → `<Tabs fullWidth>` na variante default (560px é estreito pro `line`)
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
Drawer flutuante lateral (right/left/top/bottom) — default `md` (560px) com header + body scrollável + footer sticky.
|
|
11
|
+
|
|
12
|
+
## Quando usar
|
|
13
|
+
- Detail/edit panel que precisa de mais espaço que `<Modal>`
|
|
14
|
+
- Forms longos com seções (preferível a Modal large)
|
|
15
|
+
- Quando user precisa ver conteúdo ao lado enquanto interage
|
|
16
|
+
|
|
17
|
+
## Import
|
|
18
|
+
```tsx
|
|
19
|
+
import { Panel } from "@/components/ui/Panel";
|
|
20
|
+
|
|
21
|
+
// Composição manual (avançado): PanelRoot, PanelTrigger, PanelContent,
|
|
22
|
+
// PanelHeader, PanelBody, PanelFooter, PanelCloseAction
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Variants
|
|
26
|
+
| Variant | Valores | Default | Tamanho |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| `side` | right / left / top / bottom | right | right/left: largura via `size`, altura full (gutter 24px); top/bottom: largura via inset, altura conforme conteúdo |
|
|
29
|
+
| `size` | sm / md / lg / xl / full \| string CSS | md | 320 / 560 / 720 / 920px / full (viewport − gutter). Aceita CSS arbitrário: `"720px"`, `"60vw"` |
|
|
30
|
+
|
|
31
|
+
## Props essenciais
|
|
32
|
+
| Prop | Tipo | Função |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `open` | boolean | Visibilidade (controlled) |
|
|
35
|
+
| `onOpenChange` | (open: boolean) => void | Callback de abertura/fechamento (o X do header fecha por aqui) |
|
|
36
|
+
| `trigger` | ReactNode | Elemento que abre o panel sem state externo (vira `<SheetTrigger asChild>`) |
|
|
37
|
+
| `side` | "right" \| "left" \| "top" \| "bottom" | Lado de ancoragem (default: "right") |
|
|
38
|
+
| `size` | "sm" \| "md" \| "lg" \| "xl" \| "full" \| string | Largura (default: "md" = 560px) |
|
|
39
|
+
| `title` | string | Header title |
|
|
40
|
+
| `description` | string | Header subtitle |
|
|
41
|
+
| `titleIcon` | LucideIcon | Ícone à esquerda do título (cor brand) |
|
|
42
|
+
| `footer` | ReactNode | Footer sticky — geralmente botões de ação (único caminho pro footer no modo all-in-one) |
|
|
43
|
+
|
|
44
|
+
## Exemplo mínimo
|
|
45
|
+
```tsx
|
|
46
|
+
<Panel
|
|
47
|
+
open={panelOpen}
|
|
48
|
+
onOpenChange={setPanelOpen}
|
|
49
|
+
side="right"
|
|
50
|
+
title="Editar cliente"
|
|
51
|
+
description="Salvo automaticamente"
|
|
52
|
+
footer={
|
|
53
|
+
<>
|
|
54
|
+
<Button variant="ghost" onClick={() => setPanelOpen(false)}>Cancelar</Button>
|
|
55
|
+
<Button onClick={handleSave}>Salvar</Button>
|
|
56
|
+
</>
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<ClientForm />
|
|
60
|
+
</Panel>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Painel de DETALHE de registro → o bloco `dsgreen-paneldetail-1`
|
|
64
|
+
|
|
65
|
+
Detalhe de registro tem estrutura definida e referenciável por ID: a composição inteira mora
|
|
66
|
+
em `src/blocks/paneldetail/` e renderiza em `#/blocks-paneldetail`. Cite o ID em vez de
|
|
67
|
+
recompor — `use a referência dsgreen-paneldetail-1 no painel de detalhe do pedido`. As zonas
|
|
68
|
+
(identidade no header · métricas em cards · seções colapsáveis · ação primária no footer) e o
|
|
69
|
+
porquê de cada uma estão documentados no arquivo do bloco.
|
|
70
|
+
|
|
71
|
+
⚠️ **O bloco usa `FloatingPanel`, e a razão é este componente aqui:** o header do `Panel`
|
|
72
|
+
aceita `title`/`description` como **string**, então avatar, `Chip` de status inline e ação de
|
|
73
|
+
ícone não cabem nele. Precisa desse header → `FloatingPanel` (`titleSlot` + `headerActions`).
|
|
74
|
+
Escolha o `Panel` quando o que importa é **bloquear** a tela atrás e o header é texto simples.
|
|
75
|
+
|
|
76
|
+
## Cuidados / Gotchas
|
|
77
|
+
- **Aba dentro do Panel** → `<Tabs fullWidth>` com a variante **default** (`segmented`). O Panel tem 560px: `line` aqui vira um trilho curto que lê como fragmento, e sem `fullWidth` as abas ficam num canto. `line` só se houver um `segmented` num nível acima.
|
|
78
|
+
|
|
79
|
+
- **Children são auto-embrulhados em `<PanelBody>`** — NÃO passar `<PanelBody>`/`<PanelFooter>` como filhos do `<Panel>` (duplica o body e o footer perde o sticky). Footer vai SEMPRE via prop `footer`. Pra composição manual, usar `PanelRoot`/`PanelTrigger`/`PanelContent` do barrel
|
|
80
|
+
- **Modal** (com backdrop suave / `SheetOverlay`) — bloqueia interação com página atrás. Pra non-modal, usar `<FloatingPanel>`
|
|
81
|
+
- **Mobile (<md)**: independente do `side`, vira **sheet bottom-up colado nas bordas** do device — flush nas laterais + bottom, só cantos superiores arredondados, sem outline/shadow, `max-height: 92vh`, slide bottom-up. O backdrop (modal) continua
|
|
82
|
+
- Body é único elemento scrollável (`overflow-y-auto` + `min-h-0`) — header e footer ficam fixos; conteúdo longo rola automaticamente
|
|
83
|
+
- **Footer fluido**: botões crescem lado a lado e **empilham quando não cabem** (`flex-wrap` + `flex-1` + `min-w-140px`). Não precisa passar `fullWidth` nos Buttons
|
|
84
|
+
- No desktop o `side` controla orientação/slide normalmente (right/left/top/bottom)
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# Scheduler
|
|
2
|
+
|
|
3
|
+
**Categoria:** Data Display · **Dep:** `date-fns`
|
|
4
|
+
|
|
5
|
+
Calendário de **eventos ao longo do tempo**, com 4 modos de visualização
|
|
6
|
+
(mês · semana · dia · lista), toolbar embutida e detalhe por callback.
|
|
7
|
+
|
|
8
|
+
A toolbar traz: título do período + grupo `‹ Hoje ›` à esquerda; e à direita
|
|
9
|
+
busca, área custom, **seletor de view** (dropdown), **Filtro** e ação primária —
|
|
10
|
+
nessa ordem, do recorte mais amplo (qual período) pro mais específico (o que
|
|
11
|
+
dentro dele).
|
|
12
|
+
|
|
13
|
+
**O botão Filtro carrega dois sinais independentes:**
|
|
14
|
+
|
|
15
|
+
- **Verde** (`primary soft` + `border-border-brand` → brand a **14%** de
|
|
16
|
+
opacidade, texto e borda em brand) = a ferramenta está **engajada**: painel
|
|
17
|
+
aberto **ou** filtro aplicado.
|
|
18
|
+
- **Ponto** no canto = existe **filtro aplicado**, e só isso.
|
|
19
|
+
|
|
20
|
+
| Situação | Verde? | Ponto? |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| nada aberto, nada filtrado | não | não |
|
|
23
|
+
| painel aberto, sem filtro | **sim** | não |
|
|
24
|
+
| filtro aplicado, painel fechado | **sim** | **sim** |
|
|
25
|
+
| filtro aplicado, painel aberto | **sim** | **sim** |
|
|
26
|
+
|
|
27
|
+
A cor é a mesma receita do `ToolbarToolButton` do `TableToolbar` — o precedente
|
|
28
|
+
do DS pra "esta ferramenta tem algo ligado". O ponto **não** é redundante com
|
|
29
|
+
ela: é o que separa "abri pra olhar" de "tem filtro mexendo no que eu vejo", e é
|
|
30
|
+
o portador não-cromático da informação pra quem não distingue o verde.
|
|
31
|
+
|
|
32
|
+
O botão Filtro abre um **painel-coluna à direita da grade** — não um overlay.
|
|
33
|
+
|
|
34
|
+
> ⚠️ **Não confunda com `Calendar`.** `Calendar` é o primitivo
|
|
35
|
+
> shadcn/react-day-picker: **seletor de data** dentro de um form. O `Scheduler`
|
|
36
|
+
> exibe **eventos**. As intenções são opostas, por isso os nomes são distintos.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quando usar
|
|
41
|
+
|
|
42
|
+
| Precisa de | Use |
|
|
43
|
+
|---|---|
|
|
44
|
+
| escolher uma data num campo | `Calendar` / `DatePicker` |
|
|
45
|
+
| escolher mês+ano | `MonthYearPicker` |
|
|
46
|
+
| **ver compromissos numa grade de tempo** | **`Scheduler`** |
|
|
47
|
+
| grade de registros com colunas | `DataTable` |
|
|
48
|
+
| lista de cards | `DataList` |
|
|
49
|
+
| quadro por estágio de workflow | `Kanban` |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Estado atual — o que está e o que NÃO está pronto
|
|
54
|
+
|
|
55
|
+
Leia antes de planejar uma tela em cima disto:
|
|
56
|
+
|
|
57
|
+
| | Status |
|
|
58
|
+
|---|---|
|
|
59
|
+
| view `month` | ✅ grade 6×7, multi-dia com pontas truncadas, `+N mais` em popover, `+` de criar no hover |
|
|
60
|
+
| views `week` / `day` | ✅ a MESMA view (`views/time-grid.tsx`) com 7 ou 1 coluna — gutter de horas, banda de dia inteiro, lane-packing, linha do "agora", faixa de hora clicável |
|
|
61
|
+
| view `list` | ✅ agenda agrupada por dia, **só os dias que têm evento** |
|
|
62
|
+
| drag & drop | ✅ **por ponteiro** — mover no mês (muda a data, preserva hora e duração), mover em week/day (coluna + minutos snapados), **redimensionar** pela borda do bloco em week/day |
|
|
63
|
+
| navegação por teclado | ✅ roving tabindex — cada grade é **uma** parada de `Tab`, setas movem dentro, `Home`/`End` vão às pontas da linha, `Enter` cria no slot focado. ⛔ **Arrastar por teclado não existe** — ver o gotcha do dnd |
|
|
64
|
+
|
|
65
|
+
O núcleo é puro e testado: `hooks/layout.test.ts` (51 casos de borda) mais
|
|
66
|
+
`hooks/use-scheduler-dnd.test.ts` (19 casos da resolução do drop).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Props essenciais
|
|
71
|
+
|
|
72
|
+
| Prop | Tipo | Default | Nota |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `events` | `SchedulerEvent[]` | — | nunca é mutado pelo componente |
|
|
75
|
+
| `view` / `defaultView` | `"month" \| "week" \| "day" \| "list"` | `"month"` | controlado / não-controlado |
|
|
76
|
+
| `date` / `defaultDate` | `Date` | hoje | idem |
|
|
77
|
+
| `onDateChange` · `onViewChange` | `(v) => void` | — | **obrigatórios no modo controlado** |
|
|
78
|
+
| `locale` | `Locale` (date-fns) | en-US | passe `ptBR` |
|
|
79
|
+
| `weekStartsOn` | `0`–`6` | `0` (domingo) | |
|
|
80
|
+
| `hourFormat` | `"12h" \| "24h"` | `"24h"` | |
|
|
81
|
+
| `onEventClick` | `(event, evt) => void` | — | é assim que o detalhe abre |
|
|
82
|
+
| `onSlotClick` | `(start, end) => void` | — | o `+` revelado no hover da célula |
|
|
83
|
+
| `searchable` | `boolean` | `true` | |
|
|
84
|
+
| `filterFields` | `SchedulerFilterField[]` | — | declarativo; o componente filtra sozinho |
|
|
85
|
+
| `filterModel` | `Record<string, string[]>` | — | controlado / **pré-aplicado** |
|
|
86
|
+
| `filterMode` | `"client" \| "server"` | `"client"` | `server` só emite |
|
|
87
|
+
| `toolbarActions` | `ReactNode` | — | área custom, entre o filtro e o seletor de view |
|
|
88
|
+
| `defaultFilterPanelOpen` | `boolean` | `false` | o painel-coluna de filtro já vem aberto |
|
|
89
|
+
| `primaryAction` | `ReactNode` | — | botão primário à direita |
|
|
90
|
+
| `renderEvent` | `({event, view, selected}) => ReactNode` | — | troca só o miolo |
|
|
91
|
+
|
|
92
|
+
### `SchedulerEvent`
|
|
93
|
+
|
|
94
|
+
| Campo | Tipo | Nota |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `id` | `string` | |
|
|
97
|
+
| `title` | `ReactNode` | |
|
|
98
|
+
| `start` · `end` | `Date` | **`end` é obrigatório**; duração zero se diz `end === start` |
|
|
99
|
+
| `allDay` | `boolean` | |
|
|
100
|
+
| `color` | `"brand" \| "info" \| "success" \| "warning" \| "danger" \| "neutral"` | default `"brand"` |
|
|
101
|
+
| `categoryId` · `tagIds` | `string` · `string[]` | filtráveis |
|
|
102
|
+
| `description` | `ReactNode` | aparece na view `list` |
|
|
103
|
+
| `searchText` | `string` | **necessário quando `title` não é string** |
|
|
104
|
+
| `meta` | `unknown` | payload cru, devolvido em `onEventClick` |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Exemplo mínimo
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { ptBR } from "date-fns/locale";
|
|
112
|
+
import { Scheduler } from "@/components/ui/Scheduler";
|
|
113
|
+
|
|
114
|
+
<div className="h-[720px]">
|
|
115
|
+
<Scheduler
|
|
116
|
+
events={eventos}
|
|
117
|
+
locale={ptBR}
|
|
118
|
+
filterFields={[
|
|
119
|
+
{ id: "categoryId", label: "Categoria", options: [
|
|
120
|
+
{ value: "interna", label: "Interna" },
|
|
121
|
+
{ value: "cliente", label: "Cliente" },
|
|
122
|
+
]},
|
|
123
|
+
]}
|
|
124
|
+
onEventClick={(evento) => setSelecionado(evento)}
|
|
125
|
+
primaryAction={<Button variant="filled" size="sm">Novo evento</Button>}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Gotchas
|
|
133
|
+
|
|
134
|
+
### 1. O pai precisa ter altura — e é a altura dele que dimensiona tudo
|
|
135
|
+
|
|
136
|
+
A grade é `flex-1` dentro de um `flex-col`. Num pai sem altura definida ela
|
|
137
|
+
colapsa pra altura mínima das células. Embrulhe em algo com altura
|
|
138
|
+
(`h-[720px]`, ou `flex-1 min-h-0` numa página que já tem altura).
|
|
139
|
+
|
|
140
|
+
**Não existe prop de "tela cheia".** O componente já é `h-full`: pra usá-lo como
|
|
141
|
+
página inteira, dê `h-screen` ao container e deixe o `Scheduler` em `flex-1`.
|
|
142
|
+
|
|
143
|
+
E isso não é só esticar — **a célula do mês passa a caber mais eventos**. O corte
|
|
144
|
+
do `+N mais` é derivado da altura MEDIDA da linha, como as linhas da tabela.
|
|
145
|
+
Medido no browser, mesmo dataset:
|
|
146
|
+
|
|
147
|
+
| viewport | altura da linha | pills por célula | células com "+N mais" |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| 800px | 76px | **2** | 13 |
|
|
150
|
+
| 1400px | 176px | **6** | 2 |
|
|
151
|
+
|
|
152
|
+
Pra travar num número fixo (quando a tela precisa de altura de linha previsível
|
|
153
|
+
independentemente do conteúdo), a view de mês aceita `maxPerCell` — mas o
|
|
154
|
+
default adaptativo é o que aproveita a tela.
|
|
155
|
+
|
|
156
|
+
Exemplo vivo: **Scheduler Full Screen**, na categoria Examples do showcase
|
|
157
|
+
(`#/scheduler-full`).
|
|
158
|
+
|
|
159
|
+
> ⚠️ A re-medição pós-montagem depende de `ResizeObserver`. Ela **não pôde ser
|
|
160
|
+
> verificada** no browser de teste: a emulação de viewport por CDP não dispara
|
|
161
|
+
> `ResizeObserver`, nem `window.resize`, nem `MediaQueryList.change` — medido,
|
|
162
|
+
> zero callbacks nos três. O que foi verificado é a montagem correta em cada
|
|
163
|
+
> altura (a tabela acima veio de reloads reais). Em browser de verdade o
|
|
164
|
+
> `ResizeObserver` é o mesmo mecanismo que o resto do DS usa.
|
|
165
|
+
|
|
166
|
+
### 2. A cor **nunca** vai no texto — e nunca é o único portador da informação
|
|
167
|
+
|
|
168
|
+
Medido em WCAG antes de decidir: `text-fg-{cor}` sobre a pílula tingida dá
|
|
169
|
+
**1.72–4.49** no claro e **2.97–4.31** no escuro. Nenhuma das 6 famílias passa
|
|
170
|
+
AA, e `warning` chega a 1.72:1.
|
|
171
|
+
|
|
172
|
+
Por isso o texto é sempre `fg-default` (medido no browser: **16.2–18.2** no
|
|
173
|
+
claro, **16.4–17.8** no escuro — AAA nos dois modos) e a cor mora no **dot, na
|
|
174
|
+
borda e no tint de fundo**.
|
|
175
|
+
|
|
176
|
+
Corolário pra quem monta a tela: **cor é reforço**. O título tem que dizer o
|
|
177
|
+
que o evento é — quem não distingue as cores precisa continuar entendendo a
|
|
178
|
+
agenda.
|
|
179
|
+
|
|
180
|
+
### 3. O teto real é ~5 categorias, não 6
|
|
181
|
+
|
|
182
|
+
`brand` (matiz 151) e `success` (161) estão a **10°** de distância. Como tint a
|
|
183
|
+
14% os dois viram praticamente o mesmo off-white — **não use os dois como
|
|
184
|
+
categorias distintas no mesmo calendário**.
|
|
185
|
+
|
|
186
|
+
`info` é matiz 280, ou seja **violeta**: se você precisa de "roxo", ele já
|
|
187
|
+
existe.
|
|
188
|
+
|
|
189
|
+
Precisa de 6+ categorias simultâneas e distinguíveis? Isso é o gatilho pra
|
|
190
|
+
abrir uma paleta categórica própria (`event-*`) via cascata de token — não
|
|
191
|
+
force a união atual.
|
|
192
|
+
|
|
193
|
+
### 4. Busca não lê `title` rico
|
|
194
|
+
|
|
195
|
+
O motor casa **só string**: `searchText`, `title` (se for string),
|
|
196
|
+
`description` (idem) e `tagIds`. Varrer a árvore de um `ReactNode` é caro e
|
|
197
|
+
frágil, então não é feito.
|
|
198
|
+
|
|
199
|
+
Consequência: `title={<b>Reunião</b> — Cliente X}` **não é encontrável** —
|
|
200
|
+
falha em silêncio. Declare `searchText` nesses casos.
|
|
201
|
+
|
|
202
|
+
### 4b. O painel de filtro é uma COLUNA, e some abaixo de 1024px
|
|
203
|
+
|
|
204
|
+
O botão **Filtro** não abre popover nem sheet: abre uma coluna à direita que
|
|
205
|
+
**empurra** a grade (~296px). É deliberado — filtro é o controle cujo resultado
|
|
206
|
+
você quer ver *enquanto* mexe, e por cima da grade isso viraria
|
|
207
|
+
marcar → fechar → olhar → reabrir.
|
|
208
|
+
|
|
209
|
+
Três consequências práticas:
|
|
210
|
+
|
|
211
|
+
- **A grade encolhe.** Se a sua tela é estreita, prefira deixar
|
|
212
|
+
`defaultFilterPanelOpen` em `false` (o default).
|
|
213
|
+
- **O cabeçalho do painel é `sticky` e a barra é a do DS.** O bloco "Filtros /
|
|
214
|
+
Limpar / ×" fica fixo no topo enquanto o resto rola, com `bg-bg-surface`
|
|
215
|
+
opaco — `sticky` não cria fundo, e sem ele o mini-calendário passaria por
|
|
216
|
+
baixo do título. A rolagem usa `scrollbar-thin`, a `@utility` do tema gerado
|
|
217
|
+
(trilho transparente + thumb tokenizado), que é o padrão do repo com 26 usos.
|
|
218
|
+
- **O painel nunca estica a altura da linha.** Ele acompanha a altura da grade e
|
|
219
|
+
**rola por dentro** (medido: 664px de caixa com 958px de conteúdo). É por isso
|
|
220
|
+
que o root é `h-full` — sem isso ele dimensionava por conteúdo, o painel ficava
|
|
221
|
+
mais alto que a grade e passava a mandar na altura das duas, vazando pra fora
|
|
222
|
+
do container. Se você vir o calendário cortado, o pai é que está sem altura
|
|
223
|
+
(gotcha nº 1).
|
|
224
|
+
- **Abaixo de 1024px o painel vira DRAWER**, não some. A coluna não cabe junto
|
|
225
|
+
de uma semana legível, então nessa faixa o mesmo painel abre num
|
|
226
|
+
`FloatingPanel side="right"` por cima — o veículo que `DataTable` e
|
|
227
|
+
`DataList` já usam pro filtro deles (`ToolbarSimpleFilterDrawer`). Mesmo
|
|
228
|
+
conteúdo, mesmo estado, mesmo botão: só o invólucro muda.
|
|
229
|
+
|
|
230
|
+
No drawer, escolher uma data no mini-calendário **fecha** o painel — ele
|
|
231
|
+
cobre a grade, e manter aberto esconderia justamente o resultado. Como
|
|
232
|
+
coluna isso não acontece.
|
|
233
|
+
|
|
234
|
+
⚠️ **O breakpoint vive em UM lugar só:** `useMediaQuery("(min-width: 1024px)")`
|
|
235
|
+
em `scheduler.tsx`. Não acrescente media query no `schedulerFilterAside`.
|
|
236
|
+
|
|
237
|
+
> Até 2026-09-01 este parágrafo mandava mudar **dois** lugares — o `lg:flex` do
|
|
238
|
+
> style *mais* o hook. O `lg:flex` foi removido justamente porque a duplicação
|
|
239
|
+
> produzia um estado em que o botão se dizia aberto e o painel estava
|
|
240
|
+
> `display: none`; o `scheduler.tsx` diz isso em maiúsculas desde então. A doc
|
|
241
|
+
> continuou mandando reintroduzir o defeito — L-060: comentário é load-bearing.
|
|
242
|
+
|
|
243
|
+
### 4c. "Nada marcado" = sem filtro, não "esconde tudo"
|
|
244
|
+
|
|
245
|
+
Num app de calendário de verdade, desmarcar todas as agendas esconde tudo.
|
|
246
|
+
**Aqui não**: campo sem opção marcada significa *sem restrição* — a convenção do
|
|
247
|
+
`filterModel` do DS, a mesma do `DataTable` e do `DataList`. O cabeçalho do
|
|
248
|
+
grupo mostra **"Todas"** nesse estado, pra não parecer que a seleção se perdeu.
|
|
249
|
+
|
|
250
|
+
Inverter isso só neste componente faria o mesmo `filterModel` significar coisas
|
|
251
|
+
opostas em telas diferentes.
|
|
252
|
+
|
|
253
|
+
### 4d. A contagem ao lado da opção ignora os filtros de campo
|
|
254
|
+
|
|
255
|
+
O número é "quantos eventos casam com a **busca** e têm esta opção" — **não**
|
|
256
|
+
"quantos vou ver se marcar".
|
|
257
|
+
|
|
258
|
+
A base exclui de propósito todos os filtros de campo, inclusive o do próprio
|
|
259
|
+
campo, pra que os números fiquem **estáveis enquanto você marca e desmarca**.
|
|
260
|
+
Com a facet count clássica (base = os outros campos já aplicados), cada clique
|
|
261
|
+
mexeria nos números das outras listas, e a leitura vira alvo móvel exatamente na
|
|
262
|
+
hora de decidir. As duas leituras são defensáveis; confundi-las é que engana.
|
|
263
|
+
|
|
264
|
+
### 5. `filterFields` no modo `client` só filtra 3 ids
|
|
265
|
+
|
|
266
|
+
`categoryId`, `tagIds` e `color`. Qualquer outro `id` **renderiza o chip e não
|
|
267
|
+
filtra nada** — o componente emite `console.warn` em DEV nomeando o campo.
|
|
268
|
+
Para filtrar por campo próprio, use `filterMode="server"` e filtre fora.
|
|
269
|
+
|
|
270
|
+
### 5b. Drag & drop: você PRECISA aplicar a mudança
|
|
271
|
+
|
|
272
|
+
O componente é dumb sobre mutação — ele emite e para:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
const [eventos, setEventos] = useState(EVENTOS);
|
|
276
|
+
const aplicar = ({ id, start, end }) =>
|
|
277
|
+
setEventos((a) => a.map((e) => (e.id === id ? { ...e, start, end } : e)));
|
|
278
|
+
|
|
279
|
+
<Scheduler
|
|
280
|
+
events={eventos}
|
|
281
|
+
draggable
|
|
282
|
+
resizable
|
|
283
|
+
onEventMove={aplicar}
|
|
284
|
+
onEventResize={aplicar}
|
|
285
|
+
/>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
O que cada gesto significa:
|
|
289
|
+
|
|
290
|
+
| gesto | resultado |
|
|
291
|
+
|---|---|
|
|
292
|
+
| arrastar no **mês** | muda a **data**; hora e duração preservadas |
|
|
293
|
+
| arrastar em **week/day** | combina coluna (dia) + `delta.y` (minutos, snapado por `snapMinutes`) |
|
|
294
|
+
| arrastar a **borda** do bloco (week/day) | muda a **duração**, uma ponta só |
|
|
295
|
+
|
|
296
|
+
Detalhes que evitam surpresa:
|
|
297
|
+
|
|
298
|
+
- **A duração nunca é snapada separadamente.** Um evento de 50min movido continua
|
|
299
|
+
com 50min — snapar as duas pontas o encurtaria pra 45 a cada arrasto.
|
|
300
|
+
- **Resize não existe no mês** (a altura da pílula não representa duração) nem em
|
|
301
|
+
evento `allDay`.
|
|
302
|
+
- **All-day em week/day ignora o `delta.y`** e só troca de dia: ele mora na banda,
|
|
303
|
+
não na grade de horas.
|
|
304
|
+
- **Arrastar pra baixo perto do fim da grade encosta no limite** em vez de vazar
|
|
305
|
+
pro dia seguinte. Mudar de dia se faz atravessando a coluna — o gesto explícito.
|
|
306
|
+
- **O bloco que segue o cursor é um `DragOverlay`, em `position: fixed`.** É
|
|
307
|
+
obrigatório: três ancestrais cortam (o frame da grade, a pilha de eventos da
|
|
308
|
+
célula e o corpo rolável de week/day), e elemento transformado continua sujeito
|
|
309
|
+
ao clipping do ancestral — a primeira versão movia o próprio bloco e ele
|
|
310
|
+
**desaparecia** ao sair da célula. Se você embrulhar o `Scheduler` num
|
|
311
|
+
container com `transform`, `filter` ou `contain: paint`, esse container passa
|
|
312
|
+
a ser o containing block do `fixed` e o defeito volta.
|
|
313
|
+
- **Só dá pra mover pra um dia VISÍVEL na grade.** No mês isso inclui as células
|
|
314
|
+
cinzas dos meses vizinhos (a grade de setembro/2026 vai de 30/ago a 10/out, e
|
|
315
|
+
as duas pontas são alvo válido). Pra um mês fora da tela, navegue primeiro —
|
|
316
|
+
não há auto-avanço ao arrastar sobre as setas.
|
|
317
|
+
|
|
318
|
+
- **O dnd é por PONTEIRO.** O `KeyboardSensor` do dnd-kit não está registrado,
|
|
319
|
+
de propósito: ele reivindica `Space`/`Enter`, que num `<button>` de evento já
|
|
320
|
+
significam "abrir o detalhe" — as duas coisas brigavam e o arraste era
|
|
321
|
+
cancelado (medido na live region do dnd-kit). `Space`/`Enter` abrem o detalhe,
|
|
322
|
+
que é onde data e hora se editam em campo de formulário: a rota acessível pra
|
|
323
|
+
reagendar. O raciocínio completo está no topo de `use-scheduler-dnd.ts`.
|
|
324
|
+
|
|
325
|
+
> ⚠️ O gesto de ponteiro **não pôde ser verificado no browser de teste**: o
|
|
326
|
+
> `PointerSensor` usa `setPointerCapture` com `pointerId` real, e nem
|
|
327
|
+
> `left_click_drag` nem `PointerEvent` sintéticos o ativam — medido, o evento não
|
|
328
|
+
> sai do lugar. A resolução do drop é coberta por
|
|
329
|
+
> `hooks/use-scheduler-dnd.test.ts` (20 casos), e no DOM foi medido: 12 blocos
|
|
330
|
+
> com 24 alças na semana, e o droppable respondendo (a live region do dnd-kit
|
|
331
|
+
> nomeou `day:<ms>` ao passar por cima).
|
|
332
|
+
|
|
333
|
+
### 5c. Teclado: a grade é UMA parada de `Tab`
|
|
334
|
+
|
|
335
|
+
Roving tabindex (padrão WAI-ARIA de `grid`): a célula ativa tem `tabIndex=0`, as
|
|
336
|
+
outras `-1`.
|
|
337
|
+
|
|
338
|
+
| tecla | ação |
|
|
339
|
+
|---|---|
|
|
340
|
+
| `←` `→` | dia anterior / seguinte |
|
|
341
|
+
| `↑` `↓` | mesma coluna, linha acima / abaixo (mês: semana; week/day: hora) |
|
|
342
|
+
| `Home` / `End` | início / fim da **linha** — não da grade |
|
|
343
|
+
| `PageUp` / `PageDown` | primeira / última célula |
|
|
344
|
+
| `Enter` ou `Space` | cria no slot focado (dispara `onSlotClick`) |
|
|
345
|
+
|
|
346
|
+
Sem isso a grade do mês custava **42 `Tab`** e a da semana **168**. O `+` de
|
|
347
|
+
criar tem `tabIndex={-1}` de propósito: ele é alcançado pelo `Enter` na célula,
|
|
348
|
+
e deixá-lo tabbável reintroduziria as 42 paradas.
|
|
349
|
+
|
|
350
|
+
Medido no browser: `→` levou de 0 pra 1, `↓` de 1 pra 8 (+7), `Home` de 8 pra 7
|
|
351
|
+
(início da linha), com sempre exatamente 1 célula em `tabIndex=0`.
|
|
352
|
+
|
|
353
|
+
### 6. `draggable` e `resizable` nascem `false`
|
|
354
|
+
|
|
355
|
+
Deliberado (mesmo default do `enableDnD` do `Kanban`): dnd ligado sem
|
|
356
|
+
`onEventMove` conectado deixa o usuário arrastar e ver o evento voltar sozinho
|
|
357
|
+
— o pior estado possível, porque parece bug do app.
|
|
358
|
+
|
|
359
|
+
O componente **não liga o dnd** se não houver `onEventMove`/`onEventResize`
|
|
360
|
+
conectado, e em DEV avisa por `console.warn` nomeando qual falta. `event.draggable`
|
|
361
|
+
e `event.resizable` sobrepõem por evento — é como se diz "esta reunião é fixa"
|
|
362
|
+
sem desligar o board todo.
|
|
363
|
+
|
|
364
|
+
### 6b. `SchedulerFilterPanel` também é exportado solto
|
|
365
|
+
|
|
366
|
+
Mesmo padrão do `TableToolbar`, que expõe `ToolbarSearch`, `ToolbarSegmented` e
|
|
367
|
+
as outras partes pelo barrel. Serve pra tela que queira posicionar o painel em
|
|
368
|
+
outro lugar do próprio layout — e pra doc page, que o renderiza isolado pra
|
|
369
|
+
mostrar a anatomia (cabeçalho sticky · mini-calendário · um grupo por campo).
|
|
370
|
+
|
|
371
|
+
**Em uso normal você não precisa dele:** o `Scheduler` monta e controla o seu.
|
|
372
|
+
Usando solto, todo o estado é seu — `filterModel`, `counts`, `date`, `now` e os
|
|
373
|
+
callbacks vêm por prop, e nada é derivado sozinho.
|
|
374
|
+
|
|
375
|
+
#### `embedded` — quando o painel NÃO é a superfície
|
|
376
|
+
|
|
377
|
+
| | `embedded={false}` (default) | `embedded` |
|
|
378
|
+
|---|---|---|
|
|
379
|
+
| Largura | `w-[280px]` fixa | `w-full` |
|
|
380
|
+
| Moldura | borda + radius + `bg-bg-surface` | nenhuma |
|
|
381
|
+
| Cabeçalho próprio | título + Limpar + × | não renderiza |
|
|
382
|
+
|
|
383
|
+
Ligue quando ele viver **dentro** de outro contêiner que já é a superfície e já
|
|
384
|
+
traz cabeçalho — é o que o próprio `Scheduler` faz no modo drawer (<1024px),
|
|
385
|
+
dentro de um `FloatingPanel`.
|
|
386
|
+
|
|
387
|
+
São duas mudanças numa prop só porque é uma decisão só: *"existe outro contêiner
|
|
388
|
+
em volta?"*. Separadas, dava pra combinar metade de cada e obter um card de 280px
|
|
389
|
+
sem cabeçalho dentro de um painel de 375 — que é exatamente o defeito.
|
|
390
|
+
|
|
391
|
+
### 7. O painel de detalhe é seu, não do componente
|
|
392
|
+
|
|
393
|
+
O `Scheduler` não importa `FloatingPanel`. Ele emite
|
|
394
|
+
`onEventClick(event, evt)` e devolve `event.meta` intacto; quem monta o painel
|
|
395
|
+
é a tela. O padrão de referência é o bloco **`dsgreen-paneldetail-2`**
|
|
396
|
+
(`side="right"`, `size="lg"`, `titleSlot` com o contexto, lista plana de
|
|
397
|
+
propriedades) — há um exemplo completo em `src/preview/pages/SchedulerDoc.tsx`.
|
|
398
|
+
|
|
399
|
+
⚠️ O body do `FloatingPanel` **não tem gap entre filhos**. Sem um wrapper
|
|
400
|
+
`flex flex-col gap-gp-2xl`, título e lista ficam colados.
|
|
401
|
+
|
|
402
|
+
### 8. No mês, evento multi-dia aparece por dia, não como barra contínua
|
|
403
|
+
|
|
404
|
+
`hooks/layout.ts` já calcula `colStart`/`colSpan` por linha, mas a view desenha
|
|
405
|
+
o evento em **cada dia que ocupa**, com as pontas truncadas marcando a
|
|
406
|
+
continuação. A barra única atravessando as colunas exige reservar a mesma faixa
|
|
407
|
+
vertical em todas as células (lane-packing por linha da grade) e vem com o dnd.
|
|
408
|
+
A extensão do evento é lida corretamente — o que falta é refinamento visual.
|
|
409
|
+
|
|
410
|
+
### 9. `filterModel` pré-aplicado é o caminho certo pra "já vem filtrado"
|
|
411
|
+
|
|
412
|
+
Nunca monte um form de filtro acima da grade (L-051). Passe `filterModel` e o
|
|
413
|
+
chip aparece já aplicado, com o `×` pra desfazer.
|
|
414
|
+
|
|
415
|
+
### 10. Nada aqui importa de `TableToolbar`
|
|
416
|
+
|
|
417
|
+
A gramática visual do segmented, da busca e dos chips é **copiada** em
|
|
418
|
+
`scheduler.styles.ts`, não importada. Cross-import entre pastas de `ui/` é o
|
|
419
|
+
que gerou o `registryDependency` dangling da L-049 (`@igreen/table-toolbar` não
|
|
420
|
+
existe como item de registry). Se você editar o visual da toolbar do
|
|
421
|
+
`TableToolbar`, considere se esta cópia deve acompanhar.
|