From a9d9004299636b99905d4e35bebb37644b183b3c Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 30 Nov 2025 11:45:19 +0100 Subject: [PATCH 01/10] Fix Text and Description components to render as block elements --- application/shared-webapp/ui/components/Description.tsx | 4 +++- application/shared-webapp/ui/components/Text.tsx | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/application/shared-webapp/ui/components/Description.tsx b/application/shared-webapp/ui/components/Description.tsx index 9465d17b49..835edc677f 100644 --- a/application/shared-webapp/ui/components/Description.tsx +++ b/application/shared-webapp/ui/components/Description.tsx @@ -5,5 +5,7 @@ import { Text, type TextProps } from "react-aria-components"; import { twMerge } from "tailwind-merge"; export function Description({ className, ...props }: Readonly) { - return ; + return ( + + ); } diff --git a/application/shared-webapp/ui/components/Text.tsx b/application/shared-webapp/ui/components/Text.tsx index 9df1d8ff69..d25d3683fe 100644 --- a/application/shared-webapp/ui/components/Text.tsx +++ b/application/shared-webapp/ui/components/Text.tsx @@ -3,7 +3,8 @@ */ import type { TextProps } from "react-aria-components"; import { Text as AriaText } from "react-aria-components"; +import { twMerge } from "tailwind-merge"; -export function Text(props: Readonly) { - return ; +export function Text({ className, ...props }: Readonly) { + return ; } From 9d6ac70367358b69961bf088539f5cb21bafdee6 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 3 Dec 2025 22:52:01 +0100 Subject: [PATCH 02/10] Consolidate frontend AI rules for dialogs and add z-index guidance --- .agent/rules/frontend/frontend.md | 139 +++++++++++------- .agent/rules/frontend/modal-dialog.md | 88 ----------- .claude/rules/frontend/frontend.md | 139 +++++++++++------- .claude/rules/frontend/modal-dialog.md | 89 ----------- .cursor/rules/frontend/frontend.mdc | 139 +++++++++++------- .cursor/rules/frontend/modal-dialog.mdc | 88 ----------- .github/copilot/rules/frontend/frontend.md | 139 +++++++++++------- .../copilot/rules/frontend/modal-dialog.md | 83 ----------- .windsurf/rules/frontend/frontend.md | 139 +++++++++++------- .windsurf/rules/frontend/modal-dialog.md | 89 ----------- 10 files changed, 415 insertions(+), 717 deletions(-) delete mode 100644 .agent/rules/frontend/modal-dialog.md delete mode 100644 .claude/rules/frontend/modal-dialog.md delete mode 100644 .cursor/rules/frontend/modal-dialog.mdc delete mode 100644 .github/copilot/rules/frontend/modal-dialog.md delete mode 100644 .windsurf/rules/frontend/modal-dialog.md diff --git a/.agent/rules/frontend/frontend.md b/.agent/rules/frontend/frontend.md index 304ce31a54..4df2520b2f 100644 --- a/.agent/rules/frontend/frontend.md +++ b/.agent/rules/frontend/frontend.md @@ -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 `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -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) @@ -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 ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ 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 ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.agent/rules/frontend/modal-dialog.md b/.agent/rules/frontend/modal-dialog.md deleted file mode 100644 index cfcac3f518..0000000000 --- a/.agent/rules/frontend/modal-dialog.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -trigger: glob -globs: *Dialog.tsx,*Modal.tsx -description: Rules for modal dialogs using React Aria Components ---- -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - diff --git a/.claude/rules/frontend/frontend.md b/.claude/rules/frontend/frontend.md index 6316ca51f1..cab5791023 100644 --- a/.claude/rules/frontend/frontend.md +++ b/.claude/rules/frontend/frontend.md @@ -50,7 +50,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 `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -59,6 +59,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) @@ -70,79 +73,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 ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ 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 ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.claude/rules/frontend/modal-dialog.md b/.claude/rules/frontend/modal-dialog.md deleted file mode 100644 index fdd1aceeb1..0000000000 --- a/.claude/rules/frontend/modal-dialog.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -trigger: glob -globs: *Dialog.tsx,*Modal.tsx -description: Rules for modal dialogs using React Aria Components ---- - -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - diff --git a/.cursor/rules/frontend/frontend.mdc b/.cursor/rules/frontend/frontend.mdc index 4e3d1b47a4..6e734568e3 100644 --- a/.cursor/rules/frontend/frontend.mdc +++ b/.cursor/rules/frontend/frontend.mdc @@ -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](mdc: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 `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -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) @@ -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 ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ 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 ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.cursor/rules/frontend/modal-dialog.mdc b/.cursor/rules/frontend/modal-dialog.mdc deleted file mode 100644 index 7e8e6f7da8..0000000000 --- a/.cursor/rules/frontend/modal-dialog.mdc +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: Rules for modal dialogs using React Aria Components -globs: *Dialog.tsx,*Modal.tsx -alwaysApply: false ---- -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - diff --git a/.github/copilot/rules/frontend/frontend.md b/.github/copilot/rules/frontend/frontend.md index 5ea7e5ebed..8d8fbf65ee 100644 --- a/.github/copilot/rules/frontend/frontend.md +++ b/.github/copilot/rules/frontend/frontend.md @@ -44,7 +44,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 `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -53,6 +53,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) @@ -64,79 +67,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 ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ 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 ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.github/copilot/rules/frontend/modal-dialog.md b/.github/copilot/rules/frontend/modal-dialog.md deleted file mode 100644 index f82c05e414..0000000000 --- a/.github/copilot/rules/frontend/modal-dialog.md +++ /dev/null @@ -1,83 +0,0 @@ -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - diff --git a/.windsurf/rules/frontend/frontend.md b/.windsurf/rules/frontend/frontend.md index 6316ca51f1..cab5791023 100644 --- a/.windsurf/rules/frontend/frontend.md +++ b/.windsurf/rules/frontend/frontend.md @@ -50,7 +50,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 `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -59,6 +59,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) @@ -70,79 +73,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 ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ 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 ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.windsurf/rules/frontend/modal-dialog.md b/.windsurf/rules/frontend/modal-dialog.md deleted file mode 100644 index fdd1aceeb1..0000000000 --- a/.windsurf/rules/frontend/modal-dialog.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -trigger: glob -globs: *Dialog.tsx,*Modal.tsx -description: Rules for modal dialogs using React Aria Components ---- - -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - From c3ed0a9d55532e5ed07331eba19a16733f398718 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 3 Dec 2025 22:57:30 +0100 Subject: [PATCH 03/10] Establish consistent z-index layering and fix toasts appearing behind dialogs --- .../federated-modules/common/SwitchingAccountLoader.tsx | 2 +- .../routes/admin/users/-components/UserProfileSidePane.tsx | 4 ++-- .../WebApp/routes/admin/users/-components/UserQuerying.tsx | 2 +- application/shared-webapp/ui/components/AddToHomescreen.tsx | 2 +- application/shared-webapp/ui/components/AppLayout.tsx | 4 ++-- application/shared-webapp/ui/components/Modal.tsx | 4 ++-- application/shared-webapp/ui/components/SideMenu.tsx | 6 +++--- application/shared-webapp/ui/components/Toast.tsx | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/application/account-management/WebApp/federated-modules/common/SwitchingAccountLoader.tsx b/application/account-management/WebApp/federated-modules/common/SwitchingAccountLoader.tsx index 88bf9d5aef..91ab22341d 100644 --- a/application/account-management/WebApp/federated-modules/common/SwitchingAccountLoader.tsx +++ b/application/account-management/WebApp/federated-modules/common/SwitchingAccountLoader.tsx @@ -3,7 +3,7 @@ import { Loader2 } from "lucide-react"; export function SwitchingAccountLoader() { return ( -
+

diff --git a/application/account-management/WebApp/routes/admin/users/-components/UserProfileSidePane.tsx b/application/account-management/WebApp/routes/admin/users/-components/UserProfileSidePane.tsx index bb1830128a..79f6e5c5bf 100644 --- a/application/account-management/WebApp/routes/admin/users/-components/UserProfileSidePane.tsx +++ b/application/account-management/WebApp/routes/admin/users/-components/UserProfileSidePane.tsx @@ -246,12 +246,12 @@ export function UserProfileSidePane({ return ( <> {/* Backdrop for small screens */} - {isSmallScreen &&

diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 8fba67ad3f..e0a8d7bdd6 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -264,12 +264,18 @@ msgstr "Fornavn" msgid "Free, open-source .NET and React starter kit for building modern SaaS applications." msgstr "Gratis, open-source .NET og React startpakke til at bygge moderne SaaS-applikationer." +msgid "Get started" +msgstr "Kom i gang" + msgid "GitHub" msgstr "GitHub" msgid "Go to account settings" msgstr "Gå til kontoindstillinger" +msgid "Go to app" +msgstr "Gå til app" + msgid "Help your team recognize your invites" msgstr "Hjælp dit team med at genkende dine invitationer" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index fa74fd7b91..ae90370019 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -264,12 +264,18 @@ msgstr "First name" msgid "Free, open-source .NET and React starter kit for building modern SaaS applications." msgstr "Free, open-source .NET and React starter kit for building modern SaaS applications." +msgid "Get started" +msgstr "Get started" + msgid "GitHub" msgstr "GitHub" msgid "Go to account settings" msgstr "Go to account settings" +msgid "Go to app" +msgstr "Go to app" + msgid "Help your team recognize your invites" msgstr "Help your team recognize your invites" From 2ecf0ce9611faa7e9302d293c5fcf008108b628a Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 3 Dec 2025 23:13:54 +0100 Subject: [PATCH 06/10] Make social links in footer open in new tab --- .../WebApp/shared/components/PublicFooter.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/application/account-management/WebApp/shared/components/PublicFooter.tsx b/application/account-management/WebApp/shared/components/PublicFooter.tsx index d77132acd3..2f263e3095 100644 --- a/application/account-management/WebApp/shared/components/PublicFooter.tsx +++ b/application/account-management/WebApp/shared/components/PublicFooter.tsx @@ -72,6 +72,8 @@ export function PublicFooter() { @@ -84,6 +86,8 @@ export function PublicFooter() { @@ -96,6 +100,8 @@ export function PublicFooter() { From 96923cf1950f3744bdd10f2ff280e1d2d844ddd0 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 3 Dec 2025 23:42:43 +0100 Subject: [PATCH 07/10] Move antiforgery token to import.meta for general code availability --- application/shared-webapp/build/environment.d.ts | 1 + application/shared-webapp/build/environment/runtime.ts | 9 ++++++++- .../build/plugin/RunTimeEnvironmentPlugin.ts | 3 ++- .../shared-webapp/infrastructure/http/httpClient.ts | 10 +--------- .../shared-webapp/infrastructure/http/queryClient.ts | 4 ++-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/application/shared-webapp/build/environment.d.ts b/application/shared-webapp/build/environment.d.ts index 82d6721f45..c1e3e9ea40 100644 --- a/application/shared-webapp/build/environment.d.ts +++ b/application/shared-webapp/build/environment.d.ts @@ -104,5 +104,6 @@ export declare global { build_env: BuildEnv; runtime_env: RuntimeEnv; user_info_env: UserInfoEnv; + antiforgeryToken: string; } } diff --git a/application/shared-webapp/build/environment/runtime.ts b/application/shared-webapp/build/environment/runtime.ts index 573e69d328..fbff03c85d 100644 --- a/application/shared-webapp/build/environment/runtime.ts +++ b/application/shared-webapp/build/environment/runtime.ts @@ -13,6 +13,7 @@ */ const runtimeEnvElement = document.head.getElementsByTagName("meta").namedItem("runtimeEnv"); const userInfoEnvElement = document.head.getElementsByTagName("meta").namedItem("userInfoEnv"); +const antiforgeryTokenElement = document.head.getElementsByTagName("meta").namedItem("antiforgeryToken"); if (runtimeEnvElement == null) { throw new Error("Runtime environment is not configured"); @@ -22,9 +23,14 @@ if (userInfoEnvElement == null) { throw new Error("UserInfo environment is not configured"); } +if (antiforgeryTokenElement == null) { + throw new Error("Antiforgery token is not configured"); +} + try { const runtimeEnv: RuntimeEnv = JSON.parse(runtimeEnvElement.content); const userInfoEnv: UserInfoEnv = JSON.parse(userInfoEnvElement.content); + const antiforgeryToken: string = antiforgeryTokenElement.content; const environment = { ...import.meta.build_env, @@ -36,7 +42,8 @@ try { buildEnv: import.meta.build_env, runtimeEnv, userInfoEnv, - env: environment + env: environment, + antiforgeryToken }); } catch { throw new Error("Could not read runtime environment"); diff --git a/application/shared-webapp/build/plugin/RunTimeEnvironmentPlugin.ts b/application/shared-webapp/build/plugin/RunTimeEnvironmentPlugin.ts index bfcaf112eb..87e6368f98 100644 --- a/application/shared-webapp/build/plugin/RunTimeEnvironmentPlugin.ts +++ b/application/shared-webapp/build/plugin/RunTimeEnvironmentPlugin.ts @@ -33,7 +33,8 @@ export function RunTimeEnvironmentPlugin> }), "import.meta.runtime_env": "getApplicationEnvironment().runtimeEnv", "import.meta.user_info_env": "getApplicationEnvironment().userInfoEnv", - "import.meta.env": "getApplicationEnvironment().env" + "import.meta.env": "getApplicationEnvironment().env", + "import.meta.antiforgeryToken": "getApplicationEnvironment().antiforgeryToken" } }, output: { diff --git a/application/shared-webapp/infrastructure/http/httpClient.ts b/application/shared-webapp/infrastructure/http/httpClient.ts index 8eaa6c1115..72d78fb839 100644 --- a/application/shared-webapp/infrastructure/http/httpClient.ts +++ b/application/shared-webapp/infrastructure/http/httpClient.ts @@ -14,14 +14,6 @@ import { normalizeError } from "./errorHandler"; // Default timeout for all fetch requests (in milliseconds) export const DEFAULT_TIMEOUT = 30000; -/** - * Gets the antiforgery token from the meta tag - */ -export function getAntiforgeryToken(): string { - const metaTag = document.querySelector('meta[name="antiforgeryToken"]'); - return metaTag?.getAttribute("content") ?? ""; -} - /** * Direct fetch wrapper for non-strongly-typed HTTP calls * Adds antiforgery tokens, timeout handling, and error processing @@ -42,7 +34,7 @@ export async function enhancedFetch(input: RequestInfo | URL, init?: RequestInit if (method !== "GET") { enhancedInit.headers = { ...enhancedInit.headers, - "x-xsrf-token": getAntiforgeryToken() + "x-xsrf-token": import.meta.antiforgeryToken }; } diff --git a/application/shared-webapp/infrastructure/http/queryClient.ts b/application/shared-webapp/infrastructure/http/queryClient.ts index c53ec406fd..62ff3cfd01 100644 --- a/application/shared-webapp/infrastructure/http/queryClient.ts +++ b/application/shared-webapp/infrastructure/http/queryClient.ts @@ -15,7 +15,7 @@ import createFetchClient from "openapi-fetch"; import createClient from "openapi-react-query"; import { getHasPendingAuthSync } from "../auth/AuthSyncService"; import { type HttpError, normalizeError } from "./errorHandler"; -import { DEFAULT_TIMEOUT, getAntiforgeryToken } from "./httpClient"; +import { DEFAULT_TIMEOUT } from "./httpClient"; /** * Creates HTTP middleware for the OpenAPI client @@ -36,7 +36,7 @@ function createHttpMiddleware() { // Only add the token for non-GET requests if (request.method !== "GET") { - request.headers.set("x-xsrf-token", getAntiforgeryToken()); + request.headers.set("x-xsrf-token", import.meta.antiforgeryToken); } // Handle request timeout with AbortController From 3b917a411e0c5db78e36f638756a15644e90de77 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 3 Dec 2025 23:45:41 +0100 Subject: [PATCH 08/10] Configure TanStack router for auto code splitting --- .../shared-webapp/build/plugin/FileSystemRouterPlugin.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/application/shared-webapp/build/plugin/FileSystemRouterPlugin.ts b/application/shared-webapp/build/plugin/FileSystemRouterPlugin.ts index 43c0cd853d..041c422984 100644 --- a/application/shared-webapp/build/plugin/FileSystemRouterPlugin.ts +++ b/application/shared-webapp/build/plugin/FileSystemRouterPlugin.ts @@ -13,7 +13,11 @@ export function FileSystemRouterPlugin(): RsbuildPlugin { const extraConfig: RsbuildConfig = { tools: { rspack: { - plugins: [TanStackRouterRspack()] + plugins: [ + TanStackRouterRspack({ + autoCodeSplitting: true + }) + ] } } }; From 2596cf86967b1e937873b7994e573b46e5c94c62 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Thu, 4 Dec 2025 01:06:34 +0100 Subject: [PATCH 09/10] Redesign change user role dialog with radio button layout and role descriptions --- .../-components/ChangeUserRoleDialog.tsx | 196 ++++++++++++------ .../shared/translations/locale/da-DK.po | 13 +- .../shared/translations/locale/en-US.po | 13 +- 3 files changed, 153 insertions(+), 69 deletions(-) diff --git a/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx b/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx index 216455116c..7b6046bb13 100644 --- a/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx +++ b/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx @@ -1,14 +1,20 @@ import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; -import { AlertDialog } from "@repo/ui/components/AlertDialog"; +import { Avatar } from "@repo/ui/components/Avatar"; import { Button } from "@repo/ui/components/Button"; -import { DialogContent, DialogFooter } from "@repo/ui/components/DialogFooter"; +import { Dialog } from "@repo/ui/components/Dialog"; +import { DialogContent, DialogFooter, DialogHeader } from "@repo/ui/components/DialogFooter"; +import { Form } from "@repo/ui/components/Form"; +import { Heading } from "@repo/ui/components/Heading"; import { Modal } from "@repo/ui/components/Modal"; -import { Select, SelectItem } from "@repo/ui/components/Select"; +import { Radio, RadioGroup } from "@repo/ui/components/RadioGroup"; +import { Text } from "@repo/ui/components/Text"; import { toastQueue } from "@repo/ui/components/Toast"; -import { useCallback, useState } from "react"; +import { getInitials } from "@repo/utils/string/getInitials"; +import { useQueryClient } from "@tanstack/react-query"; +import { XIcon } from "lucide-react"; +import { useState } from "react"; import { api, type components, UserRole } from "@/shared/lib/api/client"; -import { getUserRoleLabel } from "@/shared/lib/api/userRole"; type UserDetails = components["schemas"]["UserDetails"]; @@ -19,19 +25,14 @@ interface ChangeUserRoleDialogProps { } export function ChangeUserRoleDialog({ user, isOpen, onOpenChange }: Readonly) { + const queryClient = useQueryClient(); const [selectedRole, setSelectedRole] = useState(null); - const changeUserRoleMutation = api.useMutation("put", "/api/account-management/users/{id}/change-user-role"); - const handleConfirm = useCallback(async () => { - if (!user || !selectedRole) { - return; - } - - try { - await changeUserRoleMutation.mutateAsync({ - params: { path: { id: user.id } }, - body: { userRole: selectedRole } - }); + const changeUserRoleMutation = api.useMutation("put", "/api/account-management/users/{id}/change-user-role", { + onSuccess: () => { + if (!user) { + return; + } const userDisplayName = `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || user.email; toastQueue.add({ @@ -39,58 +40,131 @@ export function ChangeUserRoleDialog({ user, isOpen, onOpenChange }: Readonly { - onOpenChange(false); - setSelectedRole(null); - }, [onOpenChange]); + if (!user) { + return null; + } + + const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email; + const currentRole = selectedRole ?? user.role; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + changeUserRoleMutation.mutate({ + params: { + path: { id: user.id } + }, + body: { userRole: currentRole } + }); + }; + + const handleOpenChange = (open: boolean) => { + if (!open) { + setSelectedRole(null); + } + onOpenChange(open); + }; return ( - - -
- -

- - Select a new role for{" "} - {user ? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || user.email : ""} - -

+ + + {({ close }) => ( + <> + + + + Change user role + + -
- -
-
+
+ +
+ +
+ {displayName} + {user.title && {user.title}} + {user.email} +
+
- - - - -
-
+ setSelectedRole(value as UserRole)} + orientation="vertical" + > +
+ +
+ + Owner + + + Full access including user roles and account settings + +
+
+
+
+ +
+ + Admin + + + Full access except changing user roles and account settings + +
+
+
+
+ +
+ + Member + + + Standard user access + +
+
+
+
+ + + + + + + + )} +
); } diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index e0a8d7bdd6..00b2782d64 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -264,6 +264,12 @@ msgstr "Fornavn" msgid "Free, open-source .NET and React starter kit for building modern SaaS applications." msgstr "Gratis, open-source .NET og React startpakke til at bygge moderne SaaS-applikationer." +msgid "Full access except changing user roles and account settings" +msgstr "Fuld adgang undtagen ændring af brugerroller og kontoindstillinger" + +msgid "Full access including user roles and account settings" +msgstr "Fuld adgang inklusive brugerroller og kontoindstillinger" + msgid "Get started" msgstr "Kom i gang" @@ -444,10 +450,6 @@ msgstr "Skærmbilleder af dashboard-projektet med desktop- og mobilversioner" msgid "Search" msgstr "Søg" -#. placeholder {0}: user ? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || user.email : "" -msgid "Select a new role for <0>{0}" -msgstr "Vælg en ny rolle for <0>{0}" - msgid "Select Account" msgstr "Vælg konto" @@ -475,6 +477,9 @@ msgstr "Tilmeld dig på sekunder for at bygge på PlatformPlatform – ligesom t msgid "Signup verification code" msgstr "Tilmeldingsbekræftelseskode" +msgid "Standard user access" +msgstr "Standard brugeradgang" + msgid "Success" msgstr "Succes" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index ae90370019..e1a652b6d1 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -264,6 +264,12 @@ msgstr "First name" msgid "Free, open-source .NET and React starter kit for building modern SaaS applications." msgstr "Free, open-source .NET and React starter kit for building modern SaaS applications." +msgid "Full access except changing user roles and account settings" +msgstr "Full access except changing user roles and account settings" + +msgid "Full access including user roles and account settings" +msgstr "Full access including user roles and account settings" + msgid "Get started" msgstr "Get started" @@ -444,10 +450,6 @@ msgstr "Screenshots of the dashboard project with desktop and mobile versions" msgid "Search" msgstr "Search" -#. placeholder {0}: user ? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || user.email : "" -msgid "Select a new role for <0>{0}" -msgstr "Select a new role for <0>{0}" - msgid "Select Account" msgstr "Select Account" @@ -475,6 +477,9 @@ msgstr "Sign up in seconds to start building on PlatformPlatform – just like t msgid "Signup verification code" msgstr "Signup verification code" +msgid "Standard user access" +msgstr "Standard user access" + msgid "Success" msgstr "Success" From 7dde28c1a89da2db2c82c8cf9784f7f18b8e6adc Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Thu, 4 Dec 2025 01:25:19 +0100 Subject: [PATCH 10/10] Fix git hook to allow read-only commands while blocking dangerous operations --- .claude/hooks/pre-tool-use-bash.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/hooks/pre-tool-use-bash.sh b/.claude/hooks/pre-tool-use-bash.sh index 709d2fea04..1128d2e071 100755 --- a/.claude/hooks/pre-tool-use-bash.sh +++ b/.claude/hooks/pre-tool-use-bash.sh @@ -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 ;;