fix(audit-wave-9): standardize on Sheet for previews; doctrine in CLAUDE.md

Swap the one outlier (client-interests-tab.tsx) from Vaul Drawer to
Sheet side=right so every detail-preview surface uses the same
primitive. Document the doctrine: Sheet for side panels on both desktop
and mobile; Vaul Drawer reserved for mobile-only bottom-sheet UX
(currently just MoreSheet).

Closes ui/ux M11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 11:50:07 +02:00
parent b2588ecdd8
commit 4233aa3ac3
94 changed files with 1674 additions and 895 deletions

View File

@@ -1,7 +1,8 @@
'use client';
import { formatErrorBanner } from '@/lib/api/toast-error';
import { formatEnum } from '@/lib/constants';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -120,7 +121,7 @@ const GROUP_LABELS: Record<string, string> = {
};
function formatAction(action: string): string {
return action.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
return formatEnum(action);
}
interface RoleFormProps {
@@ -136,41 +137,36 @@ interface RoleFormProps {
onSuccess: () => void;
}
export function RoleForm({ open, onOpenChange, role, onSuccess }: RoleFormProps) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [permissions, setPermissions] = useState<Record<string, Record<string, boolean>>>(
structuredClone(DEFAULT_PERMISSIONS),
export function RoleForm(props: RoleFormProps) {
return (
<RoleFormBody key={props.open ? `open:${props.role?.id ?? 'new'}` : 'closed'} {...props} />
);
}
function RoleFormBody({ open, onOpenChange, role, onSuccess }: RoleFormProps) {
// Merge role permissions over defaults to fill any missing keys.
const initialPermissions = (() => {
const merged = structuredClone(DEFAULT_PERMISSIONS);
if (role) {
for (const [group, actions] of Object.entries(role.permissions)) {
if (merged[group]) {
for (const [action, value] of Object.entries(actions as Record<string, boolean>)) {
merged[group]![action] = value;
}
}
}
}
return merged;
})();
const [name, setName] = useState(role?.name ?? '');
const [description, setDescription] = useState(role?.description ?? '');
const [permissions, setPermissions] =
useState<Record<string, Record<string, boolean>>>(initialPermissions);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const isEdit = !!role;
useEffect(() => {
if (open) {
if (role) {
setName(role.name);
setDescription(role.description ?? '');
// Merge role permissions over defaults to fill any missing keys
const merged = structuredClone(DEFAULT_PERMISSIONS);
for (const [group, actions] of Object.entries(role.permissions)) {
if (merged[group]) {
for (const [action, value] of Object.entries(actions as Record<string, boolean>)) {
merged[group]![action] = value;
}
}
}
setPermissions(merged);
} else {
setName('');
setDescription('');
setPermissions(structuredClone(DEFAULT_PERMISSIONS));
}
setError(null);
}
}, [open, role]);
function togglePermission(group: string, action: string) {
setPermissions((prev) => {
const next = structuredClone(prev);