product-runner 0.5.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/README.md +165 -0
- package/common/agents/README.md +68 -0
- package/common/agents/agente-conceituacao.md +141 -0
- package/common/agents/agente-documentacao-funcional.md +107 -0
- package/common/agents/agente-gerador-spec.md +106 -0
- package/common/agents/agente-kickoff.md +121 -0
- package/common/agents/agente-prod-runner.md +107 -0
- package/common/agents/agente-review-code.md +97 -0
- package/common/agents/agente-review-llm.md +94 -0
- package/common/agents/agente-review-product.md +98 -0
- package/common/agents/agente-user-review.md +99 -0
- package/common/agents/protocolo-de-gates.md +51 -0
- package/common/claude-md.template.md +210 -0
- package/common/design-principles.md +229 -0
- package/common/lessons-learned.md +440 -0
- package/common/pipeline.md +143 -0
- package/common/spec-guide.md +327 -0
- package/common/specs/_open-issues.md +46 -0
- package/common/specs/_overview.md +75 -0
- package/dist/cli.js +187 -0
- package/dist/migrations.js +147 -0
- package/dist/scaffold.js +276 -0
- package/dist/update.js +400 -0
- package/migrations/0.3.0.md +54 -0
- package/migrations/0.4.0.md +76 -0
- package/migrations/0.5.0.md +55 -0
- package/migrations/README.md +68 -0
- package/package.json +41 -0
- package/profile-cli/README.md +54 -0
- package/profile-cli/claude-md.extension.md +102 -0
- package/profile-cli/code-patterns.md +363 -0
- package/profile-ssr/DESIGN-SYSTEM.md +795 -0
- package/profile-ssr/README.md +51 -0
- package/profile-ssr/api-patterns.md +70 -0
- package/profile-ssr/claude-md.extension.md +113 -0
- package/profile-ssr/code-patterns.md +175 -0
- package/profile-ssr/ui-patterns.md +97 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
# Design System — Reference (LLM)
|
|
2
|
+
|
|
3
|
+
> Reference document for AI tools (Claude Code, etc.) and developers working on the painel.
|
|
4
|
+
> When in doubt about UI decisions, consult this file before improvising.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Invariants (non-negotiable)
|
|
9
|
+
|
|
10
|
+
1. **Tokens are the source of truth.** Import from `@/lib/tokens`. Never hardcode color, spacing, radius, motion, or typography values in components.
|
|
11
|
+
2. **Components in `components/ui/` import tokens, never hardcode.** Tailwind utilities backed by tokens are the only sanctioned styling path.
|
|
12
|
+
3. **DS rules apply everywhere.** Any UI built without consulting this file is suspect. If a rule blocks something legitimate, surface the conflict instead of working around it silently.
|
|
13
|
+
4. **Semantic theme tokens are mandatory for structural styling.** Background, text, border, state and focus-ring of base components must use the semantic utilities (`bg-surface`, `text-text-primary`, `border-border-default`, `bg-state-*`, `ring-focus-ring`) — never raw `neutral-*` / `primary-*`. Primitives (`primary/neutral/success/warning/danger`) are reserved for decorative / non-structural cases. See [Theming (Light/Dark)](#theming-lightdark).
|
|
14
|
+
5. **Light/dark support is mandatory for DS components.** Components must be theme-agnostic: they render correctly in both themes via CSS variables + `darkMode: 'class'`, without changing component code. The active theme is selected by toggling the `.dark` class on the root (`<html>`).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Tokens
|
|
19
|
+
|
|
20
|
+
Tokens live in `@/lib/tokens`. Tailwind config consumes them via `theme.extend`.
|
|
21
|
+
|
|
22
|
+
There are two layers:
|
|
23
|
+
|
|
24
|
+
- **Primitives** — raw scales. Decorative / non-structural use only.
|
|
25
|
+
- **Semantic** — theme-aware roles (background, text, border, state, focus). Mandatory for structural styling. See [Theming (Light/Dark)](#theming-lightdark).
|
|
26
|
+
|
|
27
|
+
Available namespaces:
|
|
28
|
+
|
|
29
|
+
- `tokens.color.{primary, neutral, success, warning, danger}.{50..900}` — primitives
|
|
30
|
+
- `tokens.semantic.{light,dark}.{bg,text,border,state,focus}` — semantic roles
|
|
31
|
+
- `tokens.space.{1, 2, 3, 4, 6, 8}`
|
|
32
|
+
- `tokens.radius.{sm, md, lg, xl}`
|
|
33
|
+
- `tokens.motion.{fast, base, slow}`
|
|
34
|
+
- `tokens.font.{family.{sans, mono}, size.{xs, sm, base, lg, xl}}`
|
|
35
|
+
|
|
36
|
+
Use Tailwind utilities for everything: `bg-surface`, `text-text-primary`, `p-4`, `rounded-lg`, `duration-base`. For inline styles needing a token value, import from `@/lib/tokens` directly.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Theming (Light/Dark)
|
|
41
|
+
|
|
42
|
+
The DS supports light and dark themes through a semantic token layer. Components never branch on theme — they reference semantic utilities, and the active theme is chosen by toggling the `.dark` class on `<html>`.
|
|
43
|
+
|
|
44
|
+
### Token model: primitives × semantic
|
|
45
|
+
|
|
46
|
+
| Layer | Where | Example | When to use |
|
|
47
|
+
| ------------- | --------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
|
|
48
|
+
| **Primitive** | `tokens.color.*` → `bg-primary-600` | `primary-600`, `neutral-200` | Decorative accents, charts, illustrations, one-off non-structural color. |
|
|
49
|
+
| **Semantic** | `tokens.semantic.*` → CSS vars → `bg-surface` | `surface`, `text-primary` | **All structural styling** of base components: bg, text, border, state, focus. |
|
|
50
|
+
|
|
51
|
+
Flow: `tokens.semantic.{light,dark}` (conceptual map) → RGB-triplet CSS variables in `src/styles/globals.css` (`:root` = light, `.dark` = dark) → Tailwind utilities in `tailwind.config.ts` (`rgb(var(--token) / <alpha-value>)`). `tokens.ts` stays the single source of truth for the role→primitive mapping; keep both files in sync.
|
|
52
|
+
|
|
53
|
+
### Rule of use
|
|
54
|
+
|
|
55
|
+
- **Structural → semantic, always.** Surfaces, body/secondary text, separators, the focus ring, and primary/success/warning/danger _state fills_ on base components use semantic utilities.
|
|
56
|
+
- **Primitive → decorative only.** Reach for `neutral-*` / `primary-*` only when a value is genuinely not structural (and never for the structural roles a semantic token already covers).
|
|
57
|
+
- **State tones (success/warning/danger primitives)** remain acceptable for tonal chips/alerts (non-structural), but neutral and primary structural color must go through semantic tokens.
|
|
58
|
+
- **On-color foreground** (text on a saturated state fill, e.g. a primary button) stays `text-white` — it is correct in both themes.
|
|
59
|
+
|
|
60
|
+
### Official semantic utilities
|
|
61
|
+
|
|
62
|
+
| Role | Utilities | CSS variable(s) |
|
|
63
|
+
| ---------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
64
|
+
| Background | `bg-canvas`, `bg-surface`, `bg-elevated` | `--bg-canvas`, `--bg-surface`, `--bg-elevated` |
|
|
65
|
+
| Text | `text-text-primary`, `text-text-secondary`, `text-text-inverse` | `--text-primary`, `--text-secondary`, `--text-inverse` |
|
|
66
|
+
| Border | `border-border-default`, `border-border-subtle` | `--border-default`, `--border-subtle` |
|
|
67
|
+
| State | `bg-state-primary`, `bg-state-success`, `bg-state-warning`, `bg-state-danger` | `--state-primary`, `--state-success`, `--state-warning`, `--state-danger` |
|
|
68
|
+
| Focus ring | `ring-focus-ring` (use with `focus-visible:ring-2`) | `--focus-ring` |
|
|
69
|
+
|
|
70
|
+
These accept alpha (e.g. `bg-state-primary/10`, `hover:bg-text-primary/5`) because the config maps them as `rgb(var(--token) / <alpha-value>)`. Two recurring theme-agnostic patterns: subtle hovers via `hover:bg-text-primary/5|10`, and inverse surfaces (tooltip/toast info) via `bg-text-primary text-text-inverse`.
|
|
71
|
+
|
|
72
|
+
### Do / Don't
|
|
73
|
+
|
|
74
|
+
**Do**
|
|
75
|
+
|
|
76
|
+
- Use `bg-surface` / `bg-canvas` / `bg-elevated` for component surfaces.
|
|
77
|
+
- Use `text-text-primary` / `text-text-secondary` for content text.
|
|
78
|
+
- Use `border-border-default` / `-subtle` for separators and outlines.
|
|
79
|
+
- Use `ring-focus-ring` for focus-visible rings, `border-state-danger` + `ring-state-danger` for error fields.
|
|
80
|
+
- Express subtle hovers as `hover:bg-text-primary/5` (or `/10`) so they adapt to the theme.
|
|
81
|
+
|
|
82
|
+
**Don't**
|
|
83
|
+
|
|
84
|
+
- Don't use `bg-white`, `bg-neutral-50`, `text-neutral-900`, `border-neutral-200`, `focus:ring-primary-500` for structural styling.
|
|
85
|
+
- Don't hardcode hex/rgb/hsl in `components/ui/*`.
|
|
86
|
+
- Don't branch component logic on the theme — semantic tokens already do it.
|
|
87
|
+
- Don't add a new CSS variable without also adding its `:root` + `.dark` value and a Tailwind utility mapping.
|
|
88
|
+
|
|
89
|
+
### Quick substitution table
|
|
90
|
+
|
|
91
|
+
| Before | After |
|
|
92
|
+
| ------------------------ | ----------------------- |
|
|
93
|
+
| `bg-white` | `bg-surface` |
|
|
94
|
+
| `bg-neutral-50` | `bg-canvas` |
|
|
95
|
+
| `text-neutral-900` | `text-text-primary` |
|
|
96
|
+
| `text-neutral-600/500` | `text-text-secondary` |
|
|
97
|
+
| `border-neutral-200` | `border-border-default` |
|
|
98
|
+
| `focus:ring-primary-500` | `focus:ring-focus-ring` |
|
|
99
|
+
|
|
100
|
+
### Example
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
// Theme-agnostic surface — renders correctly in light and dark.
|
|
104
|
+
<div className="bg-surface text-text-primary border border-border-default rounded-lg p-4">
|
|
105
|
+
<h3 className="font-semibold">Resumo</h3>
|
|
106
|
+
<p className="text-text-secondary">Detalhe secundário.</p>
|
|
107
|
+
<button className="mt-3 h-9 px-4 rounded-md bg-state-primary text-white hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">
|
|
108
|
+
Salvar
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Enabling dark mode: toggle the class on the root — `document.documentElement.classList.toggle('dark')`. The DS Explorer header (`/ds-explorer`) has a working toggle that exercises every component in both themes.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Patterns
|
|
118
|
+
|
|
119
|
+
Transversal rules that govern how components compose. Each component declares which patterns it applies.
|
|
120
|
+
|
|
121
|
+
### Alinhamento de ações (`action-alignment`)
|
|
122
|
+
|
|
123
|
+
Em qualquer container que agrupe ações (footer de form, card, modal, drawer, toolbar), as ações ficam alinhadas à direita, com a primária na ponta direita.
|
|
124
|
+
|
|
125
|
+
**Rule:** Ações secundárias (Cancelar/Limpar/Voltar) à esquerda do grupo. Primária (Salvar/Enviar/Confirmar/Excluir destrutivo) sempre na ponta direita. Espaçamento entre botões = `space.2`.
|
|
126
|
+
|
|
127
|
+
**Applies to:** Form, Card (footer com ações), Modal/Dialog, Drawer/Sheet, Page toolbar
|
|
128
|
+
**Does NOT apply to:** Botões-gatilho avulsos no meio de conteúdo, ações inline em linhas de tabela, CTAs em landing/marketing, toolbar contextual em editor
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### Timing de validação (`validation-timing`)
|
|
133
|
+
|
|
134
|
+
Quando disparar validação para que o feedback seja útil sem ser ruidoso.
|
|
135
|
+
|
|
136
|
+
**Rule:** Campos individuais validam onBlur. Form inteiro valida onSubmit. Nunca onChange a cada tecla, exceto para feedback contínuo previsível (medidor de senha, contador de caracteres). Após erro de submit, revalidar onChange para o erro sumir conforme o usuário corrige.
|
|
137
|
+
|
|
138
|
+
**Applies to:** Input, Select, Form, Form fields compostos
|
|
139
|
+
**Does NOT apply to:** Search-as-you-type, editores colaborativos em tempo real, medidor de força de senha / contador de caracteres
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Estados vazios (`empty-states`)
|
|
144
|
+
|
|
145
|
+
Toda lista, tabela ou área de conteúdo dinâmico precisa de um estado vazio que explique e ofereça caminho.
|
|
146
|
+
|
|
147
|
+
**Rule:** Estado vazio tem: (1) ícone/ilustração simples, (2) título curto que diz por que está vazio, (3) descrição opcional com próximo passo, (4) ação primária quando aplicável. Diferenciar 3 tipos: nunca-teve-dados (onboarding), filtro-vazio (limpar filtro), erro-de-carga (tentar novamente).
|
|
148
|
+
|
|
149
|
+
**Applies to:** Table, Listas, Dashboard widgets, Resultados de busca/filtro, Caixa de entrada
|
|
150
|
+
**Does NOT apply to:** Áreas vazias durante loading (use Skeleton), campos de formulário vazios, containers que falham por erro de rede (use Alert)
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### Estados de carregamento (`loading-states`)
|
|
155
|
+
|
|
156
|
+
Qual indicador usar em função da latência esperada.
|
|
157
|
+
|
|
158
|
+
**Rule:** <300ms: nada. 300ms–1s: spinner inline ou loading no botão acionado. 1–10s: skeleton replicando layout final. >10s: skeleton + mensagem de progresso ou estimativa. Sempre prever fallback de erro com retry.
|
|
159
|
+
|
|
160
|
+
**Applies to:** Table (skeleton de linhas), Card com dados async, Dashboard widgets, Botão durante submit, Página em navegação
|
|
161
|
+
**Does NOT apply to:** Operações locais síncronas, ações instantâneas confirmadas (use Toast pós-conclusão)
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### Hierarquia de feedback (`feedback-hierarchy`)
|
|
166
|
+
|
|
167
|
+
Qual canal usar — Toast, Alert ou Modal — em função da urgência e do bloqueio que o feedback deve impor.
|
|
168
|
+
|
|
169
|
+
**Rule:** Toast = evento transitório do sistema, não-bloqueante (salvo, copiado, enviado), auto-dismiss 4–7s. Alert = info persistente em-página, contextual à view, não bloqueia. Modal = decisão obrigatória antes de continuar, bloqueia até resposta.
|
|
170
|
+
|
|
171
|
+
**Applies to:** Toast, Alert, Modal, Banners de sistema
|
|
172
|
+
**Does NOT apply to:** Mensagens de erro inline em formulário (parte do Input), Tooltips (informação on-demand)
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Components
|
|
177
|
+
|
|
178
|
+
21 components total, alphabetical.
|
|
179
|
+
|
|
180
|
+
### Accordion
|
|
181
|
+
|
|
182
|
+
`@/components/ui/accordion` — Seções colapsáveis para revelar conteúdo sob demanda, reduzindo a primeira impressão de uma tela densa.
|
|
183
|
+
|
|
184
|
+
**Patterns:** —
|
|
185
|
+
|
|
186
|
+
| prop | type | default |
|
|
187
|
+
| ------------- | ----------------------------------------------------- | ------- |
|
|
188
|
+
| items | `{ id: string, title: string, content: ReactNode }[]` | — |
|
|
189
|
+
| allowMultiple | `boolean` | false |
|
|
190
|
+
| defaultOpen | `string[]` | [] |
|
|
191
|
+
|
|
192
|
+
**Do:** Use para FAQs, configurações em grupos, detalhes opcionais • Títulos descritivos (não "Mais") • allowMultiple quando seções são independentes • Estado inicial fechado por padrão
|
|
193
|
+
**Don't:** Accordion para fluxo sequencial obrigatório (use Stepper) • Esconder informação crítica em accordion fechado • Aninhar accordions além de 1 nível • Animar abertura por mais de 200ms
|
|
194
|
+
|
|
195
|
+
**Example:**
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
<Accordion items={faqItems} allowMultiple />
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### Alert
|
|
204
|
+
|
|
205
|
+
`@/components/ui/alert` — Mensagem persistente em-página sobre estado relevante.
|
|
206
|
+
|
|
207
|
+
**Patterns:** `feedback-hierarchy`
|
|
208
|
+
|
|
209
|
+
| prop | type | default |
|
|
210
|
+
| ------- | ---------------------------------------- | ------- |
|
|
211
|
+
| tone | `'info'\|'success'\|'warning'\|'danger'` | info |
|
|
212
|
+
| title | `string` | — |
|
|
213
|
+
| onClose | `() => void` | — |
|
|
214
|
+
|
|
215
|
+
**Do:** Use para info contextual permanente • Inclua ação se houver ("Tentar novamente") • Tom semântico correto
|
|
216
|
+
**Don't:** Múltiplos alerts amontoados • Tom vermelho em info benigna
|
|
217
|
+
|
|
218
|
+
**Example:**
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
<Alert tone="warning" title="Sessão expira em 5 min">
|
|
222
|
+
Salve seu trabalho.
|
|
223
|
+
</Alert>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### Avatar
|
|
229
|
+
|
|
230
|
+
`@/components/ui/avatar` — Representar identidade de usuário ou entidade.
|
|
231
|
+
|
|
232
|
+
**Patterns:** —
|
|
233
|
+
|
|
234
|
+
| prop | type | default |
|
|
235
|
+
| ---- | ------------------ | ------- |
|
|
236
|
+
| name | `string` | — |
|
|
237
|
+
| src | `string` | — |
|
|
238
|
+
| size | `'sm'\|'md'\|'lg'` | md |
|
|
239
|
+
|
|
240
|
+
**Do:** Sempre aria-label com nome • Fallback de iniciais • Tamanhos consistentes
|
|
241
|
+
**Don't:** Avatar sem alt/label • Imagem genérica que confunde com ícone • Múltiplos tamanhos misturados na mesma lista
|
|
242
|
+
|
|
243
|
+
**Example:**
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
<Avatar name="Henrique Souza" />
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### Badge
|
|
252
|
+
|
|
253
|
+
`@/components/ui/badge` — Etiqueta curta para status, contagem ou categoria.
|
|
254
|
+
|
|
255
|
+
**Patterns:** —
|
|
256
|
+
|
|
257
|
+
| prop | type | default |
|
|
258
|
+
| ---- | --------------------------------------------------- | ------- |
|
|
259
|
+
| tone | `'neutral'\|'info'\|'success'\|'warning'\|'danger'` | neutral |
|
|
260
|
+
|
|
261
|
+
**Do:** 1-2 palavras • Use junto a um elemento que ele descreve • Tom semântico
|
|
262
|
+
**Don't:** Badge isolado sem âncora visual • Badge em vez de Button • Frase completa
|
|
263
|
+
|
|
264
|
+
**Example:**
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
<Badge tone="success">Aprovado</Badge>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### Breadcrumb
|
|
273
|
+
|
|
274
|
+
`@/components/ui/breadcrumb` — Mostrar localização hierárquica e oferecer caminho de volta.
|
|
275
|
+
|
|
276
|
+
**Patterns:** —
|
|
277
|
+
|
|
278
|
+
| prop | type | default |
|
|
279
|
+
| ----- | ------------------------------------ | ------- |
|
|
280
|
+
| items | `{ label: string, href?: string }[]` | — |
|
|
281
|
+
|
|
282
|
+
**Do:** Último item não-link, com aria-current="page" • Use em hierarquias profundas (≥2) • Separador consistente
|
|
283
|
+
**Don't:** Breadcrumb em telas planas • Substituir nav primária • Quebrar em múltiplas linhas em desktop
|
|
284
|
+
|
|
285
|
+
**Example:**
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
<Breadcrumb
|
|
289
|
+
items={[
|
|
290
|
+
{ label: 'Início', href: '/' },
|
|
291
|
+
{ label: 'Documentos', href: '/docs' },
|
|
292
|
+
{ label: 'Inventário 2025' },
|
|
293
|
+
]}
|
|
294
|
+
/>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
### Button
|
|
300
|
+
|
|
301
|
+
`@/components/ui/button` — Disparar ação imediata do usuário.
|
|
302
|
+
|
|
303
|
+
**Patterns:** `action-alignment`
|
|
304
|
+
|
|
305
|
+
| prop | type | default |
|
|
306
|
+
| --------- | ------------------------------------------- | ------- |
|
|
307
|
+
| variant | `'primary'\|'secondary'\|'danger'\|'ghost'` | primary |
|
|
308
|
+
| size | `'sm'\|'md'\|'lg'` | md |
|
|
309
|
+
| loading | `boolean` | false |
|
|
310
|
+
| disabled | `boolean` | false |
|
|
311
|
+
| leftIcon | `IconComponent` | — |
|
|
312
|
+
| rightIcon | `IconComponent` | — |
|
|
313
|
+
|
|
314
|
+
**Do:** Use 1 botão primário por área de decisão • Texto-verbo no infinitivo ("Salvar", "Enviar") • Use loading em ações ≥ 200ms
|
|
315
|
+
**Don't:** Múltiplos primários competindo • Texto vago ("OK", "Clique aqui") • Tamanho < 40px em mobile
|
|
316
|
+
|
|
317
|
+
**Example:**
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
<Button loading>Salvando…</Button>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
### Card
|
|
326
|
+
|
|
327
|
+
`@/components/ui/card` — Agrupar conteúdo relacionado em uma unidade visual.
|
|
328
|
+
|
|
329
|
+
**Patterns:** `action-alignment`
|
|
330
|
+
|
|
331
|
+
| prop | type | default |
|
|
332
|
+
| --------- | ----------- | ------- |
|
|
333
|
+
| title | `ReactNode` | — |
|
|
334
|
+
| footer | `ReactNode` | — |
|
|
335
|
+
| className | `string` | — |
|
|
336
|
+
|
|
337
|
+
**Do:** Use para agrupar conteúdo coeso • Mantenha hierarquia: title → content → actions • Footer só para ações ou metadados
|
|
338
|
+
**Don't:** Card aninhado em card aninhado em card • Conteúdo desconexo no mesmo card • Sombra agressiva
|
|
339
|
+
|
|
340
|
+
**Example:**
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
<Card
|
|
344
|
+
title="Resumo"
|
|
345
|
+
footer={
|
|
346
|
+
<div className="flex justify-end">
|
|
347
|
+
<Button size="sm">Detalhes</Button>
|
|
348
|
+
</div>
|
|
349
|
+
}
|
|
350
|
+
>
|
|
351
|
+
<p>Conteúdo principal.</p>
|
|
352
|
+
</Card>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
### Checkbox
|
|
358
|
+
|
|
359
|
+
`@/components/ui/checkbox` — Seleção independente (0..N) ou toggle binário em formulário.
|
|
360
|
+
|
|
361
|
+
**Patterns:** —
|
|
362
|
+
|
|
363
|
+
| prop | type | default |
|
|
364
|
+
| -------- | ------------- | ------- |
|
|
365
|
+
| label | `string` | — |
|
|
366
|
+
| checked | `boolean` | — |
|
|
367
|
+
| onChange | `(e) => void` | — |
|
|
368
|
+
|
|
369
|
+
**Do:** Label clicável (associada via htmlFor) • Use para opções independentes • Erro de obrigatório claro
|
|
370
|
+
**Don't:** Checkbox para escolha exclusiva (use radio) • Sem label visível • Estado indeterminado sem necessidade
|
|
371
|
+
|
|
372
|
+
**Example:**
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
<Checkbox
|
|
376
|
+
label="Receber novidades"
|
|
377
|
+
checked={v}
|
|
378
|
+
onChange={(e) => setV(e.target.checked)}
|
|
379
|
+
/>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
### DatePicker
|
|
385
|
+
|
|
386
|
+
`@/components/ui/date-picker` — Selecionar uma data por digitação ou pelo calendário, com formato local.
|
|
387
|
+
|
|
388
|
+
**Patterns:** `validation-timing`
|
|
389
|
+
|
|
390
|
+
| prop | type | default |
|
|
391
|
+
| -------- | ------------------------------ | ------- |
|
|
392
|
+
| value | `Date \| null` | — |
|
|
393
|
+
| onChange | `(date: Date \| null) => void` | — |
|
|
394
|
+
| label | `string` | — |
|
|
395
|
+
| hint | `string` | — |
|
|
396
|
+
| error | `string` | — |
|
|
397
|
+
|
|
398
|
+
**Do:** Aceite digitação E seleção visual • Use formato local (dd/mm/aaaa em pt-BR) • Hoje destacado, selecionado destacado distintamente • Botões "Hoje" e "Limpar" como atalhos
|
|
399
|
+
**Don't:** Forçar máscara que bloqueia colar • Calendário sem destaque visual de hoje • DatePicker para datas pré-definidas (use Select com opções) • Calendário cobrindo o input após seleção
|
|
400
|
+
|
|
401
|
+
**Example:**
|
|
402
|
+
|
|
403
|
+
```tsx
|
|
404
|
+
<DatePicker
|
|
405
|
+
label="Data de nascimento"
|
|
406
|
+
value={d}
|
|
407
|
+
onChange={setD}
|
|
408
|
+
hint="Use dd/mm/aaaa"
|
|
409
|
+
/>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### Form
|
|
415
|
+
|
|
416
|
+
`@/components/ui/form` — Composição de campos com validação, submissão e feedback. Usar com react-hook-form + zod resolver.
|
|
417
|
+
|
|
418
|
+
**Patterns:** `action-alignment`, `validation-timing`
|
|
419
|
+
|
|
420
|
+
_Sem props diretas — é composição._
|
|
421
|
+
|
|
422
|
+
**Do:** Reutilize schema do service (não duplique validação) • Foco vai para o primeiro campo com erro • Confirme sucesso explicitamente (Toast ou Alert) • Erros de API via Toast
|
|
423
|
+
**Don't:** Submeter sem feedback • Esconder campos obrigatórios em accordions fechados • Resetar dados após erro • Duplicar validação entre client e service
|
|
424
|
+
|
|
425
|
+
A API real é `Form` + `FormFooter` (não `FormField`/`FormItem`/`FormMessage`).
|
|
426
|
+
Os campos (`Input`, `Select`, `Checkbox`) trazem label/hint/error embutidos.
|
|
427
|
+
|
|
428
|
+
**Example:**
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
const [data, setData] = useState({ email: '', role: 'analyst' });
|
|
432
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
433
|
+
|
|
434
|
+
<Form onSubmit={handleSubmit}>
|
|
435
|
+
<Input
|
|
436
|
+
label="E-mail"
|
|
437
|
+
type="email"
|
|
438
|
+
required
|
|
439
|
+
value={data.email}
|
|
440
|
+
onChange={(e) => setData({ ...data, email: e.target.value })}
|
|
441
|
+
onBlur={() => validateEmail(data.email, setErrors)}
|
|
442
|
+
error={errors.email}
|
|
443
|
+
/>
|
|
444
|
+
<FormFooter>
|
|
445
|
+
<Button variant="ghost" type="button">
|
|
446
|
+
Limpar
|
|
447
|
+
</Button>
|
|
448
|
+
<Button type="submit">Enviar</Button>
|
|
449
|
+
</FormFooter>
|
|
450
|
+
</Form>;
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**RHF + zod compatibility:** Input/Select/Checkbox exportam `forwardRef`. Use `{...register('campo')}` direto.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### Input
|
|
458
|
+
|
|
459
|
+
`@/components/ui/input` — Coletar texto curto do usuário.
|
|
460
|
+
|
|
461
|
+
**Patterns:** `validation-timing`
|
|
462
|
+
|
|
463
|
+
| prop | type | default |
|
|
464
|
+
| -------- | --------------- | ------- |
|
|
465
|
+
| label | `string` | — |
|
|
466
|
+
| hint | `string` | — |
|
|
467
|
+
| error | `string` | — |
|
|
468
|
+
| required | `boolean` | false |
|
|
469
|
+
| type | `HTMLInputType` | text |
|
|
470
|
+
|
|
471
|
+
**Do:** Sempre use label visível • Indique \* em campos obrigatórios • Erros descrevem o problema E como corrigir
|
|
472
|
+
**Don't:** Placeholder substituindo label • Erro só por cor da borda • Validação só ao submeter, sem hint prévio
|
|
473
|
+
|
|
474
|
+
**Example:**
|
|
475
|
+
|
|
476
|
+
```tsx
|
|
477
|
+
<Input label="E-mail" hint="Usaremos para login" type="email" />
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
### Modal
|
|
483
|
+
|
|
484
|
+
`@/components/ui/dialog` — Tarefa focada que exige atenção total ou confirmação destrutiva.
|
|
485
|
+
|
|
486
|
+
**Patterns:** `action-alignment`, `feedback-hierarchy`
|
|
487
|
+
|
|
488
|
+
| prop | type | default |
|
|
489
|
+
| ------- | ------------ | ------- |
|
|
490
|
+
| open | `boolean` | — |
|
|
491
|
+
| onClose | `() => void` | — |
|
|
492
|
+
| title | `ReactNode` | — |
|
|
493
|
+
| footer | `ReactNode` | — |
|
|
494
|
+
|
|
495
|
+
**Do:** Foco vai para 1º elemento ao abrir • Esc fecha
|
|
496
|
+
**Don't:** Modal para fluxo longo • Modal sobre modal • Confirmações triviais ("Tem certeza que quer salvar?")
|
|
497
|
+
|
|
498
|
+
**Example:**
|
|
499
|
+
|
|
500
|
+
```tsx
|
|
501
|
+
<Modal
|
|
502
|
+
open={open}
|
|
503
|
+
onClose={() => setOpen(false)}
|
|
504
|
+
title="Excluir documento"
|
|
505
|
+
footer={
|
|
506
|
+
<>
|
|
507
|
+
<Button variant="ghost">Cancelar</Button>
|
|
508
|
+
<Button variant="danger">Excluir</Button>
|
|
509
|
+
</>
|
|
510
|
+
}
|
|
511
|
+
>
|
|
512
|
+
Esta ação não pode ser desfeita.
|
|
513
|
+
</Modal>
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
### Nav
|
|
519
|
+
|
|
520
|
+
`@/components/ui/nav` — Estrutura de navegação primária. Exporta dois componentes para composição flexível: `TopBar` (header) e `SideNav` (lista lateral).
|
|
521
|
+
|
|
522
|
+
**Patterns:** —
|
|
523
|
+
|
|
524
|
+
**TopBar:**
|
|
525
|
+
|
|
526
|
+
| prop | type | default |
|
|
527
|
+
| ------- | ----------- | ------- |
|
|
528
|
+
| brand | `ReactNode` | — |
|
|
529
|
+
| actions | `ReactNode` | — |
|
|
530
|
+
|
|
531
|
+
**SideNav:**
|
|
532
|
+
|
|
533
|
+
| prop | type | default |
|
|
534
|
+
| ---------- | ------------------------------- | ------- |
|
|
535
|
+
| items | `{ id, label, href?, icon? }[]` | — |
|
|
536
|
+
| activeId | `string` | — |
|
|
537
|
+
| onNavigate | `(id: string) => void` | — |
|
|
538
|
+
|
|
539
|
+
**Do:** Marque rota ativa com aria-current="page" • Limite top-level a ~5-7 itens • Mantenha ordem estável
|
|
540
|
+
**Don't:** Reordenar itens dinamicamente • Esconder navegação principal em hover • Ícones sem texto em desktop
|
|
541
|
+
|
|
542
|
+
**Example:**
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
<TopBar brand={<Logo />} actions={<Avatar name="HS" />} />
|
|
546
|
+
<SideNav items={navItems} activeId={route} />
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
### Pagination
|
|
552
|
+
|
|
553
|
+
`@/components/ui/pagination` — Navegar entre páginas de uma lista longa, mantendo posição clara e atalhos para extremos.
|
|
554
|
+
|
|
555
|
+
**Patterns:** `loading-states`
|
|
556
|
+
|
|
557
|
+
| prop | type | default |
|
|
558
|
+
| ------------ | ------------------------ | ------- |
|
|
559
|
+
| page | `number` | — |
|
|
560
|
+
| totalPages | `number` | — |
|
|
561
|
+
| onChange | `(page: number) => void` | — |
|
|
562
|
+
| siblingCount | `number` | 1 |
|
|
563
|
+
|
|
564
|
+
**Do:** Mostre primeira e última página sempre • Use ellipsis para gaps • Indique página atual com aria-current="page"
|
|
565
|
+
**Don't:** Listar todas as páginas em listas longas (>10) • Pagination quando infinite scroll é melhor (feeds) • Tamanho de alvo < 44px em mobile • Esconder total de páginas
|
|
566
|
+
|
|
567
|
+
**Example:**
|
|
568
|
+
|
|
569
|
+
```tsx
|
|
570
|
+
<Pagination page={page} totalPages={50} onChange={setPage} />
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
### Popover
|
|
576
|
+
|
|
577
|
+
`@/components/ui/popover` — Painel flutuante interativo, ancorado a um gatilho, com conteúdo rico (texto, ações, formulários curtos).
|
|
578
|
+
|
|
579
|
+
**Patterns:** `action-alignment`
|
|
580
|
+
|
|
581
|
+
| prop | type | default |
|
|
582
|
+
| -------- | ----------------- | ------- |
|
|
583
|
+
| trigger | `ReactNode` | — |
|
|
584
|
+
| side | `'top'\|'bottom'` | bottom |
|
|
585
|
+
| children | `ReactNode` | — |
|
|
586
|
+
|
|
587
|
+
**Do:** Use para conteúdo rico mas curto (até 3 ações) • Esc deve fechar • Posicione próximo ao gatilho
|
|
588
|
+
**Don't:** Popover para hint simples (use Tooltip) • Popover para fluxo longo (use Modal/Drawer) • Popover sem ancoragem visual ao gatilho • Múltiplos popovers abertos simultaneamente
|
|
589
|
+
|
|
590
|
+
**Example:**
|
|
591
|
+
|
|
592
|
+
```tsx
|
|
593
|
+
<Popover trigger={<Button>Conta</Button>}>
|
|
594
|
+
<button>Perfil</button>
|
|
595
|
+
<button>Configurações</button>
|
|
596
|
+
<button>Sair</button>
|
|
597
|
+
</Popover>
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
### Select
|
|
603
|
+
|
|
604
|
+
`@/components/ui/select` — Escolha única entre poucas opções conhecidas.
|
|
605
|
+
|
|
606
|
+
**Patterns:** `validation-timing`
|
|
607
|
+
|
|
608
|
+
| prop | type | default |
|
|
609
|
+
| ------- | ------------------------------------ | ------- |
|
|
610
|
+
| label | `string` | — |
|
|
611
|
+
| options | `{ value: string, label: string }[]` | [] |
|
|
612
|
+
| hint | `string` | — |
|
|
613
|
+
| error | `string` | — |
|
|
614
|
+
|
|
615
|
+
**Do:** Default razoável já selecionado • Ordem lógica (alfabética/frequência) • Para >10 opções use combobox com busca
|
|
616
|
+
**Don't:** Select com 1 opção • Select para múltipla seleção (use checkboxes) • Mudança de opção disparando navegação sem aviso
|
|
617
|
+
|
|
618
|
+
**Example:**
|
|
619
|
+
|
|
620
|
+
```tsx
|
|
621
|
+
<Select
|
|
622
|
+
label="Função"
|
|
623
|
+
options={[
|
|
624
|
+
{ value: 'analyst', label: 'Analista' },
|
|
625
|
+
{ value: 'manager', label: 'Gerente' },
|
|
626
|
+
]}
|
|
627
|
+
/>
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
### Skeleton
|
|
633
|
+
|
|
634
|
+
`@/components/ui/skeleton` — Placeholder de carregamento que reproduz o layout final.
|
|
635
|
+
|
|
636
|
+
**Patterns:** `loading-states`
|
|
637
|
+
|
|
638
|
+
| prop | type | default |
|
|
639
|
+
| --------- | -------- | ------- |
|
|
640
|
+
| className | `string` | — |
|
|
641
|
+
|
|
642
|
+
**Do:** Reproduza dimensões aproximadas do conteúdo final • aria-hidden + role=presentation
|
|
643
|
+
**Don't:** Forma muito diferente do real (causa shift) • Spinner + skeleton ao mesmo tempo
|
|
644
|
+
|
|
645
|
+
**Example:**
|
|
646
|
+
|
|
647
|
+
```tsx
|
|
648
|
+
<div className="space-y-2">
|
|
649
|
+
<Skeleton className="h-5 w-1/2" />
|
|
650
|
+
<Skeleton className="h-4 w-full" />
|
|
651
|
+
</div>
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
### Table
|
|
657
|
+
|
|
658
|
+
`@/components/ui/table` — Exibir dados tabulares para comparação e ordenação.
|
|
659
|
+
|
|
660
|
+
**Patterns:** `empty-states`, `loading-states`
|
|
661
|
+
|
|
662
|
+
| prop | type | default |
|
|
663
|
+
| --------- | ------------------------------------------------------ | ------- |
|
|
664
|
+
| columns | `{ key: string, label: string, sortable?: boolean }[]` | — |
|
|
665
|
+
| rows | `Record<string, ReactNode>[]` | — |
|
|
666
|
+
| maxHeight | `number \| string` | — |
|
|
667
|
+
|
|
668
|
+
**Do:** Headers de coluna sempre visíveis • Alinhe números à direita • Para encaixar uma lista pequena/moderada num espaço limitado, use `maxHeight` (rolagem vertical com header fixo no topo) • Forneça paginação >25 linhas
|
|
669
|
+
**Don't:** Tabela para dados não tabulares • Ordenação só por mouse • Larguras instáveis a cada render • `maxHeight` como substituto de paginação (a regra dos 25 vale dentro e fora do scroll)
|
|
670
|
+
|
|
671
|
+
**Altura máxima + scroll vertical:** quando `maxHeight` é definida, o wrapper vira contexto de rolagem (`overflow-auto`) e o `thead` fica `sticky` no topo, mantendo os headers visíveis enquanto as linhas rolam. `number` é interpretado como px. É **ortogonal** à paginação: serve para encaixar uma lista que cresce (ex.: compras de um par) dentro de um card sem esticar a página — **não** substitui paginação. Datasets grandes seguem a regra de paginação (>25 linhas) mesmo dentro do scroll, para não renderizar/rolar centenas de linhas.
|
|
672
|
+
|
|
673
|
+
**Example:**
|
|
674
|
+
|
|
675
|
+
```tsx
|
|
676
|
+
<Table
|
|
677
|
+
columns={[
|
|
678
|
+
{ key: 'doc', label: 'Documento', sortable: true },
|
|
679
|
+
{ key: 'status', label: 'Status' },
|
|
680
|
+
]}
|
|
681
|
+
rows={data}
|
|
682
|
+
/>
|
|
683
|
+
|
|
684
|
+
// Lista longa em altura limitada: corpo rola, header fica fixo.
|
|
685
|
+
<Table columns={columns} rows={manyRows} maxHeight={280} />
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
### Tabs
|
|
691
|
+
|
|
692
|
+
`@/components/ui/tabs` — Alternar entre seções de mesmo nível em uma área de conteúdo.
|
|
693
|
+
|
|
694
|
+
**Patterns:** —
|
|
695
|
+
|
|
696
|
+
| prop | type | default |
|
|
697
|
+
| ----- | ----------------------------------------- | ------- |
|
|
698
|
+
| items | `{ label: string, content: ReactNode }[]` | — |
|
|
699
|
+
|
|
700
|
+
**Do:** Use 2-6 abas • Labels curtos (1-2 palavras) • Mantenha conteúdos de mesma natureza
|
|
701
|
+
**Don't:** Tabs para fluxo sequencial (use Stepper) • Tabs aninhadas • Conteúdo crítico só em aba secundária
|
|
702
|
+
|
|
703
|
+
**Example:**
|
|
704
|
+
|
|
705
|
+
```tsx
|
|
706
|
+
<Tabs
|
|
707
|
+
items={[
|
|
708
|
+
{ label: 'Visão geral', content: <p>Resumo</p> },
|
|
709
|
+
{ label: 'Detalhes', content: <p>Detalhes</p> },
|
|
710
|
+
]}
|
|
711
|
+
/>
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
### Toast
|
|
717
|
+
|
|
718
|
+
`@/components/ui/toast` — Feedback transitório de evento do sistema.
|
|
719
|
+
|
|
720
|
+
**Patterns:** `feedback-hierarchy`
|
|
721
|
+
|
|
722
|
+
| prop | type | default |
|
|
723
|
+
| -------- | ---------------------------------------- | ------- |
|
|
724
|
+
| tone | `'info'\|'success'\|'warning'\|'danger'` | info |
|
|
725
|
+
| title | `string` | — |
|
|
726
|
+
| children | `ReactNode` | — |
|
|
727
|
+
|
|
728
|
+
**Do:** Mensagens curtas (1 linha) • Auto-dismiss em 4-7s para info/success • Erros persistentes até dismiss manual
|
|
729
|
+
**Don't:** Vários toasts empilhados sem agrupamento • Texto comprido
|
|
730
|
+
|
|
731
|
+
**Example:**
|
|
732
|
+
|
|
733
|
+
```tsx
|
|
734
|
+
const { toast } = useToast();
|
|
735
|
+
|
|
736
|
+
toast({
|
|
737
|
+
tone: 'success',
|
|
738
|
+
title: 'Salvo',
|
|
739
|
+
description: 'Alterações persistidas.',
|
|
740
|
+
});
|
|
741
|
+
toast({ tone: 'danger', title: 'Falha', duration: Infinity });
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
**Setup:** wrap app tree em `<ToastProvider>` no `_app.tsx`.
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
### Tooltip
|
|
749
|
+
|
|
750
|
+
`@/components/ui/tooltip` — Hint curto e on-demand sobre um elemento, exibido em hover/focus.
|
|
751
|
+
|
|
752
|
+
**Patterns:** —
|
|
753
|
+
|
|
754
|
+
| prop | type | default |
|
|
755
|
+
| ------- | ---------------------------------- | ------- |
|
|
756
|
+
| content | `string \| ReactNode` | — |
|
|
757
|
+
| side | `'top'\|'bottom'\|'left'\|'right'` | top |
|
|
758
|
+
| delay | `number (ms)` | 300 |
|
|
759
|
+
|
|
760
|
+
**Do:** Use para esclarecer ícones sem rótulo • Texto curto (1 linha, sem ações) • Delay de ~300ms antes de aparecer • Aparece em hover E focus
|
|
761
|
+
**Don't:** Tooltip com info crítica (ela some) • Tooltip com botões/links interativos (use Popover) • Tooltip em texto já legível • Tooltip que cobre o gatilho
|
|
762
|
+
|
|
763
|
+
**Example:**
|
|
764
|
+
|
|
765
|
+
```tsx
|
|
766
|
+
<Tooltip content="Filtrar resultados">
|
|
767
|
+
<Button variant="ghost" size="icon">
|
|
768
|
+
<Filter />
|
|
769
|
+
</Button>
|
|
770
|
+
</Tooltip>
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## Workflow for AI tools (Claude Code, etc.)
|
|
776
|
+
|
|
777
|
+
When implementing or editing UI:
|
|
778
|
+
|
|
779
|
+
1. Identify the components involved. If unsure, check this file's component sections.
|
|
780
|
+
2. Identify which patterns apply (from the **Patterns** field of each component).
|
|
781
|
+
3. Respect the Do/Don't rules per component and the Rule of each applied pattern.
|
|
782
|
+
4. Use tokens from `@/lib/tokens` for all visual decisions. If hardcoding seems necessary, the spec is incomplete — surface that.
|
|
783
|
+
5. If you cannot fulfill a request without violating a Do/Don't or an invariant, surface the conflict instead of silently working around it.
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## PR checklist (UI / DS)
|
|
788
|
+
|
|
789
|
+
Before opening a PR that touches UI, confirm:
|
|
790
|
+
|
|
791
|
+
- [ ] No hardcoded colors (hex/rgb/hsl) in `components/ui/*`.
|
|
792
|
+
- [ ] Structural styling uses semantic utilities (`bg-surface`/`bg-canvas`, `text-text-*`, `border-border-*`, `bg-state-*`, `ring-focus-ring`) — no raw `neutral-*` / `primary-*`.
|
|
793
|
+
- [ ] Renders correctly in both light and dark (verify with the `/ds-explorer` theme toggle).
|
|
794
|
+
- [ ] Focus-visible state is clearly visible in both themes.
|
|
795
|
+
- [ ] Contrast is acceptable (text vs background, in both themes).
|