Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 83 additions & 56 deletions .agent/rules/frontend/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Guidelines for frontend TypeScript and React development, including component st
- Use React Aria Components from `@repo/ui/components/ComponentName`:
- Search [Components](/application/shared-webapp/ui/components) when you need to find a component
- Use existing components rather than creating new ones
- Use `onPress` instead of `onClick` for event handlers
- Use `onPress` instead of `onClick` for event handlers (exception: Dialog close button uses `onClick={close}` from render prop)
- Use `onAction` for menu items and list actions
- Use `<Trans>...</Trans>` for JSX translations, `t` macro for strings
- Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()`
Expand All @@ -58,6 +58,9 @@ Guidelines for frontend TypeScript and React development, including component st
- Colocate state with components—don't lift state unnecessarily
- Use `useCallback` and `useMemo` only for proven performance issues
- Throw errors sparingly and ensure error messages include a period
- Include appropriate aria labels for accessibility (e.g., `slot="title"` on Heading in dialogs)
- Disable UI during pending operations: `isDisabled={mutation.isPending}` on buttons/fields, `isDismissable={!mutation.isPending}` on modals
- Dialog sizing: `sm:w-dialog-md` (simple), `sm:w-dialog-lg` (4-6 fields), `sm:w-dialog-xl` (complex), `sm:w-dialog-2xl` (extra-large)

3. Error handling:
- **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors)
Expand All @@ -69,79 +72,103 @@ Guidelines for frontend TypeScript and React development, including component st
- Use `isTouchDevice()` for touch vs mouse interactions
- Use `isMediumViewportOrLarger()` for desktop-specific features

5. Always follow these steps when implementing changes:
5. Z-index layering for fixed-position elements (don't invent new values):
- `z-0` to `z-20`: Content layers (sticky headers, table headers)
- `z-30` to `z-40`: Navigation (top bar, mobile header)
- `z-60`: Side menu collapsed
- `z-70`: Side panes (backdrop at `z-[65]`)
- `z-80`: Side menu expanded in overlay mode (backdrop at `z-[75]`)
- `z-90`: Modal dialogs
- `z-100`: High priority modals (nested, confirmations)
- `z-[150]`: Toasts (always visible for user feedback)
- `z-[200]`: Mobile full-screen menus
- Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context

6. Always follow these steps when implementing changes:
- Consult relevant rule files and list which ones guided your implementation
- Search the codebase for similar code before implementing new code
- Reference existing implementations to maintain consistency

6. Build and format your changes:
7. Build and format your changes:
- After each minor change, use the **execute MCP tool** with `command: "build"` for frontend
- This ensures consistent code style across the codebase

7. Verify your changes:
8. Verify your changes:
- When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect**
- Fix any compiler warnings or test failures before proceeding

## Examples

### Example 1 - Component Structure

```tsx
// ✅ DO: Create focused components with clear responsibilities
import { Trans } from "@lingui/react/macro";
import { Avatar } from "@repo/ui/components/Avatar";
import { Button } from "@repo/ui/components/Button";
import { Menu, MenuHeader, MenuItem, MenuSeparator } from "@repo/ui/components/Menu";
import { LogOutIcon, UserIcon } from "lucide-react";

export function AvatarMenu({ userInfo, onProfileClick, onLogoutClick }: AvatarMenuProps) {
// ✅ DO: Correct patterns
export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) {
const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen });
const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline

const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern

return (
<Menu placement="bottom end">
<MenuHeader>
<div className="flex flex-row items-center gap-2">
<Avatar avatarUrl={userInfo.avatarUrl} initials={userInfo.initials} isRound={true} size="sm" />
<div className="my-1 flex flex-col">
<h2>{userInfo.fullName}</h2>
<p className="text-muted-foreground">{userInfo.title ?? userInfo.email}</p>
</div>
</div>
</MenuHeader>
<MenuItem onAction={onProfileClick}>
<UserIcon className="h-4 w-4" />
<Trans>Profile</Trans>
</MenuItem>
<MenuSeparator />
<MenuItem onAction={onLogoutClick}>
<LogOutIcon className="h-4 w-4" />
<Trans>Log out</Trans>
</MenuItem>
</Menu>
<Modal isOpen={isOpen} onOpenChange={onOpenChange} isDismissable={!isPending}> // ✅ Prevent dismiss during pending
<Dialog className="sm:w-dialog-md"> // ✅ Use dialog width classes (not max-w-lg)
{({ close }) => ( // ✅ Dialog render prop provides close function
<>
<XIcon onClick={close} className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" /> // ✅ Close button pattern (onClick is exception)
<DialogHeader description={t`Select users from the list.`}>
<Heading slot="title" className="text-2xl"><Trans>Select users</Trans></Heading>
</DialogHeader>
<DialogContent>
<ListBox aria-label={t`Users`} selectionMode="multiple" onSelectionChange={handleChangeSelection}>
{activeUsers.map((user) => (
<ListBoxItem key={user.id} id={user.id}>
<Text>{`${user.firstName} ${user.lastName}`}</Text>
</ListBoxItem>
))}
</ListBox>
</DialogContent>
<DialogFooter>
<Button variant="primary" onPress={handleConfirm} isPending={isPending}> // ✅ Use isPending for loading
<Trans>Confirm</Trans>
</Button>
</DialogFooter>
</>
)}
</Dialog>
</Modal>
);
}

// ❌ DON'T: Mix unrelated functionality in a single component
function BadAvatarMenu({ userInfo }) { // Bad: Mixing menu with logout functionality
// ❌ DON'T: Common anti-patterns
function BadUserDialog({ users, selectedId, isOpen, onClose }) {
const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values
const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated

useEffect(() => { // ❌ useEffect for calculations - compute inline instead
setFilteredUsers(users.filter(u => u.isActive));
setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types
}, [users, selectedId]);

const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need
return `${user.firstName} ${user.lastName}`;
}, []);

const handleSelect = (id) => console.log(id); // ❌ "handle" + noun (use handleSelectUser), console.log

return (
<div className="menu"> // ❌ DON'T: Use CSS styles instead of Tailwind
<div className="header">
<div className="flex flex-row items-center gap-2"> // ❌ Unnecessary nested <div>
<div className="m-12">
<img src={userInfo.avatarUrl} alt="User avatar" /> // ❌ DON'T: Use native <img>
<h2>{userInfo.fullName}</h2>
</div>
</div>
</div>
<button onClick={() => showProfile()}> // ❌ DON'T: Use native <button>
<i className="icon-user"></i> Profile
</button>
<button onClick={() => {
// ❌ DON'T: Implement logout logic directly in component or call fetch directly
fetch("/api/account-management/authentication/logout", { method: "POST" })
.then(() => window.location.href = "/login");
}}>
<i className="icon-logout"></i> Log out
</button>
</div>
<Modal isOpen={isOpen} onOpenChange={onClose}> // ❌ Missing isDismissable={!isPending}
<Dialog className="sm:max-w-lg bg-white"> // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background)
<h1>User Mgmt</h1> // ❌ Native <h1> (use Heading), acronym "Mgmt", missing <Trans>
<ul> // ❌ Native <ul> - use ListBox
{filteredUsers.map(user => (
<li key={user.id} onClick={() => handleSelect(user.id)}> // ❌ Native <li>, onClick (use onAction)
<img src={user.avatarUrl} /> // ❌ Native <img> - use Avatar
<Text className="text-sm">{user.email}</Text> // ❌ text-sm with Text causes blur
{getDisplayName(user)}
</li>
))}
</ul>
<Button onPress={handleSelect}>Submit</Button> // ❌ Missing isDisabled/isPending, missing <Trans>
</Dialog>
</Modal>
);
}
```
88 changes: 0 additions & 88 deletions .agent/rules/frontend/modal-dialog.md

This file was deleted.

2 changes: 1 addition & 1 deletion .claude/hooks/pre-tool-use-bash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ cmd=$(echo "$input" | sed -n 's/.*"command":"\([^"]*\)".*/\1/p')

# Check the command and decide whether to block it
case "$cmd" in
*"git merge"*|*"git rebase"*|*"git cherry-pick"*|*"git reset"*|*"git revert"*|*"git tag"*|*"git clean"*|*"git push"*|*"git remote"*|*"git config"*) echo "❌ Dangerous git operation. Only 'git add' and 'git commit' allowed. Run this yourself." >&2; exit 2 ;;
*"git merge "*|*"git rebase "*|*"git cherry-pick "*|*"git reset "*|*"git revert "*|*"git tag "*|*"git clean "*|*"git push "*|*"git push"*|*"git remote "*|*"git config "*) echo "❌ Dangerous git operation. Run this yourself." >&2; exit 2 ;;
*"dotnet build"*) echo "❌ Use **build MCP tool** instead" >&2; exit 2 ;;
*"dotnet test"*) echo "❌ Use **test MCP tool** instead" >&2; exit 2 ;;
*"dotnet format"*) echo "❌ Use **format MCP tool** instead" >&2; exit 2 ;;
Expand Down
Loading
Loading