Get the FREE Ultimate OpenClaw Setup Guide →

riligar-dev-dashboard

npx machina-cli add skill riligar/agents-kit/riligar-dev-dashboard --openclaw
Files (1)
SKILL.md
16.7 KB

Front-End — Padrões do RiLiGar

Regras concretas baseadas na arquitetura real dos projetos. Não são genéricas — são do código que existe.


Referências obrigatórias

[!IMPORTANT] Sempre respeite também:

  • @[.agent/skills/riligar-design-system] — UI exclusivo via Mantine, zero CSS
  • Rules em .agent/rules/ — clean-code, naming-conventions, code-style, javascript-only
  • references/dependencies.md — Pacotes e versões do frontend, config Vite

1. Estrutura de arquivos

src/
├── components/          # Componentes reutilizáveis
│   ├── Sidebar.jsx      # PascalCase (SEMPRE)
│   ├── RichEditor.jsx
│   └── MediaLibrary.jsx
├── pages/               # Uma pasta por página/feature
│   ├── home.jsx         # Arquivo raiz da página: kebab-case
│   ├── editor/
│   │   └── index.jsx
│   └── feeds/
│       ├── index.jsx
│       └── FeedConfig.jsx   # Sub-componentes: PascalCase
├── store/               # Zustand stores — um arquivo por domínio
│   ├── auth-store.js
│   ├── feed-store.js
│   └── post-store.js
├── services/            # Chamadas HTTP — um arquivo por domínio
│   ├── api.js           # Instância base do cliente HTTP
│   ├── feeds.js
│   └── posts.js
├── constants/           # Constantes estáticas do app
├── i18n/                # Internacionalização
│   ├── index.js
│   └── locales/
└── hooks/               # Custom hooks compartilhados

Regras de naming de arquivos

TipoConvençãoExemplo
Componentes (reutilizáveis)PascalCaseMediaLibrary.jsx
Página raizkebab-casehome.jsx, index.jsx
Sub-componentes de páginaPascalCaseFeedConfig.jsx
Storeskebab-case + sufixo -storefeed-store.js
Serviceskebab-casefeeds.js
HookscamelCase com prefixo useuseDebounce.js

2. Estado — Zustand (não é opcional)

Estado global sempre Zustand. Sem Context para estado compartilhado. Sem Redux.

Padrão de store

import { create } from 'zustand'
import { feedsService } from '../services/feeds'

const useFeedStore = create((set, get) => ({
    feeds: [],
    activeFeed: null,
    isLoading: false,

    fetchFeeds: async () => {
        set({ isLoading: true })
        const feeds = await feedsService.getAll()
        set({ feeds, isLoading: false })
    },

    setActiveFeed: (feed) => set({ activeFeed: feed }),
}))

Regras

  • Sem getters JavaScript (get isTrialing() {}) — não são reativos no Zustand. Derive no componente ou use um selector.
  • Persistência: use persist middleware apenas quando necessário (ex: activeFeed).
  • Sem lógica de UI dentro do store. Stores são dados + chamadas de API.
  • Selectors granulares: useStore((state) => state.feeds) — não useStore() inteiro.

Errado vs Certo

// ❌ Getter não-reativo — nunca vai atualizar o componente
const useSubscriptionStore = create((set) => ({
    subscription: null,
    get isTrialing() { return this.subscription?.status === 'trialing' }
}))

// ✅ Derive no componente
const subscription = useSubscriptionStore((s) => s.subscription)
const isTrialing = subscription?.status === 'trialing'

3. i18n — Todas as strings visíveis devem usar t()

O projeto usa i18next com 3 idiomas (pt-BR, en, es). Nenhuma string visível ao usuário pode ser hardcoded.

Como usar

import { useTranslation } from 'react-i18next'

const MyComponent = () => {
    const { t } = useTranslation()

    return (
        <Button onClick={handleSave}>{t('common.save')}</Button>
    )
}

Regras

  • Sempre importar useTranslation antes de usar t(). Sem isso, crash em runtime.
  • Strings de UI: t('namespace.key') — nunca hardcode.
  • Strings técnicas (logs, variável interna): podem ser em inglês, não precisam de t().
  • Chaves de namespace: domínio da feature (feeds., posts., media.) + common. para compartilhados.
  • Mensagens de erro da API já vêm traduzidas do backend — não duplique.

Errado

// ❌ String hardcoded visível ao usuário
<Button>Salvar Alterações</Button>
<Text>Nenhuma mídia encontrada.</Text>

// ❌ t() sem import — crash
const handleSave = async () => {
    showNotification({ title: t('common.success') }) // ReferenceError
}

Certo

// ✅
import { useTranslation } from 'react-i18next'

const { t } = useTranslation()
<Button>{t('common.save')}</Button>
<Text>{t('media.empty')}</Text>

4. Services — camada de HTTP

Todas as chamadas de API vão por services/. Componentes e stores não chamam HTTP diretamente.

Padrão

// services/feeds.js
import { api } from './api'

export const feedsService = {
    getAll: () => api.get('feeds').json(),
    getById: (id) => api.get(`feeds/${id}`).json(),
    create: (data) => api.post('feeds', { json: data }).json(),
    update: (id, data) => api.put(`feeds/${id}`, { json: data }).json(),
    remove: (id) => api.delete(`feeds/${id}`).json(),
}
  • api.js tem a instância base com token de auth e tratamento de erro.
  • Não duplique API_URL — já está no api.js. Nunca redefina em outro arquivo.
  • Services são funções puras de chamada HTTP. Sem lógica de estado.

5. Componentes — regras práticas

Composição

  • Um componente, uma responsabilidade.
  • Se um componente ultrapassar ~100 linhas, divide.
  • Props down, events up. Sem drilling profundo — usa store.

Interação com usuário

  • Confirmação de exclusão: sempre usar HoldButton / ButtonDelete do components/Buttons.jsx. Nunca usar confirm() nativo.
  • Hover/focus: usar props do Mantine (styles, withBorder, hover via &:hover no styles). Nunca manipular DOM diretamente via e.currentTarget.style.
// ❌ Manipulação direta de DOM
onMouseEnter={(e) => e.currentTarget.style.background = '#eee'}

// ✅ Mantine styles prop
styles={{ root: { '&:hover': { backgroundColor: 'var(--mantine-color-dimmed)' } } }}

Não redefina componentes do Mantine

Se você precisa de um Center, TextInput, Button — use o do Mantine. Nunca crie um local com o mesmo nome, isso gera shadow e confusão.


6. Constantes e valores fixos

  • Strings de config (phone numbers, mensagens template, URLs externas) vão em constants/ ou .env.
  • Nunca hardcode dentro de componentes.
// ❌
const message = `Olá, preciso de ajuda com meu plano.`

// ✅ constants/whatsapp.js
export const WHATSAPP_SUPPORT_NUMBER = '...'
export const WHATSAPP_SUPPORT_MESSAGE = '...' // ou via i18n se traduzível

7. Anti-patterns (do código real)

❌ Problema real encontrado✅ Como resolver
slugify duplicado em 2 arquivosExtrair para utils/slugify.js
stripHtml duplicado em 2 arquivosExtrair para utils/stripHtml.js
API_URL redefinido fora do api.jsImportar do services/api.js
t() usado sem useTranslation importadoSempre verificar import
Componente Mantine shadowed por localDeletar o local, usar Mantine
Código comentado espalhadoDeletar. Se precisa, usa git.
confirm() misturado com HoldButtonUsa HoldButton sempre
<style> tag com CSS rawUsa styles prop do Mantine (exceção: libs externas como ProseMirror que precisam de CSS global)
Getters no Zustand storeDerive no componente
onMouseEnter manipulando style diretamenteUsa styles prop do Mantine

8. Padrões reutilizáveis

Estruturas que repetem pela codebase. Copie o esqueleto, ajuste apenas o conteúdo domínio-específico.

8.1 Page Header

Presente em todas as pages. Estrutura idêntica sempre:

<Box py="xl">
    <Group justify="space-between" align="flex-end" mb="xl">
        <Stack gap={0}>
            <Text size="xs" fw={700} c="dimmed" tt="uppercase" lts="0.1em">
                {t('namespace.subtitle')}
            </Text>
            <Title order={1} style={{ letterSpacing: '-0.04em' }}>
                {t('namespace.title')}
            </Title>
        </Stack>
        {/* CTA opcional — ex: <Button leftSection={<IconPlus size={16} />}> */}
    </Group>
    {/* Conteúdo da página */}
</Box>

8.2 Empty State

Usado quando uma lista está vazia. Card com borda dashed, icone grande, texto e CTA:

<Card padding="xl" radius="md" withBorder style={{ borderStyle: 'dashed', textAlign: 'center' }}>
    <Stack align="center" py="xl">
        <IconDominio size={48} stroke={1} color="var(--mantine-color-gray-2)" />
        <Text c="dimmed" size="sm">{t('namespace.emptyMessage')}</Text>
        <Button onClick={handleCreate} leftSection={<IconPlus size={16} />}>
            {t('namespace.createFirst')}
        </Button>
    </Stack>
</Card>

8.3 Loading Guard

Loader só aparece quando não há dados ainda (não sobrescreve lista existente):

{loading && data.length === 0 ? (
    <Center style={{ height: 300 }}>
        <Loader />
    </Center>
) : (
    /* conteúdo normal */
)}

8.4 Card Grid

Layout responsivo padrão para listas de cards:

<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }}>
    {items.map((item) => (
        <Card key={item.id} padding="lg" radius="md" withBorder>
            {/* conteúdo do card */}
        </Card>
    ))}
</SimpleGrid>

Para galerias (mais itens pequenos): cols={{ base: 1, sm: 2, md: 3, lg: 4 }}

8.5 Modal

Sempre usa useDisclosure. Header com borderBottom específico:

const [opened, { open, close }] = useDisclosure(false)

<Modal
    opened={opened}
    onClose={close}
    centered
    radius="md"
    padding="xl"
    title={<Text fw={700}>{t('namespace.modalTitle')}</Text>}
    styles={{ header: { borderBottom: '1px solid var(--mantine-color-gray-2)', marginBottom: 20 } }}
>
    {/* corpo do modal */}
</Modal>

Para múltiplos modais na mesma page, renomeia as funções: { open: openEdit, close: closeEdit }

8.6 Status Badge

Mapeia status → configuração visual via função que recebe t:

const getStatusConfig = (t) => ({
    draft:     { color: 'gray',   icon: <IconCircleDotted size={16} />, label: t('posts.status.draft') },
    scheduled: { color: 'blue',   icon: <IconClock size={16} />,        label: t('posts.status.scheduled') },
    published: { color: 'green',  icon: <IconCircleCheck size={16} />,  label: t('posts.status.published') },
    failed:    { color: 'red',    icon: <IconCircleX size={16} />,      label: t('posts.status.failed') },
})

// Uso
const config = getStatusConfig(t)[status]
<Badge variant="dot" color={config.color}>{config.label}</Badge>

8.7 Search / Filter

Filtro local sem chamada de API — estado local + filter inline:

const [search, setSearch] = useState('')

const filtered = items.filter((item) =>
    item.name.toLowerCase().includes(search.toLowerCase())
)

<TextInput
    placeholder={t('common.search')}
    leftSection={<IconSearch size={16} />}
    value={search}
    onChange={(e) => setSearch(e.target.value)}
/>

9. Padrões de dados e lógica

9.1 Store — async action

Template exato que todas as actions seguem. Imports do service como namespace:

import { create } from 'zustand'
import * as feedsService from '../services/feeds.js'

export const useFeedStore = create((set) => ({
    feeds: [],
    loading: false,
    error: null,

    fetchFeeds: async () => {
        set({ loading: true, error: null })
        try {
            const feeds = await feedsService.getAll()
            set({ feeds, loading: false })
        } catch (error) {
            set({ error: error.message, loading: false })
            throw error
        }
    },
}))

Nota: services são importados como import * as service (namespace), não como objeto exportado.

9.2 Store — mutação de listas

Atualizações imutáveis via set(state => ...) com spread + map/filter:

// Atualizar item na lista
updateFeed: (id, data) => set((state) => ({
    feeds: state.feeds.map((f) => (f.id === id ? { ...f, ...data } : f))
})),

// Remover item
removeFeed: (id) => set((state) => ({
    feeds: state.feeds.filter((f) => f.id !== id)
})),

// Adicionar item
addFeed: (feed) => set((state) => ({
    feeds: [...state.feeds, feed]
})),

9.3 Notifications

Shape e convenção de cores consistente em toda a app:

import { notifications } from '@mantine/notifications'
import { IconCheck, IconX, IconAlertCircle } from '@tabler/icons-react'

// ✅ Sucesso
notifications.show({
    title: t('common.success'),
    message: t('namespace.savedMessage'),
    color: 'green',
    icon: <IconCheck size={18} />,
})

// ✅ Erro
notifications.show({
    title: t('common.error'),
    message: error.message,
    color: 'red',
    icon: <IconX size={18} />,
})

// ✅ Warning
notifications.show({
    title: t('common.warning'),
    message: t('namespace.warningMessage'),
    color: 'yellow',
    icon: <IconAlertCircle size={18} />,
})

Icones de notification: sempre size={18}. Mensagens de erro: usa error.message diretamente (já vem traduzido do backend).

9.4 dayjs + i18n

Locale do dayjs sincroniza com o idioma do i18n:

import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useTranslation } from 'react-i18next'

dayjs.extend(relativeTime)

const MyComponent = () => {
    const { i18n } = useTranslation()

    useEffect(() => {
        dayjs.locale(i18n.language)
    }, [i18n.language])

    // Formatos usados no projeto:
    // dayjs(date).format('DD MMM, HH:mm')       — compacto com hora
    // dayjs(date).format('DD/MM/YYYY [at] HH:mm') — completo
    // dayjs(date).fromNow()                      — relativo ("há 2 dias")
}

10. Padrões de fluxo

10.1 Route Guard (wrapper)

Componente que protege rotas verificando estado do store:

import { Navigate } from 'react-router-dom'
import { useFeedStore } from '../store/feed-store.js'

const RequireFeed = ({ children }) => {
    const activeFeed = useFeedStore((s) => s.activeFeed)
    if (!activeFeed) return <Navigate to="/" />
    return children
}

Usado na definição de rotas: <RequireFeed><EditorPage /></RequireFeed>

10.2 Notificação via URL params

Após redirects externos (OAuth, Stripe checkout), status vem via query params:

import { useSearchParams } from 'react-router-dom'

const SubscriptionPage = () => {
    const [searchParams, setSearchParams] = useSearchParams()
    const { t } = useTranslation()

    useEffect(() => {
        if (searchParams.get('success')) {
            notifications.show({ title: t('common.success'), message: t('subscription.successMessage'), color: 'green', icon: <IconCheck size={18} /> })
            setSearchParams({})
        } else if (searchParams.get('canceled')) {
            notifications.show({ title: t('common.warning'), message: t('subscription.canceledMessage'), color: 'yellow', icon: <IconAlertCircle size={18} /> })
            setSearchParams({})
        }
    }, [searchParams])
}

10.3 Autosave

Padrão usado no editor — debounce com state machine de status:

const [saveStatus, setSaveStatus] = useState('idle') // 'idle' | 'saving' | 'saved'

useEffect(() => {
    if (!content) return
    const timeout = setTimeout(async () => {
        setSaveStatus('saving')
        try {
            await postsService.update(postId, { content })
            setSaveStatus('saved')
            // Reset para idle após 3s
            setTimeout(() => setSaveStatus('idle'), 3000)
        } catch {
            setSaveStatus('idle')
        }
    }, 2000) // debounce de 2s

    return () => clearTimeout(timeout)
}, [content, postId])

11. Convenção de tamanhos de icones

Hierarquia consistente — sempre de @tabler/icons-react:

ContextoSizeExemplo
Menu items / nav14Sidebar links
Inline / badges16Botões, labels, leftSection
Notifications18Icons nas notifications
Card headers20Ação principal do card
Feature cards24Cards de destaque
Empty states48Icone do empty state (com stroke={1})

Empty states usam stroke={1} para parecer mais leve. Icones decorativos genéricos usam stroke={1.5}.


Related Skills

  • @[.agent/skills/riligar-design-system]

Source

git clone https://github.com/riligar/agents-kit/blob/prod/.agent/skills/riligar-dev-dashboard/SKILL.mdView on GitHub

Overview

Este skill define padrões concretos para o frontend RiLiGar, incluindo organização de arquivos, uso de Zustand para estado global e i18n para internacionalização. As regras não são genéricas: refletem a arquitetura real dos projetos RiLiGar e a integração com o design system Mantine. Segue convenções de nomenclatura, estrutura de stores e práticas de composição de componentes.

How This Skill Works

Organiza o código em src/ com componentes reutilizáveis, páginas, stores (Zustand), serviços e i18n. O estado global é gerenciado por stores por domínio, sem lógica de UI dentro dos stores, e com selectors granulares. Todas as strings visíveis devem usar i18n via useTranslation e t(), com importações apropriadas.

When to Use It

  • Ao construir componentes reutilizáveis que consomem estado global.
  • Ao gerenciar dados de domínio (autenticação, feeds, posts) com Zustand.
  • Ao implementar UI com uma estrutura de pastas clara e consistente.
  • Ao adicionar suporte multilíngue com i18n e evitar strings hardcoded.
  • Ao criar novos recursos seguindo as convenções de nomenclatura e organização do projeto.

Quick Start

  1. Step 1: Estruture o projeto seguindo src/components, src/pages, src/store, src/i18n.
  2. Step 2: Crie um store Zustand por domínio (ex.: feed-store.js) baseado no exemplo.
  3. Step 3: Adicione i18n: configure useTranslation e substitua strings por t().

Best Practices

  • Use Zustand como estado global; evite Context ou Redux.
  • Não inclua lógica de UI dentro das stores; mantenha apenas dados e chamadas de API.
  • Crie selectors granulares (state => state.x) para reatividade eficiente.
  • Siga naming conventions: Componentes em PascalCase, páginas em kebab-case, stores em kebab-case com -store.
  • Referencie dependências obrigatórias: riligar-design-system, rules e dependencies.md.

Example Use Cases

  • store/ feed-store.js com fetchFeeds e setActiveFeed (exemplo do padrão).
  • auth-store.js gerenciando autenticação com persistência opcional.
  • MediaLibrary.jsx como componente reutilizável seguindo PascalCase.
  • home.jsx como página raiz em kebab-case.
  • useTranslation em UI: t('common.save').

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers