@@ -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]
0 commit comments