From mozilor
Mandatory coding standards for the Webtoffee Marketing Suite. Invoke before writing any code. Covers Tailwind sf- prefix rules, TypeScript conventions, component patterns, atomic design placement, form/state/routing/API patterns, and do-nots.
How this skill is triggered — by the user, by Claude, or both
Slash command
/mozilor:coding-standardsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
All Tailwind utility classes **must** use the `sf-` prefix.
All Tailwind utility classes must use the sf- prefix.
// ✅ Correct
<div className="sf-flex sf-items-center sf-gap-4 sf-text-sm sf-font-medium">
// ❌ Wrong — will not work
<div className="flex items-center gap-4 text-sm font-medium">
This applies to !important overrides on shadcn/ui components too: !sf-h-10, not !h-10.
Custom color tokens and design system are defined in tailwind.config.ts. Use those tokens — do not use arbitrary color values. For Figma color mapping, invoke the figma-to-code skill.
.ts or .tsx — no .jsany — use proper types or unknowninterface or typetypes.ts file in the same directoryPascalCase.tsx (e.g., CampaignCard.tsx)camelCase.ts prefixed with use (e.g., useCampaignData.ts)camelCase.ts suffixed with Service (e.g., campaignService.ts)camelCase.ts suffixed with Store (e.g., campaignStore.ts)import { ... } from 'react'
import { ... } from '@/components/ui/...'
import { ... } from '@/atoms/...'
// ... other imports
interface ComponentNameProps {
// props
}
export function ComponentName({ prop1, prop2 }: ComponentNameProps) {
// hooks first
// derived state
// handlers
// render
return (...)
}
Always use React Hook Form + Zod:
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
name: z.string().min(1, 'Required'),
})
type FormData = z.infer<typeof schema>
export function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
})
// ...
}
Zod schemas for reusable forms go in src/schemas/.
// Store pattern
import { create } from 'zustand'
interface FeatureStore {
data: Item[]
isLoading: boolean
fetchData: () => Promise<void>
}
export const useFeatureStore = create<FeatureStore>((set) => ({
data: [],
isLoading: false,
fetchData: async () => {
set({ isLoading: true })
const result = await featureService.getAll()
set({ data: result, isLoading: false })
},
}))
Use persist middleware only when state must survive page refresh.
Never put Zustand store state in a useCallback dep array if that state is also reset or modified by the same effect or its siblings. This causes infinite loops:
useEffect → resetX() → state changes → useCallback recreates → useEffect re-runs → loop
Rules:
useCallback or useEffect dep arrays (setX, fetchX, resetX, etc. never change identity).useXxxStore.getState().value instead. This breaks the dep chain without creating a stale closure.useEffect that calls both a reset action AND a load action — the load function must NOT depend on the state being reset.// ❌ WRONG — discounts in dep array causes infinite loop when resetDiscounts() clears it
const load = useCallback(async () => {
const ids = new Set(discounts.map(d => d.id)) // closes over discounts
...
}, [discounts, filters]) // discounts here = infinite loop if reset fires
useEffect(() => {
resetDiscounts() // sets discounts: [] ← triggers load recreation ← triggers effect ← loop
load()
}, [filters, load])
// ✅ CORRECT — read store state at call time, not via closure
const load = useCallback(async () => {
const ids = new Set(useFeatureStore.getState().items.map(d => d.id)) // getState() = stable
...
}, [filters]) // no store state in deps
useEffect(() => {
resetItems()
load()
}, [filters, load]) // safe — load only recreates when filters changes
All catch blocks must show a destructive toast:
toast({ variant: "destructive", title: "...", description: "..." })
Never silently swallow errors or use console.error alone. Async store actions called from event handlers must be awaited or have .catch().
// src/services/featureService.ts
import { restClient } from '@/helpers/restClient'
export const featureService = {
getAll: () => restClient.get<Item[]>('/api/feature'),
create: (payload: CreatePayload) => restClient.post<Item>('/api/feature', payload),
update: (id: string, payload: UpdatePayload) => restClient.put<Item>(`/api/feature/${id}`, payload),
delete: (id: string) => restClient.delete(`/api/feature/${id}`),
}
Never call restClient directly from components or stores — always go through a service.
// src/routes/(dashboard)/feature/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { FeatureTemplate } from '@/templates/FeatureTemplate'
export const Route = createFileRoute('/(dashboard)/feature/')({
component: FeatureTemplate,
})
Always use the @/ alias:
// ✅
import { Button } from '@/components/ui/button'
import { useAuthStore } from '@/stores/authStore'
// ❌
import { Button } from '../../../components/ui/button'
Add data-testid attributes to all interactive elements and key UI elements. Use kebab-case: data-testid="campaign-create-button".
sf- prefixreact-router-dom directly — use TanStack Routerconsole.log in committed codeany typesrc/ — always use @/useCallback/useEffect dep arrays if that state is mutated by the same effect — use useXxxStore.getState().value instead to read without subscribing (prevents infinite loops)set*, fetch*, reset* actions are always stable referencesnpx claudepluginhub mozilor-technologies/mozilor-skills --plugin workflowCreates bite-sized, testable implementation plans from specs or requirements, with file structure and task decomposition. Activates before coding multi-step tasks.