Skip to content

Commit 1cb5c23

Browse files
committed
feat: Adiciona seção de padrões reutilizáveis no SKILL.md
1 parent ca7c9d3 commit 1cb5c23

File tree

12 files changed

+1586
-245
lines changed

12 files changed

+1586
-245
lines changed

.agent/skills/riligar-dev-dashboard/SKILL.md

Lines changed: 331 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ description: Padrões React específicos do RiLiGar. Zustand, i18n, estrutura de
1515
> Sempre respeite também:
1616
> - @[.agent/skills/riligar-design-system] — UI exclusivo via Mantine, zero CSS
1717
> - Rules em `.agent/rules/` — clean-code, naming-conventions, code-style, javascript-only
18+
> - [references/dependencies.md](references/dependencies.md) — Pacotes e versões do frontend, config Vite
1819
1920
---
2021

@@ -246,7 +247,336 @@ export const WHATSAPP_SUPPORT_MESSAGE = '...' // ou via i18n se traduzível
246247
247248
---
248249
250+
## 8. Padrões reutilizáveis
251+
252+
Estruturas que repeatem across a codebase. Copie o esqueleto, ajuste apenas o conteúdo domínio-específico.
253+
254+
### 8.1 Page Header
255+
256+
Presente em **todas** as pages. Estrutura idêntica sempre:
257+
258+
```javascript
259+
<Box py="xl">
260+
<Group justify="space-between" align="flex-end" mb="xl">
261+
<Stack gap={0}>
262+
<Text size="xs" fw={700} c="dimmed" tt="uppercase" lts="0.1em">
263+
{t('namespace.subtitle')}
264+
</Text>
265+
<Title order={1} style={{ letterSpacing: '-0.04em' }}>
266+
{t('namespace.title')}
267+
</Title>
268+
</Stack>
269+
{/* CTA opcional — ex: <Button leftSection={<IconPlus size={16} />}> */}
270+
</Group>
271+
{/* Conteúdo da página */}
272+
</Box>
273+
```
274+
275+
### 8.2 Empty State
276+
277+
Usado quando uma lista está vazia. Card com borda dashed, icone grande, texto e CTA:
278+
279+
```javascript
280+
<Card padding="xl" radius="md" withBorder style={{ borderStyle: 'dashed', textAlign: 'center' }}>
281+
<Stack align="center" py="xl">
282+
<IconDominio size={48} stroke={1} color="var(--mantine-color-gray-2)" />
283+
<Text c="dimmed" size="sm">{t('namespace.emptyMessage')}</Text>
284+
<Button onClick={handleCreate} leftSection={<IconPlus size={16} />}>
285+
{t('namespace.createFirst')}
286+
</Button>
287+
</Stack>
288+
</Card>
289+
```
290+
291+
### 8.3 Loading Guard
292+
293+
Loader só aparece quando não há dados ainda (não sobrescreve lista existente):
294+
295+
```javascript
296+
{loading && data.length === 0 ? (
297+
<Center style={{ height: 300 }}>
298+
<Loader />
299+
</Center>
300+
) : (
301+
/* conteúdo normal */
302+
)}
303+
```
304+
305+
### 8.4 Card Grid
306+
307+
Layout responsivo padrão para listas de cards:
308+
309+
```javascript
310+
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }}>
311+
{items.map((item) => (
312+
<Card key={item.id} padding="lg" radius="md" withBorder>
313+
{/* conteúdo do card */}
314+
</Card>
315+
))}
316+
</SimpleGrid>
317+
```
318+
319+
Para galerias (mais itens pequenos): `cols={{ base: 1, sm: 2, md: 3, lg: 4 }}`
320+
321+
### 8.5 Modal
322+
323+
Sempre usa `useDisclosure`. Header com borderBottom específico:
324+
325+
```javascript
326+
const [opened, { open, close }] = useDisclosure(false)
327+
328+
<Modal
329+
opened={opened}
330+
onClose={close}
331+
centered
332+
radius="md"
333+
padding="xl"
334+
title={<Text fw={700}>{t('namespace.modalTitle')}</Text>}
335+
styles={{ header: { borderBottom: '1px solid var(--mantine-color-gray-2)', marginBottom: 20 } }}
336+
>
337+
{/* corpo do modal */}
338+
</Modal>
339+
```
340+
341+
Para múltiplos modais na mesma page, renomeia as funções: `{ open: openEdit, close: closeEdit }`
342+
343+
### 8.6 Status Badge
344+
345+
Mapeia status → configuração visual via função que recebe `t`:
346+
347+
```javascript
348+
const getStatusConfig = (t) => ({
349+
draft: { color: 'gray', icon: <IconCircleDotted size={16} />, label: t('posts.status.draft') },
350+
scheduled: { color: 'blue', icon: <IconClock size={16} />, label: t('posts.status.scheduled') },
351+
published: { color: 'green', icon: <IconCircleCheck size={16} />, label: t('posts.status.published') },
352+
failed: { color: 'red', icon: <IconCircleX size={16} />, label: t('posts.status.failed') },
353+
})
354+
355+
// Uso
356+
const config = getStatusConfig(t)[status]
357+
<Badge variant="dot" color={config.color}>{config.label}</Badge>
358+
```
359+
360+
### 8.7 Search / Filter
361+
362+
Filtro local sem chamada de API — estado local + filter inline:
363+
364+
```javascript
365+
const [search, setSearch] = useState('')
366+
367+
const filtered = items.filter((item) =>
368+
item.name.toLowerCase().includes(search.toLowerCase())
369+
)
370+
371+
<TextInput
372+
placeholder={t('common.search')}
373+
leftSection={<IconSearch size={16} />}
374+
value={search}
375+
onChange={(e) => setSearch(e.target.value)}
376+
/>
377+
```
378+
379+
---
380+
381+
## 9. Padrões de dados e lógica
382+
383+
### 9.1 Store — async action
384+
385+
Template exato que todas as actions seguem. Imports do service como namespace:
386+
387+
```javascript
388+
import { create } from 'zustand'
389+
import * as feedsService from '../services/feeds.js'
390+
391+
export const useFeedStore = create((set) => ({
392+
feeds: [],
393+
loading: false,
394+
error: null,
395+
396+
fetchFeeds: async () => {
397+
set({ loading: true, error: null })
398+
try {
399+
const feeds = await feedsService.getAll()
400+
set({ feeds, loading: false })
401+
} catch (error) {
402+
set({ error: error.message, loading: false })
403+
throw error
404+
}
405+
},
406+
}))
407+
```
408+
409+
Nota: services são importados como `import * as service` (namespace), não como objeto exportado.
410+
411+
### 9.2 Store — mutação de listas
412+
413+
Atualizações imutáveis via `set(state => ...)` com spread + map/filter:
414+
415+
```javascript
416+
// Atualizar item na lista
417+
updateFeed: (id, data) => set((state) => ({
418+
feeds: state.feeds.map((f) => (f.id === id ? { ...f, ...data } : f))
419+
})),
420+
421+
// Remover item
422+
removeFeed: (id) => set((state) => ({
423+
feeds: state.feeds.filter((f) => f.id !== id)
424+
})),
425+
426+
// Adicionar item
427+
addFeed: (feed) => set((state) => ({
428+
feeds: [...state.feeds, feed]
429+
})),
430+
```
431+
432+
### 9.3 Notifications
433+
434+
Shape e convenção de cores consistente em toda a app:
435+
436+
```javascript
437+
import { notifications } from '@mantine/notifications'
438+
import { IconCheck, IconX, IconAlertCircle } from '@tabler/icons-react'
439+
440+
// ✅ Sucesso
441+
notifications.show({
442+
title: t('common.success'),
443+
message: t('namespace.savedMessage'),
444+
color: 'green',
445+
icon: <IconCheck size={18} />,
446+
})
447+
448+
// ✅ Erro
449+
notifications.show({
450+
title: t('common.error'),
451+
message: error.message,
452+
color: 'red',
453+
icon: <IconX size={18} />,
454+
})
455+
456+
// ✅ Warning
457+
notifications.show({
458+
title: t('common.warning'),
459+
message: t('namespace.warningMessage'),
460+
color: 'yellow',
461+
icon: <IconAlertCircle size={18} />,
462+
})
463+
```
464+
465+
Icones de notification: sempre `size={18}`. Mensagens de erro: usa `error.message` diretamente (já vem traduzido do backend).
466+
467+
### 9.4 dayjs + i18n
468+
469+
Locale do dayjs sincroniza com o idioma do i18n:
470+
471+
```javascript
472+
import dayjs from 'dayjs'
473+
import relativeTime from 'dayjs/plugin/relativeTime'
474+
import { useTranslation } from 'react-i18next'
475+
476+
dayjs.extend(relativeTime)
477+
478+
const MyComponent = () => {
479+
const { i18n } = useTranslation()
480+
481+
useEffect(() => {
482+
dayjs.locale(i18n.language)
483+
}, [i18n.language])
484+
485+
// Formatos usados no projeto:
486+
// dayjs(date).format('DD MMM, HH:mm') — compacto com hora
487+
// dayjs(date).format('DD/MM/YYYY [at] HH:mm') — completo
488+
// dayjs(date).fromNow() — relativo ("há 2 dias")
489+
}
490+
```
491+
492+
---
493+
494+
## 10. Padrões de fluxo
495+
496+
### 10.1 Route Guard (wrapper)
497+
498+
Componente que protege rotas verificando estado do store:
499+
500+
```javascript
501+
import { Navigate } from 'react-router-dom'
502+
import { useFeedStore } from '../store/feed-store.js'
503+
504+
const RequireFeed = ({ children }) => {
505+
const activeFeed = useFeedStore((s) => s.activeFeed)
506+
if (!activeFeed) return <Navigate to="/" />
507+
return children
508+
}
509+
```
510+
511+
Usado na definição de rotas: `<RequireFeed><EditorPage /></RequireFeed>`
512+
513+
### 10.2 Notificação via URL params
514+
515+
Após redirects externos (OAuth, Stripe checkout), status vem via query params:
516+
517+
```javascript
518+
import { useSearchParams } from 'react-router-dom'
519+
520+
const SubscriptionPage = () => {
521+
const [searchParams, setSearchParams] = useSearchParams()
522+
const { t } = useTranslation()
523+
524+
useEffect(() => {
525+
if (searchParams.get('success')) {
526+
notifications.show({ title: t('common.success'), message: t('subscription.successMessage'), color: 'green', icon: <IconCheck size={18} /> })
527+
setSearchParams({})
528+
} else if (searchParams.get('canceled')) {
529+
notifications.show({ title: t('common.warning'), message: t('subscription.canceledMessage'), color: 'yellow', icon: <IconAlertCircle size={18} /> })
530+
setSearchParams({})
531+
}
532+
}, [searchParams])
533+
}
534+
```
535+
536+
### 10.3 Autosave
537+
538+
Padrão usado no editor — debounce com state machine de status:
539+
540+
```javascript
541+
const [saveStatus, setSaveStatus] = useState('idle') // 'idle' | 'saving' | 'saved'
542+
543+
useEffect(() => {
544+
if (!content) return
545+
const timeout = setTimeout(async () => {
546+
setSaveStatus('saving')
547+
try {
548+
await postsService.update(postId, { content })
549+
setSaveStatus('saved')
550+
// Reset para idle após 3s
551+
setTimeout(() => setSaveStatus('idle'), 3000)
552+
} catch {
553+
setSaveStatus('idle')
554+
}
555+
}, 2000) // debounce de 2s
556+
557+
return () => clearTimeout(timeout)
558+
}, [content, postId])
559+
```
560+
561+
---
562+
563+
## 11. Convenção de tamanhos de icones
564+
565+
Hierarquia consistente — sempre de `@tabler/icons-react`:
566+
567+
| Contexto | Size | Exemplo |
568+
|---|---|---|
569+
| Menu items / nav | 14 | Sidebar links |
570+
| Inline / badges | 16 | Botões, labels, leftSection |
571+
| Notifications | 18 | Icons nas notifications |
572+
| Card headers | 20 | Ação principal do card |
573+
| Feature cards | 24 | Cards de destaque |
574+
| Empty states | 48 | Icone do empty state (com `stroke={1}`) |
575+
576+
Empty states usam `stroke={1}` para parecer mais leve. Icones decorativos genéricos usam `stroke={1.5}`.
577+
578+
---
579+
249580
## Related Skills
250581
251582
- @[.agent/skills/riligar-design-system]
252-
- @[.agent/skills/riligar-dev-stack]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Dependências Frontend
2+
3+
## Core
4+
5+
| Pacote | Versão | Descrição |
6+
|---|---|---|
7+
| `react` | ^19.x | Biblioteca UI |
8+
| `react-dom` | ^19.x | React DOM renderer |
9+
| `react-router-dom` | ^7.x | Roteamento |
10+
| `vite` | ^5.x | Build tool |
11+
| `zustand` | ^5.x | Gerenciamento de estado |
12+
| `ky` | ^1.x | HTTP client |
13+
14+
## UI
15+
16+
| Pacote | Versão | Descrição |
17+
|---|---|---|
18+
| `@mantine/core` | ^8.x | Componentes UI |
19+
| `@mantine/hooks` | ^8.x | Hooks utilitários |
20+
| `@mantine/form` | ^8.x | Gerenciamento de formulários |
21+
| `@mantine/notifications` | ^8.x | Sistema de notificações |
22+
| `@tabler/icons-react` | ^3.x | Iconografia |
23+
24+
> Use apenas Mantine para estilização. Sem CSS Modules, Custom CSS ou CSS In-line.
25+
26+
## Configuração Vite
27+
28+
```javascript
29+
import { defineConfig } from 'vite'
30+
import react from '@vitejs/plugin-react'
31+
32+
export default defineConfig({
33+
plugins: [react()],
34+
build: {
35+
lib: {
36+
entry: 'src/index.js',
37+
formats: ['es', 'cjs'],
38+
},
39+
rollupOptions: {
40+
external: ['react', 'react-dom', '@mantine/core'],
41+
},
42+
},
43+
})
44+
```

0 commit comments

Comments
 (0)