Skip to content

Fix/notifications#749

Merged
kamilwronka merged 12 commits intodevelopfrom
fix/notifications
Apr 19, 2026
Merged

Fix/notifications#749
kamilwronka merged 12 commits intodevelopfrom
fix/notifications

Conversation

@kamilwronka
Copy link
Copy Markdown
Contributor

@kamilwronka kamilwronka commented Apr 19, 2026

Summary by CodeRabbit

  • New Features
    • App-wide internationalization enabled; many UI texts now localized.
    • Enhanced command composer with parsed command modes, context panel, and richer suggestions with icons.
  • Documentation
    • Added multiple design/skill specification documents to guide frontend design output.
  • Chores
    • Reorganized settings translations into modular bundles and updated color-name utilities.
  • Tests
    • Large expansion of unit/integration tests across commands, notifications, detectors, timers, and related stores.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Warning

Rate limit exceeded

@kamilwronka has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 39 minutes and 42 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 39 minutes and 42 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4657c42d-2da9-4ad3-b6f4-af1e8c68b2c9

📥 Commits

Reviewing files that changed from the base of the PR and between 8b31048 and f04fcc8.

📒 Files selected for processing (4)
  • apps/game-client/src/features/command/command-state.test.ts
  • apps/game-client/src/features/command/command-state.ts
  • apps/game-client/src/features/command/command.tsx
  • apps/game-client/src/i18n/translations/settings/command.json
📝 Walkthrough

Walkthrough

Adds multiple new agent/Claude skill spec files, restructures settings i18n from a monolithic JSON into many modular files, integrates i18n across the game client UI, implements centralized command parsing/state and related UI/components, refactors timer color-name resolution to i18n-backed getters, and adds extensive test coverage across notifications, detector, timers, and settings.

Changes

Cohort / File(s) Summary
Agent / Claude Skills
.agents/skills/*.md, .agents/skills/.../DESIGN.md, .claude/skills/*
Added eight new agent skill spec Markdown files and small Claude pointer files; purely documentation/spec content describing design/motion/verification rules (no runtime code changes).
i18n Restructure & Resources
apps/game-client/src/i18n/config.ts, apps/game-client/src/i18n/translations/settings.json (deleted), apps/game-client/src/i18n/translations/settings/index.ts, apps/game-client/src/i18n/translations/settings/*.json
Removed monolithic settings.json, added settings/ directory with many new JSON translation files and an index aggregator; updated i18n config import to use the new module.
Command Parsing & UI
apps/game-client/src/features/command/command-state.ts, .../command.tsx, .../command.test.tsx, .../command-state.test.ts, components/command-*.tsx
Introduced getCommandState() and CommandState types, moved command parsing out of inline logic, updated submit flow to use parsed state (normal/notification/party), added CommandContextPanel, refactored suggestions/actions to use constants/i18n/icons, and added tests.
Notifications: store + tests + UI i18n
apps/game-client/src/store/notifications.store.test.ts, apps/game-client/src/features/notifications/**/*.test.tsx, apps/game-client/src/features/notifications/hooks/use-notifications.test.tsx, apps/game-client/src/features/notifications/*.tsx
Expanded tests for merge/removal, auto-hide lifecycle (set/pause/resume/clear), sound-play filtering, scroll/viewport sync; many notification components switched hardcoded Polish strings to i18n keys.
NPC Detector: logic, i18n, tests
apps/game-client/src/features/npc-detector/**/*.ts(x), apps/game-client/src/store/npc-detector.store.test.ts
Added/updated tests for NPC list, detection animations, and interactions; integrated i18n for user-facing messages and refined detection animation lifecycle tests.
Timer Color-name refactor
apps/game-client/src/features/timers/constants/color-names.ts, .../color-utils.ts, .../default-color-item.tsx, .../hidden-colors-list.tsx, timers-settings-colors.tsx, timer-color-picker.tsx, color-statistics.ts
Replaced compile-time DEFAULT_COLOR_NAMES map with DEFAULT_COLOR_IDS and runtime getDefaultColorName / getDefaultColorNames() that resolve via i18n; updated all consumers to call the getter.
Widespread i18n integration (components & features)
apps/game-client/src/components/*, apps/game-client/src/features/* (chat, timers, party-finder, quick-access, settings, hooks, processors)
Replaced many hardcoded Polish strings with useTranslation()/i18n.t(...) across UI components, hooks, and processors; also injected translated validation messages into form schemas.
Tests: settings, forms, hooks
apps/game-client/src/features/settings/components/**/*.test.tsx, various new test files across features
Added many unit/integration tests for detector routing, notification category forms, settings tabs, timers, notifications, party-finder flows, and hooks (detector, notifications, character normalization).
Character API normalization
apps/game-client/src/api/characters.api.ts, apps/game-client/src/hooks/api/use-character-list.test.ts
Introduced deeper normalization pipeline for character payloads (alias support, tuple handling, coercion helpers), centralized world filtering/sorting, adjusted cache handling, and added tests for normalization cases.
Skills lock update
skills-lock.json
Added entries for the eight new skills with source and computed hashes.

Sequence Diagram(s)

sequenceDiagram
    participant UI as CommandWindow (Client)
    participant Parser as getCommandState
    participant Suggestions as CommandSuggestions
    participant Party as PartyHandler
    participant NotifAPI as Notification API
    participant ChatAPI as Chat API

    UI->>Parser: onInput(message)
    Parser-->>Suggestions: compute commandQuery / open state
    Suggestions-->>UI: show suggestion list
    UI->>Parser: onSubmit(message)
    Parser-->>UI: return CommandState (mode, canSubmit, submissionMessage)
    alt mode == "party"
        UI->>Party: handlePartyCommand(submissionMessage, selectedGuilds)
        Party-->>ChatAPI: send PARTY message
    else mode == "notification"
        UI->>NotifAPI: createNotification(submissionMessage, recipients)
        NotifAPI-->>ChatAPI: may send NOTIFICATION chat message
    else mode == "normal"
        UI->>ChatAPI: send NORMAL chat message (submissionMessage)
    end
    ChatAPI-->>UI: ack / close window
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through keys and strings today,

Translations sprouted where Polish lay,
Commands now parse with tidy art,
Notifications hum and tests take part,
A little rabbit cheers — merge away! 🎉

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/notifications

@kamilwronka
Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c2211aec8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +51 to +53
const hasContent = isCommand
? commandPayload.length > 0
: inputValue.trim().length > 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow !grp without requiring a payload

getCommandState currently treats all command-prefixed inputs as requiring non-empty commandPayload, which makes !grp (or !grp ) unsendable because canSubmit becomes false. That regresses party-gathering behavior: usePartyCommand accepts description: string | undefined, and the legacy chat input still supports !grp with an empty description, so users can no longer trigger party search from the new command window unless they add text.

Useful? React with 👍 / 👎.

# Conflicts:
#	apps/game-client/src/features/settings/components/detector/detector-routing-settings-tab-form.test.tsx
#	apps/game-client/src/i18n/translations/settings.json
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 19, 2026

@copilot resolve the merge conflicts in this pull request

Resolved and pushed in commit 8b31048d.

This merge-conflict resolution had no UI changes (code/test/translation merge only).

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • checkpoint.prisma.io
    • Triggering command: /opt/hostedtoolcache/node/24.14.1/x64/bin/node /opt/hostedtoolcache/node/24.14.1/x64/bin/node /home/REDACTED/work/monorepo/monorepo/node_modules/.pnpm/prisma@7.6.0_@types+react-dom@19.2.3_@types+react@19.2.14__@types+react@19.2.14_react-d_c3d04ead8d86e26df6fd9d6c3f1a453c/node_modules/prisma/build/child {"product":"prisma","version":"7.6.0","cli_install_type":"local","information":"","local_timestamp":"2026-04-19T12:09:58Z","project_hash":"a9e7acc8","cli_path":"/home/REDACTED/work/monorepo/monorepo/apps/activity/node_modules/prisma/build/index.js","cli_pat ures/include /.bi�� che/node-gyp/24.14.1/include/node -I bin/sh -I che/node-gyp/24./home/REDACTED/work/monorepo/monorepo/apps/search/node_modules/.bin/..�� -I che/node-gyp/24.14.1/deps/openssl/openssl/include -I install-entry.js -I bin/node -I che/node-gyp/24.-c -I /bin/sh (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.14.1/x64/bin/node /opt/hostedtoolcache/node/24.14.1/x64/bin/node /home/REDACTED/work/monorepo/monorepo/node_modules/.pnpm/prisma@7.6.0_@types+react-dom@19.2.3_@types+react@19.2.14__@types+react@19.2.14_react-d_c3d04ead8d86e26df6fd9d6c3f1a453c/node_modules/prisma/build/child {"product":"prisma","version":"7.6.0","cli_install_type":"local","information":"","local_timestamp":"2026-04-19T12:09:59Z","project_hash":"f034c80f","cli_path":"/home/REDACTED/work/monorepo/monorepo/apps/api/node_modules/prisma/build/index.js","cli_path_has ures/include /.bi�� bj.target/cpufeatures.node" "Release/cpufeatures.node" 2>/dev/null || (rm -rf "Release/cpufeatursh ../deps/cpu_features/src/impl_ppc_linux.c -helpers/node_modules/.bin/node /Release/.deps/Rnode -DUSING_V8_SHARE/home/REDACTED/work/_temp/ghcca-node/node/bin/pnpm modules/.bin/as as pack�� stdout.buffer.write(sys.executable.encode('utf-8')); -I k/_temp/ghcca-node/node/bin/node -I /home/REDACTED/.ca/home/REDACTED/work/monorepo/monorepo/apps/discord-bot/node_modules/.bin/../@nestjs/cli/bin/nest.js -I /bin/sh (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.14.1/x64/bin/node /opt/hostedtoolcache/node/24.14.1/x64/bin/node /home/REDACTED/work/monorepo/monorepo/node_modules/.pnpm/prisma@7.6.0_@types+react-dom@19.2.3_@types+react@19.2.14__@types+react@19.2.14_react-d_c3d04ead8d86e26df6fd9d6c3f1a453c/node_modules/prisma/build/child {"product":"prisma","version":"7.6.0","cli_install_type":"local","information":"","local_timestamp":"2026-04-19T12:10:12Z","project_hash":"a9e7acc8","cli_path":"/home/REDACTED/work/monorepo/monorepo/apps/activity/node_modules/prisma/build/index.js","cli_pat printf %s\n�� SOLINK_MODULE(target) Release/obj.target/cpufeatures.node -I /.bin/node -I /home/REDACTED/.ca/home/REDACTED/work/_temp/ghcca-node/node/bin/pnpm -I python3 ient�� import sys; sys.stdout.buffer.write(sys.executable.encode('utf-8')); --64 k/_temp/ghcca-node/node/bin/sh get --global de-gyp-build-optprisma generate opencollective (dns block)
  • fonts.googleapis.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/monorepo/monorepo/apps/landing/node_modules/.bin/../next/dist/bin/next build sh bin/�� deps/cpu_features/src/ et/cpu_features/deps/cpu_feature/home/REDACTED/.cache/node-gyp/24.14.1/deps/v8/include as s.o.d.raw | grepnode che/node-gyp/24./home/REDACTED/work/_temp/ghcca-node/node/bin/pnpm s/.bin/as che/node-gyp/24.build -x che/node-gyp/24.14.1/include/node -o 1/dist/node-gyp-bin/node - che/node-gyp/24./home/REDACTED/work/monorepo/monorepo/apps/docs/node_modules/.bin/../next/dist/bin/next de_modules/.bin/build che/node-gyp/24.14.1/deps/openssl/openssl/includ-I (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
apps/game-client/src/components/world-selector.tsx (1)

60-96: ⚠️ Potential issue | 🟠 Major

Remove manual memoization around translated groups.

Line 60 uses useMemo which violates the React Compiler memoization rule for TSX files. React Compiler handles this optimization automatically—manual memoization with useMemo, memo, and useCallback is not needed.

♻️ Proposed refactor
-import { type FC, useEffect, useMemo } from "react";
+import { type FC, useEffect } from "react";
-  const worldGroups = useMemo<ComboboxGroup[]>(() => {
+  const worldGroups: ComboboxGroup[] = (() => {
     if (!worlds || worlds.length === 0) return [];
 
     const recent =
       recentWorlds
@@
 
     return groups;
-  }, [t, worlds, recentWorlds]);
+  })();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/components/world-selector.tsx` around lines 60 - 96,
Remove the manual useMemo wrapper around the worldGroups computation: replace
the const worldGroups = useMemo<ComboboxGroup[]>(() => { ... }, [t, worlds,
recentWorlds]) with a plain const worldGroups = (() => { ... })() or inline
computation so the same logic that builds recent, recentValues, rest and groups
using t, worlds and recentWorlds remains but without useMemo; keep the same
variable names (recent, recentValues, rest, groups) and the ComboboxGroup shape
so behavior is unchanged.
apps/game-client/src/components/ui/autocomplete-suggestions.tsx (1)

2-26: ⚠️ Potential issue | 🟡 Minor

Default-param i18n.t(...) won't react to language changes.

Using i18n.t(...) as a default parameter resolves at call time but the component will not re-render when the active language changes, because it isn't subscribed via the useTranslation hook. If the user switches language while this component is mounted with no noResultsMessage prop, the displayed text will remain stale until the component re-renders for an unrelated reason.

Prefer resolving the default inside the component body via useTranslation, which also keeps behavior consistent with the rest of the app (e.g., window-title-bar.tsx).

♻️ Proposed change
-import type React from "react";
-import i18n from "@/i18n/config";
+import type React from "react";
+import { useTranslation } from "react-i18next";
@@
 export const AutocompleteSuggestions = <T,>({
   items,
   isOpen,
   onSelect,
   selectedIndex,
   renderItem,
   keyExtractor,
-  noResultsMessage = i18n.t("settings.command.suggestions.noResults"),
+  noResultsMessage,
   showNoResults = false,
   className = "",
 }: AutocompleteSuggestionsProps<T>) => {
+  const { t } = useTranslation();
+  const resolvedNoResultsMessage =
+    noResultsMessage ?? t("settings.command.suggestions.noResults");
   const hasResults = items.length > 0;
@@
-        <p className="ll:text-xs ll:text-gray-400 ll:text-center ll:px-3 ll:py-2">
-          {noResultsMessage}
-        </p>
+        <p className="ll:text-xs ll:text-gray-400 ll:text-center ll:px-3 ll:py-2">
+          {resolvedNoResultsMessage}
+        </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/components/ui/autocomplete-suggestions.tsx` around lines
2 - 26, The default i18n.t(...) call in the AutocompleteSuggestions component
signature is evaluated once and won't update on language changes; instead import
and call useTranslation() inside the AutocompleteSuggestions function and derive
noResultsMessage from t(...) when the prop is undefined (e.g., const { t } =
useTranslation(); const resolvedNoResultsMessage = noResultsMessage ??
t("settings.command.suggestions.noResults")), leaving the prop signature and
other props (items, isOpen, onSelect, selectedIndex, renderItem, keyExtractor,
showNoResults, className) unchanged so the component re-renders with the correct
localized text when language changes.
apps/game-client/src/features/chat/chat.tsx (1)

155-166: ⚠️ Potential issue | 🔴 Critical

Fix invalid JSX prop syntax: wrap element expressions in braces.

Line 161 uses invalid TSX syntax (actions=<ChatWindowActions ... />); JSX elements passed as props must be wrapped in braces: actions={<ChatWindowActions ... />}.

This pattern appears in 20+ additional files across the codebase and must be fixed systematically. Files include:

  • apps/web/src/features/guild/stats/components/loot-top-npcs-chart.tsx (lines 108, 117)
  • apps/web/src/features/guild/stats/components/loot-rarity-chart.tsx (lines 98, 138)
  • apps/web/src/features/guild/stats/components/loot-timeline-chart.tsx (line 120)
  • apps/web/src/features/guild/settings/roles/roles.tsx (line 70)
  • apps/game-client/src/features/command/command.tsx (line 136)
  • apps/game-client/src/features/npc-detector/npc-detector.tsx (line 66)
  • And others—search for the pattern with: rg '\b[a-zA-Z_$][\w$-]*=<[A-Z]' --glob='**/*.{tsx,jsx}'
Proposed fix for chat.tsx
         minHeight={116}
         minWidth={242}
-        actions=<ChatWindowActions
-          chatInputEnabled={isChatInputEnabled}
-          toggleChatInputEnabled={toggleChatInputEnabled}
-          filtersVisible={filtersVisible}
-          toggleFiltersVisible={toggleFiltersVisible}
-        />
+        actions={
+          <ChatWindowActions
+            chatInputEnabled={isChatInputEnabled}
+            toggleChatInputEnabled={toggleChatInputEnabled}
+            filtersVisible={filtersVisible}
+            toggleFiltersVisible={toggleFiltersVisible}
+          />
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/chat/chat.tsx` around lines 155 - 166, The JSX
prop assignment for the DraggableWindow component is using invalid syntax
(actions=<ChatWindowActions ... />); update the prop to pass a JSX element
expression by wrapping it in braces: change the actions prop on DraggableWindow
to actions={<ChatWindowActions ... />} so the ChatWindowActions element (used
here) is treated as a JSX expression. Locate the DraggableWindow usage and
similarly fix any other instances that follow the pattern identifier=<Component
... /> (e.g., places referencing ChatWindowActions, DraggableWindow, or other
components) by wrapping the element value in braces.
apps/game-client/src/features/npc-detector/components/npc-list-item.tsx (1)

420-600: ⚠️ Potential issue | 🟠 Major

Add accessible names and reuse computed action labels.

The notification and gather buttons are icon-only, so tooltip text alone does not give them a reliable accessible name. Compute translated labels once and use them for both aria-label and tooltip content; this also removes the nested ternary in Line 595.

♿ Proposed fix
   const background = getBackgroundColor(key, settingsByNpcType?.highlight);
   const borderColor = getBorderColor(key, settingsByNpcType?.highlight);
+  const notificationActionLabel = npc.notificationSent
+    ? t("settings.detector.runtime.notificationSent")
+    : t("settings.detector.runtime.sendNotification");
+
+  let gatherActionLabel = t("settings.detector.runtime.gatherParty");
+  if (isGatheringPartyPending) {
+    gatherActionLabel = t("settings.detector.runtime.gatherPending");
+  } else if (hasActivePartyGathering) {
+    gatherActionLabel = t("settings.detector.runtime.alreadyGathering");
+  }
 
   return (
@@
                 <Button
                   variant="ghost"
                   className={`ll:relative ${ACTION_BUTTON_CLASS_NAME}`}
                   disabled={isCreateNotificationPending || npc.notificationSent}
                   onClick={() => handleSendNotification(npc)}
+                  aria-label={notificationActionLabel}
                 >
@@
               <TooltipContent side="top">
-                {npc.notificationSent
-                  ? t("settings.detector.runtime.notificationSent")
-                  : t("settings.detector.runtime.sendNotification")}
+                {notificationActionLabel}
               </TooltipContent>
@@
                 <Button
                   variant="ghost"
                   className={ACTION_BUTTON_CLASS_NAME}
                   disabled={isGatheringPartyPending || hasActivePartyGathering}
                   onClick={() => handleGatherParty(npc)}
+                  aria-label={gatherActionLabel}
                 >
@@
               <TooltipContent side="top">
-                {isGatheringPartyPending
-                  ? t("settings.detector.runtime.gatherPending")
-                  : hasActivePartyGathering
-                    ? t("settings.detector.runtime.alreadyGathering")
-                    : t("settings.detector.runtime.gatherParty")}
+                {gatherActionLabel}
               </TooltipContent>

As per coding guidelines, “Avoid nested (chained) ternary expressions — use early returns or if/else if instead.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/npc-detector/components/npc-list-item.tsx`
around lines 420 - 600, Compute translated action label strings once (e.g.,
notificationLabel, notificationSentLabel, gatherLabel, gatherPendingLabel,
gatherAlreadyLabel) and reuse them for both the Button aria-label and the
TooltipContent instead of relying on tooltip-only accessibility; update the
Buttons in the icon-only controls (the Button that calls handleSendNotification
and the Button that calls handleGatherParty) to include
aria-label={notificationLabel} or aria-label={gatherLabel} and pass the same
variables into TooltipContent. Also replace the nested ternary used to choose
the gather tooltip text with a small if/else or conditional variable assignment
(using hasActivePartyGathering and isGatheringPartyPending) so the tooltip
selection logic is clear and reusable.
apps/game-client/src/features/timers/components/timers-actions.tsx (1)

34-128: ⚠️ Potential issue | 🟠 Major

Wrap the icon actions in accessible buttons.

These SVGs handle clicks directly, so they are not keyboard-operable controls and the localized tooltip text is not exposed as an accessible name. Use native buttons with translated aria-labels, and mark the icons decorative.

♿ Proposed pattern
 }) => {
   const { t } = useTranslation();
+  const filtersLabel = timerFiltersEnabled
+    ? t("settings.timers.actions.hideFilters")
+    : t("settings.timers.actions.showFilters");
 
   return [
     <Tooltip key="filters-tooltip">
       <TooltipTrigger asChild>
-        <Filter
-          key="filters"
-          className="ll-custom-cursor-pointer ll:mt-0.5 ll:stroke-gray-300 ll:hover:stroke-gray-100 ll:transition-colors"
-          size="14"
+        <button
+          type="button"
+          aria-label={filtersLabel}
+          className="ll-custom-cursor-pointer ll:mt-0.5 ll:p-0 ll:bg-transparent ll:border-0"
           onClick={toggleTimerFiltersEnabled}
-        />
+        >
+          <Filter
+            aria-hidden="true"
+            className="ll:stroke-gray-300 ll:hover:stroke-gray-100 ll:transition-colors"
+            size="14"
+          />
+        </button>
       </TooltipTrigger>
-      <TooltipContent side="top">
-        {timerFiltersEnabled
-          ? t("settings.timers.actions.hideFilters")
-          : t("settings.timers.actions.showFilters")}
-      </TooltipContent>
+      <TooltipContent side="top">{filtersLabel}</TooltipContent>
     </Tooltip>,

Apply the same pattern to the palette, sort, and hidden-timer actions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/timers/components/timers-actions.tsx` around
lines 34 - 128, The icon SVGs (Filter, Palette, SortDesc, SortAsc, Eye, EyeOff)
must be wrapped in native buttons so they are keyboard-operable and have
accessible names: replace the SVGs as direct click targets with <button>
elements (used as the child of TooltipTrigger) that call the existing handlers
(toggleTimerFiltersEnabled, toggleColorFiltersEnabled, setTimersSortOrder,
setShowHiddenTimers) and supply translated aria-labels via t(...) (use the same
strings currently in TooltipContent). Keep icons decorative by removing
interactive handlers from them and setting aria-hidden/focusable={false} on the
SVGs, and move any visual classNames you need to the button so styling remains.
Ensure the conditional logic still uses timerFiltersEnabled,
colorFiltersEnabled, timersSortOrder, and showHiddenTimers to choose labels and
styles.
🟡 Minor comments (13)
.agents/skills/design-taste-frontend/SKILL.md-134-135 (1)

134-135: ⚠️ Potential issue | 🟡 Minor

Normalize heading spelling and platform naming for clarity.

Use Navigation & Menus and macOS to avoid documentation noise and search inconsistency.

Suggested fix
-### Navigation & Menüs
-* **Mac OS Dock Magnification:** Nav-bar at the edge; icons scale fluidly on hover.
+### Navigation & Menus
+* **macOS Dock Magnification:** Nav-bar at the edge; icons scale fluidly on hover.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/design-taste-frontend/SKILL.md around lines 134 - 135, Update
the heading and platform naming to be consistent: replace the heading
"Navigation & Menüs" with "Navigation & Menus" and change "Mac OS" to "macOS"
within the paragraph (the string containing "Mac OS Dock Magnification" /
"Nav-bar at the edge; icons scale fluidly on hover.") so documentation spelling
and platform naming are normalized for search and clarity.
.agents/skills/full-output-enforcement/SKILL.md-37-39 (1)

37-39: ⚠️ Potential issue | 🟡 Minor

Add a language identifier to the fenced code block.

Line 37 uses an unlabeled fence, which trips markdownlint MD040. Use a language tag (e.g., text) to keep docs lint-clean.

Suggested fix
-```
+```text
 [PAUSED — X of Y complete. Send "continue" to resume from: next section name]
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @.agents/skills/full-output-enforcement/SKILL.md around lines 37 - 39, The
fenced code block containing the literal line "[PAUSED — X of Y complete. Send
"continue" to resume from: next section name]" is unlabeled and triggers
markdownlint MD040; update that fence in SKILL.md by changing the opening to a language-tagged fence such astext so the block becomes text ... to
satisfy the linter while preserving the block content.


</details>

</blockquote></details>
<details>
<summary>.agents/skills/high-end-visual-design/SKILL.md-1-4 (1)</summary><blockquote>

`1-4`: _⚠️ Potential issue_ | _🟡 Minor_

**Add a Skills & Capabilities section to AGENTS.md documenting the new skills.**

This PR adds six new skill definitions to `.agents/skills/`, but AGENTS.md has no Skills section to document them. Per project learnings, agent definitions and capabilities should be documented in AGENTS.md for visibility. Please add a section listing all available skills and their purposes (e.g., `high-end-visual-design`, `full-output-enforcement`, `industrial-brutalist-ui`, `design-taste-frontend`, `minimalist-ui`, `stitch-design-taste`) alongside the existing instructions.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/high-end-visual-design/SKILL.md around lines 1 - 4, Add a
"Skills & Capabilities" section to AGENTS.md that documents the six new skill
definitions added under .agents/skills/: high-end-visual-design,
full-output-enforcement, industrial-brutalist-ui, design-taste-frontend,
minimalist-ui, and stitch-design-taste; for each skill include a one-line
purpose/summary (e.g., "high-end-visual-design — enforces agency-grade fonts,
spacing, shadows, card structure and animations") and place this section
alongside the existing instructions section so agent definitions and
capabilities are visible in the same top-level doc.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/timers/components/timers-empty-state.tsx-15-15 (1)</summary><blockquote>

`15-15`: _⚠️ Potential issue_ | _🟡 Minor_

**Localize the default empty state too.**

Line 15 still hardcodes visible fallback text. Add a translation key for this branch as well, even if the Polish value remains `----`.



<details>
<summary>🌐 Proposed fix</summary>

```diff
-      {areFiltersActive ? t("settings.timers.emptyState.filtered") : "----"}
+      {areFiltersActive
+        ? t("settings.timers.emptyState.filtered")
+        : t("settings.timers.emptyState.default")}
```

</details>

As per coding guidelines, `apps/*/src/**/*.{jsx,tsx}`: All static text in frontend apps must use i18n — never hardcode user-facing strings.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/timers/components/timers-empty-state.tsx` at
line 15, The default fallback text is hardcoded; update the component to use
i18n like the filtered branch by adding a new translation key (e.g.
"settings.timers.emptyState.default") and replacing the raw "----" with
t("settings.timers.emptyState.default"); modify the translations resource(s)
(including Polish) to include this key (Polish value can stay "----") and ensure
the component still uses areFiltersActive and t(...) as currently implemented.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/settings/components/timers/timers-settings-colors.tsx-68-68 (1)</summary><blockquote>

`68-68`: _⚠️ Potential issue_ | _🟡 Minor_

**Replace `||` with `??` for nullish coalescing on line 68.**

Line 68 uses `||` which incorrectly treats empty strings as missing values, replacing intentionally saved empty custom names with the default translated name. Use `??` instead to preserve empty strings while still falling back on `null` or `undefined`.

<details>
<summary>Proposed fix</summary>

```diff
-      name: defaultColorNames[colorId] || getDefaultColorName(colorId) || "",
+      name: defaultColorNames[colorId] ?? getDefaultColorName(colorId) ?? "",
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/settings/components/timers/timers-settings-colors.tsx`
at line 68, The current assignment for the name property uses the logical OR
which treats empty strings as missing (name: defaultColorNames[colorId] ||
getDefaultColorName(colorId) || ""), so replace the ORs with nullish coalescing
to preserve intentionally empty names: use defaultColorNames[colorId] ??
getDefaultColorName(colorId) ?? "" in the code path that builds the object
(referencing defaultColorNames, getDefaultColorName and colorId) to ensure only
null/undefined fallbacks occur.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/components/timer-settings-conflict-dialog.tsx-25-28 (1)</summary><blockquote>

`25-28`: _⚠️ Potential issue_ | _🟡 Minor_

**Handle timestamp `0` correctly in the missing-date check.**

Line 26 uses `if (!date)` which treats the valid timestamp `0` as missing, incorrectly returning the unknown-date fallback instead of formatting it as `1970-01-01T00:00:00.000Z` equivalent.

<details>
<summary>Fix</summary>

```diff
   const formatDate = (date: Date | number | undefined) => {
-    if (!date) return t("settings.timerSettingsConflict.unknownDate");
+    if (date === undefined) return t("settings.timerSettingsConflict.unknownDate");
     const d = typeof date === "number" ? new Date(date) : date;
     return d.toLocaleString();
   };
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/components/timer-settings-conflict-dialog.tsx` around
lines 25 - 28, The formatDate function incorrectly treats timestamp 0 as missing
due to the falsy check; change the missing-date check in formatDate to
explicitly test for null/undefined (e.g., date == null or date === null || date
=== undefined) so that numeric 0 is considered valid, then keep the existing
typeof date === "number" branch to convert numbers to Date and call
toLocaleString; ensure the early return still uses
t("settings.timerSettingsConflict.unknownDate").
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/party-finder/get-create-party-gathering-error-message.ts-27-31 (1)</summary><blockquote>

`27-31`: _⚠️ Potential issue_ | _🟡 Minor_

**Use nullish fallback for the response message.**

Line 29 uses `||`, so an empty server-provided message is treated as missing. Prefer `??` unless blank messages must be explicitly rejected.



<details>
<summary>Proposed fix</summary>

```diff
   if (responseStatus === 400) {
     return (
-      error.response?.data?.message ||
+      error.response?.data?.message ??
       i18n.t("settings.partyFinder.errors.invalidData")
     );
   }
```

</details>

As per coding guidelines, “Prefer `??` over `||` for nullish coalescing”.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/party-finder/get-create-party-gathering-error-message.ts`
around lines 27 - 31, Replace the boolean OR fallback with a nullish coalescing
fallback in the return for the responseStatus === 400 branch: when returning
error.response?.data?.message ||
i18n.t("settings.partyFinder.errors.invalidData"), use the nullish coalescing
operator (??) so an empty string from error.response?.data?.message is preserved
rather than treated as missing; update the expression where responseStatus,
error.response?.data?.message, and i18n.t(...) are used.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/i18n/translations/settings/party-finder.json-2-2 (1)</summary><blockquote>

`2-2`: _⚠️ Potential issue_ | _🟡 Minor_

**Localize the window title in the Polish bundle.**

This is the only top-level label left in English while the rest of the resource is Polish.



<details>
<summary>🌐 Proposed copy update</summary>

```diff
-  "windowTitle": "Party finder",
+  "windowTitle": "Wyszukiwarka grupy",
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/i18n/translations/settings/party-finder.json` at line 2,
The Polish translation bundle still has the top-level label "windowTitle" in
English; update the value of the "windowTitle" key in the party-finder.json
Polish resource to the proper Polish string (e.g., "Wyszukiwarka drużyn" or
other agreed translation) so the entire resource is localized; locate the
"windowTitle" entry in
apps/game-client/src/i18n/translations/settings/party-finder.json and replace
the English text with the Polish copy.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/i18n/translations/settings/detector.json-16-16 (1)</summary><blockquote>

`16-16`: _⚠️ Potential issue_ | _🟡 Minor_

**Keep the empty-state copy fully localized.**

`Detector` stands out as English in the Polish translation bundle.



<details>
<summary>🌐 Proposed copy update</summary>

```diff
-    "emptyState": "Brak reguł routingu. Detector będzie działał lokalnie bez wysyłki na Discord.",
+    "emptyState": "Brak reguł routingu. Wykrywacz będzie działał lokalnie bez wysyłki na Discord.",
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/i18n/translations/settings/detector.json` at line 16,
The Polish translation for the empty state contains an English word "Detector";
update the value for the "emptyState" key to use fully localized Polish (e.g.,
replace "Detector" with "Detektor" and keep the rest of the sentence intact) so
the string reads like "Brak reguł routingu. Detektor będzie działał lokalnie bez
wysyłki na Discord." Ensure you edit the "emptyState" entry in the detector
translation JSON (key "emptyState") to perform this change.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/settings/components/notifications/notifications-settings-tab.test.tsx-73-93 (1)</summary><blockquote>

`73-93`: _⚠️ Potential issue_ | _🟡 Minor_

**Test the `loading` delayed state too.**

This test name covers “loading and saving”, but it only exercises `saving`, so a regression in the `loading` branch would still pass.



<details>
<summary>🧪 Proposed fix</summary>

```diff
-  it("delays visible sync status updates for loading and saving states", () => {
+  it.each(["loading", "saving"] as const)(
+    "delays visible sync status updates for %s state",
+    (status) => {
     vi.useFakeTimers();
 
     const { rerender } = render(<NotificationsSettingsTab />);
 
     expect(screen.getByTestId("sync-status")).toHaveTextContent("idle");
 
     mockUseGameAccountPreferencesSyncStatus.mockReturnValue({
-      status: "saving",
+      status,
       error: null,
     });
     rerender(<NotificationsSettingsTab />);
 
     expect(screen.getByTestId("sync-status")).toHaveTextContent("idle");
@@
-    expect(screen.getByTestId("sync-status")).toHaveTextContent("saving");
-  });
+    expect(screen.getByTestId("sync-status")).toHaveTextContent(status);
+  });
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/settings/components/notifications/notifications-settings-tab.test.tsx`
around lines 73 - 93, The test in NotificationsSettingsTab.test should also
exercise the "loading" branch: after initial render and idle assertion, set
mockUseGameAccountPreferencesSyncStatus to return { status: "loading", error:
null }, rerender NotificationsSettingsTab, assert the visible sync-status
remains "idle", advance fake timers by the same delay used for debouncing, then
assert the sync-status shows "loading"; then proceed to change the mock to {
status: "saving", error: null }, rerender, repeat the delay/advance and assert
"saving" to cover both branches (use the existing
vi.useFakeTimers/act/vi.advanceTimersByTime and the same test utilities).
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/party-finder/components/create-party-gathering-form.tsx-21-39 (1)</summary><blockquote>

`21-39`: _⚠️ Potential issue_ | _🟡 Minor_

**Localize all user-facing Zod error messages in this schema.**

The cross-field level comparison is translated, but `description.max(200)` and the numeric validators for `minLvl`/`maxLvl` (.min(1), .max(500)) still emit default Zod error messages. These are rendered directly to users at lines 176–180 and 209–213, violating the i18n guideline for all static text in frontend apps.

Pass the `t` function to `getFormSchema` and add custom messages to each validator:

<details>
<summary>Suggested fix</summary>

```diff
+import type { TFunction } from "i18next";
 import { useTranslation } from "react-i18next";
 
-const getFormSchema = (levelErrorMessage: string) =>
+const getFormSchema = (t: TFunction) =>
   z
     .object({
-      description: z.string().max(200).optional(),
-      minLvl: z.coerce.number().min(1).max(500).optional().or(z.literal("")),
-      maxLvl: z.coerce.number().min(1).max(500).optional().or(z.literal("")),
+      description: z
+        .string()
+        .max(
+          200,
+          t("settings.partyFinder.createForm.descriptionTooLong", {
+            count: 200,
+          }),
+        )
+        .optional(),
+      minLvl: z.coerce
+        .number()
+        .min(1, t("settings.partyFinder.createForm.levelMin", { min: 1 }))
+        .max(500, t("settings.partyFinder.createForm.levelMax", { max: 500 }))
+        .optional()
+        .or(z.literal("")),
+      maxLvl: z.coerce
+        .number()
+        .min(1, t("settings.partyFinder.createForm.levelMin", { min: 1 }))
+        .max(500, t("settings.partyFinder.createForm.levelMax", { max: 500 }))
+        .optional()
+        .or(z.literal("")),
     })
       .refine(
         (data) => {
@@ -35,7 +52,7 @@
         },
         {
-          message: levelErrorMessage,
+          message: t("settings.partyFinder.createForm.minMaxLevelError"),
           path: ["minLvl"],
         },
       );
@@ -61,7 +78,7 @@
     reset,
   } = useForm<FormData>({
     resolver: zodResolver(
-      getFormSchema(t("settings.partyFinder.createForm.minMaxLevelError")),
+      getFormSchema(t),
     ) as never,
```

Add these translation keys to your `createForm` message bundle.

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/party-finder/components/create-party-gathering-form.tsx`
around lines 21 - 39, getFormSchema currently uses default Zod messages for
description.max and numeric validators on minLvl/maxLvl; update the signature to
accept the i18n translator (t) and replace the validators with custom message
options (e.g., description: z.string().max(200, { message:
t('createForm.descriptionMax') }).optional(), and for minLvl/maxLvl use
z.coerce.number().min(1, { message: t('createForm.levelMin') }).max(500, {
message: t('createForm.levelMax') }).optional().or(z.literal("", { message:
t('createForm.emptyLevel') })) ), keep the existing .refine for cross-field
comparison but use t('createForm.levelRange') for its message, and add the
corresponding keys to the createForm message bundle; update callers of
getFormSchema to pass the t function.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/notifications/components/notifications-list.test.tsx-97-114 (1)</summary><blockquote>

`97-114`: _⚠️ Potential issue_ | _🟡 Minor_

**Install the `scrollTo` spy before rendering.**

This test patches `scrollTo` after `render`, so a mount-time scroll could already have happened and still pass unnoticed. Patch the prototype before rendering, then assert no calls.

<details>
<summary>🧪 Proposed test fix</summary>

```diff
   it("does not scroll on mount when there is no animation cycle", () => {
+    const scrollToSpy = vi.fn();
+    Object.defineProperty(HTMLDivElement.prototype, "scrollTo", {
+      configurable: true,
+      value: scrollToSpy,
+    });
+
     render(<NotificationsList notifications={[createStoredNotification()]} />);
 
     const viewport = document.querySelector(
       "[data-radix-scroll-area-viewport]",
     );
@@
-    const scrollToSpy = vi.fn();
-    Object.defineProperty(viewport, "scrollTo", {
-      configurable: true,
-      value: scrollToSpy,
-    });
-
     expect(scrollToSpy).not.toHaveBeenCalled();
   });
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/notifications/components/notifications-list.test.tsx`
around lines 97 - 114, The test currently patches viewport.scrollTo after
render, so move the spy installation to before mounting: define a spy on
HTMLDivElement.prototype.scrollTo (e.g., via
Object.defineProperty(HTMLDivElement.prototype, "scrollTo", ...)) prior to
calling render(<NotificationsList ... />) and then render the component with
createStoredNotification; after render, query the viewport and assert the spy
(scrollToSpy) was not called; ensure the prototype patch is
configurable/restored after the test to avoid leaking into other tests.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/command/command.tsx-82-82 (1)</summary><blockquote>

`82-82`: _⚠️ Potential issue_ | _🟡 Minor_

**Prefer `??` over `||` unless falsy-coalescing (empty string → undefined) is intentional.**

Since `submissionMessage` is typed as `string` and can be empty (per tests), the current `|| undefined` will convert empty strings to `undefined` only for the party command while leaving it unchanged elsewhere. If this intentional distinction is needed, add a comment explaining the behavior; otherwise, switch to `??` to comply with guidelines and be consistent with other usages.

```diff
-      const description = submissionState.submissionMessage || undefined;
+      const description = submissionState.submissionMessage ?? undefined;
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/command/command.tsx` at line 82, The line
setting description uses the || operator which turns an empty string into
undefined; update the expression to use the nullish coalescing operator
(submissionState.submissionMessage ?? undefined) to preserve empty strings
unless the empty→undefined behavior is intentional—if it is intentional, add an
explicit comment next to the assignment explaining why. Target the assignment
that references submissionState.submissionMessage (the description variable) and
make this change to match other usages.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (18)</summary><blockquote>

<details>
<summary>.agents/skills/stitch-design-taste/SKILL.md (1)</summary><blockquote>

`84-84`: **Clarify the minimum body-text rule (`1rem` vs `14px`).**

Line 84 mixes two different minima (`1rem` and `14px`). In standard setups, `1rem` is 16px, so this is ambiguous for downstream agents.  


<details>
<summary>Suggested doc patch</summary>

```diff
-- **Typography Scaling:** Headlines scale via `clamp()`. Body text minimum `1rem`/`14px`
+- **Typography Scaling:** Headlines scale via `clamp()`. Body text minimum `1rem` (16px base), unless a non-default root size is explicitly defined.
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/stitch-design-taste/SKILL.md at line 84, Clarify the
ambiguous minimum body-text rule in the "Typography Scaling" bullet so it
doesn't mix units: update the line that currently reads "**Typography Scaling:**
Headlines scale via `clamp()`. Body text minimum `1rem`/`14px`" to state a
single definite minimum (e.g., "Body text minimum 1rem (16px)" or "Body text
minimum 14px (≈0.875rem)") or explicitly explain the environment where 1rem
equals 14px; ensure the "Typography Scaling" bullet (the line mentioning clamp()
and body text minimum) is updated accordingly.
```

</details>

</blockquote></details>
<details>
<summary>.agents/skills/stitch-design-taste/DESIGN.md (1)</summary><blockquote>

`83-83`: **Resolve conflicting typography minimums in this spec.**

Line 83 says body text stays `1rem` minimum and also “never below `14px`”. This is internally inconsistent unless root font-size assumptions are specified.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/stitch-design-taste/DESIGN.md at line 83, The spec's
"Typography Scaling" line conflicts by requiring body text to stay "1rem"
minimum and also "Never shrink body below 14px"; resolve by choosing one
consistent minimum and adding the root-font-size assumption: replace that
sentence in the "Typography Scaling" section with a single rule such as "Body
text minimum: 14px (0.875rem assuming 16px root)" or alternatively "Body text
minimum: 1rem (assumes 16px root); do not go below 14px" and include the root
font-size assumption and the use of clamp() for headlines (retain `clamp()` and
the 375px readability requirement) so implementers know which baseline to use.
```

</details>

</blockquote></details>
<details>
<summary>.agents/skills/gpt-taste/SKILL.md (2)</summary><blockquote>

`72-72`: **Optional: Simplify redundant phrase.**

Similar to line 43, the phrase "zero empty spaces" is flagged by static analysis as redundant. Consider "no empty spaces" or "zero spaces" for consistency.


<details>
<summary>✍️ Suggested simplification</summary>

```diff
-4. **Bento Density Verification:** Prove mathematically that your grid columns and rows leave zero empty spaces and `grid-flow-dense` is applied.
+4. **Bento Density Verification:** Prove mathematically that your grid columns and rows leave no empty spaces and `grid-flow-dense` is applied.
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/gpt-taste/SKILL.md at line 72, Replace the redundant phrase
"zero empty spaces" in the SKILL.md bullet ("4. **Bento Density Verification:**
... leave zero empty spaces and `grid-flow-dense` is applied") with a simpler
form such as "no empty spaces" (or "zero spaces") for consistency with the
earlier phrasing on line 43; update the bullet text to read e.g. "leave no empty
spaces and `grid-flow-dense` is applied."
```

</details>

---

`43-43`: **Optional: Simplify redundant phrase.**

The phrase "Zero Empty Space" is flagged by static analysis as redundant. Consider "No Empty Space" or "Zero Space" for cleaner phrasing.


<details>
<summary>✍️ Suggested simplification</summary>

```diff
-- **Zero Empty Space in Grids:** LLMs notoriously leave blank, dead cells in CSS grids.
+- **No Empty Space in Grids:** LLMs notoriously leave blank, dead cells in CSS grids.
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/gpt-taste/SKILL.md at line 43, The header phrase "Zero Empty
Space in Grids" is flagged as redundant; update the sentence in SKILL.md to use
a simpler phrase such as "No Empty Space in Grids" (or "Zero Space in Grids")
while keeping the required instruction intact: retain references to Tailwind's
`grid-flow-dense` (`grid-auto-flow: dense`) and the requirement to verify
`col-span` and `row-span` interlock for every Bento Grid so no empty cells
remain. Ensure the rest of the sentence and symbols (`grid-flow-dense`,
`col-span`, `row-span`, Bento Grid) are unchanged.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/command/command-state.test.ts (1)</summary><blockquote>

`27-38`: **Add a regression case for `!grp` token boundaries.**

Please cover inputs like `!grupa` or `!grpfoo` so the parser does not accidentally classify non-`!grp` commands as party mode.

<details>
<summary>🧪 Proposed test</summary>

```diff
   it("extracts the !grp description and keeps the party mode", () => {
     expect(
       getCommandState("!grp szukam tanca", { selectedGuildCount: 1 }),
     ).toMatchObject({
       mode: "party",
       isNotification: true,
       isPartyCommand: true,
       hasContent: true,
       canSubmit: true,
       submissionMessage: "szukam tanca",
     });
   });
+
+  it("does not treat words starting with !grp as the party command", () => {
+    expect(getCommandState("!grupa", { selectedGuildCount: 1 })).toMatchObject({
+      mode: "notification",
+      isNotification: true,
+      isPartyCommand: false,
+      submissionMessage: "grupa",
+    });
+  });
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/command/command-state.test.ts` around lines 27
- 38, Add regression tests in the same test suite for getCommandState to ensure
token boundaries are respected: add cases like getCommandState("!grupa
something", { selectedGuildCount: 1 }) and getCommandState("!grpfoo", {
selectedGuildCount: 1 }) and assert that isPartyCommand is false (and mode is
not "party") while the submissionMessage/hasContent reflect the full input
(i.e., the parser should not strip the leading token as a party command); place
these new it() blocks alongside the existing "!grp" test so getCommandState is
explicitly verified to reject "!grupa" and "!grpfoo" as party commands.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/settings/components/timers/components/hidden-colors-list.tsx (1)</summary><blockquote>

`37-37`: **Prefer `??` over `||` for the fallback.**

`colorNames[colorId] || getDefaultColorName(colorId)` will also fall back when the custom name is an empty string. If that is actually desired, `||` is fine; otherwise switch to `??` so only `undefined`/`null` trigger the default — this also matches the repo style guide.

<details>
<summary>Proposed change</summary>

```diff
-                {colorNames[colorId] || getDefaultColorName(colorId)}
+                {colorNames[colorId] ?? getDefaultColorName(colorId)}
```
</details>

As per coding guidelines: "Prefer `??` over `||` for nullish coalescing".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/settings/components/timers/components/hidden-colors-list.tsx`
at line 37, The expression using logical OR should use nullish coalescing so
empty strings are preserved: replace the fallback expression in HiddenColorsList
(where colorNames[colorId] is used with getDefaultColorName(colorId)) to use ??
instead of ||; update the JSX line that references colorNames, colorId, and
getDefaultColorName so it reads as a nullish coalescing fallback (only
null/undefined fall back to getDefaultColorName).
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/notifications/components/single-notification-message.test.tsx (1)</summary><blockquote>

`8-20`: **Consider a typed factory instead of `as never` for the notification fixture.**

Casting the partial object via `as never` bypasses the `notification` prop's type contract entirely. If the `Notification` shape changes (new required field, renamed key), this test will silently keep compiling while the production component may break at runtime. A small typed fixture builder (e.g. `buildNotification(overrides)`) returning the real type would surface such drift during refactors and makes the test more resilient.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/notifications/components/single-notification-message.test.tsx`
around lines 8 - 20, Replace the unsafe "as never" test fixture with a typed
factory: implement a buildNotification(overrides?: Partial<Notification>):
Notification helper that returns a fully populated Notification object (provide
sensible defaults for notificationId, discordId, guildId, world, createdAt,
message, servers, etc.), then use SingleNotificationMessage with
notification={buildNotification({ notificationId: "message-1", ... })}; this
ensures the fixture matches the actual Notification type and surfaces type drift
in SingleNotificationMessage's props.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/timers/components/timers-filters.tsx (1)</summary><blockquote>

`185-185`: **Prefer `??` over `||` for the fallback.**

Per coding guidelines, use nullish coalescing. Empty-string default color names (unlikely but possible) would otherwise be unintentionally replaced by the fallback.

<details>
<summary>♻️ Proposed change</summary>

```diff
-                    {defaultColorNames[colorId] || getDefaultColorName(colorId)}
+                    {defaultColorNames[colorId] ?? getDefaultColorName(colorId)}
```
</details>

As per coding guidelines: "Prefer `??` over `||` for nullish coalescing".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/timers/components/timers-filters.tsx` at line
185, Replace the logical-OR fallback with nullish-coalescing in the render
expression: in the component that uses defaultColorNames and getDefaultColorName
(the expression referencing defaultColorNames[colorId] ||
getDefaultColorName(colorId)), change the fallback to use ?? so empty-string or
falsey but non-null/undefined values from defaultColorNames[colorId] are
preserved (i.e., use defaultColorNames[colorId] ??
getDefaultColorName(colorId)).
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/timers/utils/color-statistics.ts (1)</summary><blockquote>

`47-47`: **Prefer `??` over `||` for fallback chain.**

Guideline-aligned nullish coalescing avoids collapsing accidental empty-string names.

<details>
<summary>♻️ Proposed change</summary>

```diff
-      name: defaultColorNames[color] || getDefaultColorName(color) || color,
+      name: defaultColorNames[color] ?? getDefaultColorName(color) ?? color,
```
</details>

As per coding guidelines: "Prefer `??` over `||` for nullish coalescing".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/timers/utils/color-statistics.ts` at line 47,
Replace the logical-OR fallbacks in the color name assignment to use nullish
coalescing so empty-string names are preserved: change the expression that sets
the name (which currently uses defaultColorNames[color] ||
getDefaultColorName(color) || color) to use ?? between defaultColorNames[color],
getDefaultColorName(color), and color, referencing the same identifiers
(defaultColorNames and getDefaultColorName) so the fallback chain only triggers
for null or undefined.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/timers/components/timer-color-picker.tsx (1)</summary><blockquote>

`71-71`: **Prefer `??` over `||` for the fallback.**

<details>
<summary>♻️ Proposed change</summary>

```diff
-                {defaultColorNames[id] || getDefaultColorName(id)}
+                {defaultColorNames[id] ?? getDefaultColorName(id)}
```
</details>

As per coding guidelines: "Prefer `??` over `||` for nullish coalescing".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/timers/components/timer-color-picker.tsx` at
line 71, Replace the logical-or fallback with nullish coalescing: in the JSX
expression that renders the color name (using defaultColorNames and
getDefaultColorName with id), change the fallback operator from || to ?? so that
undefined/null values fall back to getDefaultColorName(id) while preserving
falsy but valid values like empty string or 0.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/notifications/components/single-notification-npc.test.tsx (1)</summary><blockquote>

`9-32`: **Replace `as never` test fixtures with properly-typed fixtures.**

The casts at lines 32 and 56 bypass type checking for the `SingleNotificationNpc` component props. If the notification shape changes, these tests will still compile with stale fixtures. Instead, extract the prop type and assign fixtures to that type so incompatibilities surface at test time.

<details>
<summary>Example approach</summary>

```diff
+import type { ComponentProps } from "react";
 import { render, screen } from "@testing-library/react";
 import { describe, expect, it } from "vitest";
 import { SingleNotificationNpc } from "./single-notification-npc";
+
+type NotificationProp = ComponentProps<
+  typeof SingleNotificationNpc
+>["notification"];
```

Then declare each fixture as `NotificationProp` instead of casting inline.
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/notifications/components/single-notification-npc.test.tsx`
around lines 9 - 32, Replace the inline "as never" casts with properly‑typed
fixtures by extracting the notification prop type from the SingleNotificationNpc
component (e.g., via ComponentProps<typeof
SingleNotificationNpc>['notification'] or the exported prop type) and declare
each test fixture variable with that type, then assign the object literal to the
typed variable and use that variable in the test render; remove the "as never"
casts so TypeScript will catch any shape mismatches between the fixture and
SingleNotificationNpc's props.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/command/components/command-suggestions.tsx (1)</summary><blockquote>

`4-70`: **Remove `useMemo` here.**

`COMMAND_SUGGESTIONS` is tiny, and the repository’s React Compiler policy disallows manual React memoization in TSX.

<details>
<summary>♻️ Proposed refactor</summary>

```diff
-import { useEffect, useMemo, useState, type FC } from "react";
+import { useEffect, useState, type FC } from "react";
@@
-  const filtered = useMemo(
-    () =>
-      COMMAND_SUGGESTIONS.filter((suggestion) =>
-        suggestion.prefix.startsWith(commandQuery),
-      ).sort((left, right) => {
-        const leftExactMatch = left.prefix === commandQuery ? 1 : 0;
-        const rightExactMatch = right.prefix === commandQuery ? 1 : 0;
+  const filtered = COMMAND_SUGGESTIONS.filter((suggestion) =>
+    suggestion.prefix.startsWith(commandQuery),
+  ).sort((left, right) => {
+    const leftExactMatch = left.prefix === commandQuery ? 1 : 0;
+    const rightExactMatch = right.prefix === commandQuery ? 1 : 0;
 
-        if (leftExactMatch !== rightExactMatch) {
-          return rightExactMatch - leftExactMatch;
-        }
+    if (leftExactMatch !== rightExactMatch) {
+      return rightExactMatch - leftExactMatch;
+    }
 
-        return left.prefix.length - right.prefix.length;
-      }),
-    [commandQuery],
-  );
+    return left.prefix.length - right.prefix.length;
+  });
```

</details>

   
As per coding guidelines, “React Compiler handles memoization — do not use `memo`, `useMemo`, or `useCallback`”.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/command/components/command-suggestions.tsx`
around lines 4 - 70, The code uses useMemo to compute filtered suggestions
inside useCommandSuggestions which violates the React Compiler policy; replace
the memoized computation with a plain synchronous computation: remove useMemo
import from the top, change the filtered declaration to a simple const filtered
= COMMAND_SUGGESTIONS.filter(...).sort(...) (keeping the same filter/sort logic
and dependency on commandQuery), and ensure useCommandSuggestions,
COMMAND_SUGGESTIONS, and the filtered variable remain otherwise unchanged so
selection and rendering logic still work.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/command/components/command-context-panel.tsx (1)</summary><blockquote>

`36-44`: **Replace the nested ternary with explicit branching.**

The recipient-details logic is easier to read and maintain as `if` branches, especially with localized overflow text.

<details>
<summary>♻️ Proposed refactor</summary>

```diff
-  const recipientsDetails =
-    selectedGuildNames.length > 0
-      ? hiddenGuildCount > 0
-        ? t("settings.command.recipients.selectedNamesOverflow", {
-            names: visibleGuildNames,
-            count: hiddenGuildCount,
-          })
-        : visibleGuildNames
-      : t("settings.command.recipients.missingHint");
+  let recipientsDetails = t("settings.command.recipients.missingHint");
+
+  if (selectedGuildNames.length > 0) {
+    recipientsDetails = visibleGuildNames;
+  }
+
+  if (hiddenGuildCount > 0) {
+    recipientsDetails = t("settings.command.recipients.selectedNamesOverflow", {
+      names: visibleGuildNames,
+      count: hiddenGuildCount,
+    });
+  }
```

</details>

  
As per coding guidelines, “Avoid nested (chained) ternary expressions — use early returns or `if`/`else if` instead”.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/command/components/command-context-panel.tsx`
around lines 36 - 44, The nested ternary that computes recipientsDetails should
be rewritten using explicit if/else branching: check selectedGuildNames.length >
0 first, then inside that block check hiddenGuildCount > 0 and set
recipientsDetails to t("settings.command.recipients.selectedNamesOverflow", {
names: visibleGuildNames, count: hiddenGuildCount }) or to visibleGuildNames
accordingly; otherwise set it to t("settings.command.recipients.missingHint").
Update the logic around selectedGuildNames, hiddenGuildCount, visibleGuildNames,
and the t(...) calls in command-context-panel.tsx (where recipientsDetails is
defined) to replace the nested ternary with clear if / else if / else
statements.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/store/notifications.store.test.ts (1)</summary><blockquote>

`73-81`: **Add `afterEach` to restore real timers.**

`vi.useFakeTimers()` is installed in `beforeEach` but never reset. If this suite runs in the same worker as files that assume real timers (and don't explicitly switch), timing-sensitive tests there can become flaky.

<details>
<summary>♻️ Proposed cleanup</summary>

```diff
   beforeEach(() => {
     vi.useFakeTimers();
     vi.setSystemTime(new Date("2026-04-19T10:00:00.000Z"));
     ...
   });
+
+  afterEach(() => {
+    vi.useRealTimers();
+  });
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/store/notifications.store.test.ts` around lines 73 - 81,
Add an afterEach that restores real timers to complement the beforeEach that
calls vi.useFakeTimers(); specifically, add an afterEach hook that calls
vi.useRealTimers() (or vi.clearAllTimers() then vi.useRealTimers()) so tests
that assume real timers are not affected; update the test file's suite
surrounding beforeEach/vi.useFakeTimers to include this afterEach to ensure
timers are reset between tests.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/npc-detector/components/npc-list-item.test.tsx (2)</summary><blockquote>

`144-198`: **Restore stubbed globals and clean up spies to prevent cross-test leakage.**

A few cleanup gaps:

1. `vi.stubGlobal("message", vi.fn())` at line 197 is never unstubbed — `window.message` will bleed into any subsequent tests in the same worker.
2. `vi.spyOn(console, "error")...mockImplementation(...)` at line 341-343 isn't restored; `vi.clearAllMocks()` does not restore original implementations.

<details>
<summary>♻️ Proposed cleanup</summary>

```diff
   beforeEach(() => {
     vi.clearAllMocks();
     vi.useRealTimers();
     ...
     vi.stubGlobal("message", vi.fn());
   });
+
+  afterEach(() => {
+    vi.unstubAllGlobals();
+    vi.restoreAllMocks();
+  });
```

Note: if you add `vi.restoreAllMocks()`, verify that the module-level `vi.fn()`s declared outside `describe` are still usable across tests (they are — `vi.restoreAllMocks` only restores `vi.spyOn` targets, not standalone `vi.fn()`s).

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/npc-detector/components/npc-list-item.test.tsx`
around lines 144 - 198, Add an afterEach cleanup that unstubs globals and
restores spies: call vi.unstubAllGlobals() (to remove the
vi.stubGlobal("message", ...) global) and vi.restoreAllMocks() (to restore any
vi.spyOn console.error mock and similar spies); place this in the same
describe("NpcListItem", ...) block so it runs after each test and prevents
cross-test leakage while keeping module-level vi.fn() mocks intact.
```

</details>

---

`243-311`: **Index-based button lookups are brittle — prefer accessible name/role queries.**

`screen.getAllByRole("button")[0]` and `[1]` (lines 243, 308-309, 354-355) depend on DOM insertion order and will silently break if a neutral button (e.g., tooltip trigger, icon button) is inserted before the send/gather controls. Prefer an accessible name query so the assertion fails with a clear message:

```diff
-    const [sendButton] = screen.getAllByRole("button");
+    const sendButton = screen.getByRole("button", { name: /wyślij|send/i });
```

Apply the same approach to the `gatherButton` selection.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/npc-detector/components/npc-list-item.test.tsx`
around lines 243 - 311, Replace brittle index-based button lookups in
npc-list-item.test.tsx (the uses of screen.getAllByRole("button")[0] assigned to
sendButton and screen.getAllByRole("button")[1] assigned to gatherButton) with
accessible role+name queries: locate the send and gather controls via
screen.getByRole('button', { name: /send/i }) and screen.getByRole('button', {
name: /gather/i }) (or the exact accessible labels used in the UI), and update
the tests that reference sendButton and gatherButton accordingly so they fail
clearly if the labeled buttons are missing.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/command/command.tsx (1)</summary><blockquote>

`59-65`: **Duplicated `onSelect` handler — extract once and reuse.**

The inline `(prefix) => { setValue("message", prefix); textareaRef.current?.focus(); }` appears in both `useCommandSuggestions` (line 61-64) and `<CommandSuggestions .../>` (line 183-186). Extract a single handler to avoid drift.

<details>
<summary>♻️ Proposed refactor</summary>

```diff
+  const handleSuggestionSelect = (prefix: string) => {
+    setValue("message", prefix);
+    textareaRef.current?.focus();
+  };
+
   const suggestions = useCommandSuggestions({
     inputValue: messageValue,
-    onSelect: (prefix) => {
-      setValue("message", prefix);
-      textareaRef.current?.focus();
-    },
+    onSelect: handleSuggestionSelect,
   });
@@
                 <CommandSuggestions
-                  onSelect={(prefix) => {
-                    setValue("message", prefix);
-                    textareaRef.current?.focus();
-                  }}
+                  onSelect={handleSuggestionSelect}
```
</details>




Also applies to: 181-190

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/command/command.tsx` around lines 59 - 65,
Extract the duplicated onSelect handler into a single function and use it in
both places: define a const handleSelect = (prefix: string) => {
setValue("message", prefix); textareaRef.current?.focus(); } near the component
scope (so it can access setValue and textareaRef), then pass handleSelect to
useCommandSuggestions(...) and as the onSelect prop on <CommandSuggestions
.../>; ensure the function name is unique (handleSelect) and that both
references replace the inline arrow handlers to prevent drift.
```

</details>

</blockquote></details>
<details>
<summary>apps/game-client/src/features/notifications/components/single-notification.test.tsx (1)</summary><blockquote>

`186-241`: **Add an `afterEach` to restore timers and the `SVGElement.prototype.animate` override.**

`vi.useFakeTimers()` and `Object.defineProperty(SVGElement.prototype, "animate", ...)` are set in `beforeEach` without teardown. The prototype override persists across tests/files in the same worker and fake timers remain active if a subsequent file doesn't set them explicitly — both can introduce flaky behavior in unrelated suites.



<details>
<summary>♻️ Proposed cleanup</summary>

```diff
 describe("SingleNotification", () => {
+  const originalAnimate = Object.getOwnPropertyDescriptor(
+    SVGElement.prototype,
+    "animate",
+  );
+
   beforeEach(() => {
     ...
   });
+
+  afterEach(() => {
+    vi.useRealTimers();
+    if (originalAnimate) {
+      Object.defineProperty(SVGElement.prototype, "animate", originalAnimate);
+    } else {
+      // `@ts-expect-error` removing the stub
+      delete SVGElement.prototype.animate;
+    }
+  });
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/notifications/components/single-notification.test.tsx`
around lines 186 - 241, Add an afterEach that calls vi.useRealTimers() to
restore real timers and removes/restores the SVG animate override set in
beforeEach (undo Object.defineProperty on SVGElement.prototype.animate, e.g.
delete SVGElement.prototype.animate or restore the original value captured
before override), referencing the mockAnimate used in beforeEach and the
vi.useFakeTimers()/vi.useRealTimers() calls and SVGElement.prototype.animate to
ensure no prototype or timer leaks across tests.
```

</details>

</blockquote></details>

</blockquote></details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

* **COLOR CONSISTENCY:** Stick to one palette for the entire output. Do not fluctuate between warm and cool grays within the same project.

**Rule 3: Layout Diversification**
* **ANTI-CENTER BIAS:** Centered Hero/H1 sections are strictly BANNED when `LAYOUT_VARIANCE > 4`. Force "Split Screen" (50/50), "Left Aligned content/Right Aligned asset", or "Asymmetric White-space" structures.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix config variable mismatch (LAYOUT_VARIANCE vs DESIGN_VARIANCE).

Line 50 references LAYOUT_VARIANCE, but the defined baseline variable is DESIGN_VARIANCE (Line 9). This creates conflicting control logic in the spec.

Suggested fix
-* **ANTI-CENTER BIAS:** Centered Hero/H1 sections are strictly BANNED when `LAYOUT_VARIANCE > 4`. Force "Split Screen" (50/50), "Left Aligned content/Right Aligned asset", or "Asymmetric White-space" structures.
+* **ANTI-CENTER BIAS:** Centered Hero/H1 sections are strictly BANNED when `DESIGN_VARIANCE > 4`. Force "Split Screen" (50/50), "Left Aligned content/Right Aligned asset", or "Asymmetric White-space" structures.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* **ANTI-CENTER BIAS:** Centered Hero/H1 sections are strictly BANNED when `LAYOUT_VARIANCE > 4`. Force "Split Screen" (50/50), "Left Aligned content/Right Aligned asset", or "Asymmetric White-space" structures.
* **ANTI-CENTER BIAS:** Centered Hero/H1 sections are strictly BANNED when `DESIGN_VARIANCE > 4`. Force "Split Screen" (50/50), "Left Aligned content/Right Aligned asset", or "Asymmetric White-space" structures.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/design-taste-frontend/SKILL.md at line 50, The rule
references the wrong config variable: replace the `LAYOUT_VARIANCE` token used
in the "ANTI-CENTER BIAS" rule with the actual baseline variable
`DESIGN_VARIANCE` (or alternatively rename the baseline `DESIGN_VARIANCE` to
`LAYOUT_VARIANCE` everywhere) so the condition is consistent; update the rule
text mentioning `LAYOUT_VARIANCE > 4` to `DESIGN_VARIANCE > 4` (or update the
baseline name) and ensure the same symbol (`DESIGN_VARIANCE` or
`LAYOUT_VARIANCE`) is used throughout the document, including the
header/baseline definition and every rule that references it.

Comment on lines +69 to +75
## 7. Subtle Motion & Micro-Animations
Motion should feel invisible — present but never distracting. The goal is quiet sophistication, not spectacle.
- Scroll Entry: Elements fade in gently as they enter the viewport. Use `translateY(12px)` + `opacity: 0` resolving over `600ms` with `cubic-bezier(0.16, 1, 0.3, 1)`. Use `IntersectionObserver`, never `window.addEventListener('scroll')`.
- Hover States: Cards lift with an ultra-subtle shadow shift (`box-shadow` transitioning from `0 0 0` to `0 2px 8px rgba(0,0,0,0.04)` over `200ms`). Buttons respond with `scale(0.98)` on `:active`.
- Staggered Reveals: Lists and grid items enter with a cascade delay (`animation-delay: calc(var(--index) * 80ms)`). Never mount everything at once.
- Background Ambient Motion: Optional. A single, very slow-moving radial gradient blob (`animation-duration: 20s+`, `opacity: 0.02-0.04`) drifting behind hero sections. Must be applied to a `position: fixed; pointer-events: none` layer. Never on scrolling containers.
- Performance: Animate exclusively via `transform` and `opacity`. No layout-triggering properties (`top`, `left`, `width`, `height`). Use `will-change: transform` sparingly and only on actively animating elements.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Include reduced-motion handling in motion protocol requirements.

The motion section prescribes scroll-entry, stagger, and ambient animation patterns, but doesn’t specify behavior for users requesting reduced motion. Add explicit prefers-reduced-motion guidance so generated implementations are accessibility-safe by default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/minimalist-ui/SKILL.md around lines 69 - 75, Add explicit
prefers-reduced-motion guidance: for each motion rule (Scroll Entry, Hover
States, Staggered Reveals, Background Ambient Motion) require honoring `@media`
(prefers-reduced-motion: reduce) or a runtime reduce-motion flag so animations
are suppressed — e.g., skip IntersectionObserver entry animations and render
elements fully visible (opacity:1, transform:none), remove stagger delays
(animation-delay:0), disable ambient gradient animation, and prevent
hover/active transforms; recommend using a minimal non-animating fallback
(instant transition or 0ms) and document that implementations must check
prefers-reduced-motion before applying translateY/opacity/animation-duration
effects.

Comment on lines +67 to +75
- **Instant transitions with zero duration.** Add smooth transitions (200-300ms) to all interactive elements.
- **Missing focus ring.** Ensure visible focus indicators for keyboard navigation. This is an accessibility requirement, not optional.
- **No loading states.** Replace generic circular spinners with skeleton loaders that match the layout shape.
- **No empty states.** An empty dashboard showing nothing is a missed opportunity. Design a composed "getting started" view.
- **No error states.** Add clear, inline error messages for forms. Do not use `window.alert()`.
- **Dead links.** Buttons that link to `#`. Either link to real destinations or visually disable them.
- **No indication of current page in navigation.** Style the active nav link differently so users know where they are.
- **Scroll jumping.** Anchor clicks jump instantly. Add `scroll-behavior: smooth`.
- **Animations using `top`, `left`, `width`, `height`.** Switch to `transform` and `opacity` for GPU-accelerated, smooth animation.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add reduced-motion exceptions for animation and smooth scrolling directives.

Line 67 and Line 74 enforce motion globally, but there is no prefers-reduced-motion fallback. This can violate accessibility expectations and produce a poor UX for motion-sensitive users.

Suggested doc patch
 - **Instant transitions with zero duration.** Add smooth transitions (200-300ms) to all interactive elements.
+  - Respect reduced-motion preferences: disable or minimize non-essential transitions under `@media (prefers-reduced-motion: reduce)`.
 ...
 - **Scroll jumping.** Anchor clicks jump instantly. Add `scroll-behavior: smooth`.
+  - Apply smooth scrolling only when reduced motion is not requested.
🧰 Tools
🪛 LanguageTool

[grammar] ~67-~67: Ensure spelling is correct
Context: ...duration.** Add smooth transitions (200-300ms) to all interactive elements. - **Missi...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/redesign-existing-projects/SKILL.md around lines 67 - 75, The
doc currently mandates global motion (e.g., "scroll-behavior: smooth" and
animation guidance to use "transform" and "opacity") but omits a
prefers-reduced-motion fallback; update the guidance to require adding a CSS
prefers-reduced-motion media query that disables or reduces non-essential
animations/transitions and overrides smooth scrolling for motion-sensitive users
(e.g., recommend using `@media` (prefers-reduced-motion: reduce) to set
animations/transition-duration to 0ms or minimal and to reset scroll-behavior to
auto), and note that interactive components (focus rings, skeleton loaders,
active nav styling) should respect this reduced-motion setting too (refer to
"scroll-behavior: smooth", "transform and opacity", and "prefers-reduced-motion"
in the doc).

Comment on lines +95 to +99
- **Perpetual Micro-Loops:** Every active dashboard component has an infinite-loop state — Pulse on status dots, Typewriter on search bars, Float on feature icons, Shimmer on loading states
- **Staggered Orchestration:** Lists and grids mount with cascaded delays (`animation-delay: calc(var(--index) * 100ms)`). Waterfall reveals, never instant mount
- **Layout Transitions:** Smooth re-ordering via shared element IDs. Items swap positions with physics, simulating real-time intelligence
- **Hardware Rules:** Animate ONLY `transform` and `opacity`. Never `top`, `left`, `width`, `height`. Grain/noise filters on fixed, pointer-events-none pseudo-elements only
- **Performance:** CPU-heavy perpetual animations isolated in microscopic leaf components. Never trigger parent re-renders. Target 60fps minimum
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add reduced-motion fallback to motion intent requirements.

This section mandates infinite-loop behaviors and rich motion patterns without a reduced-motion path. The doc should explicitly require a prefers-reduced-motion variant.

🧰 Tools
🪛 LanguageTool

[grammar] ~99-~99: Ensure spelling is correct
Context: ...Never trigger parent re-renders. Target 60fps minimum ## 9. Anti-Patterns (Banned) -...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~99-~99: Ensure spelling is correct
Context: ... parent re-renders. Target 60fps minimum ## 9. Anti-Patterns (Banned) - No emojis — ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/stitch-design-taste/DESIGN.md around lines 95 - 99, Update
the motion requirements in the DESIGN.md section (items "Perpetual Micro-Loops",
"Staggered Orchestration", "Layout Transitions", and "Hardware Rules") to
mandate a prefers-reduced-motion fallback: add language that infinite-loop
effects (Pulse, Typewriter, Float, Shimmer) must have a non-animating
alternative state, staggered mounts using animation-delay (calc(var(--index) *
100ms)) must collapse to instantaneous or sequenced appearance, and
shared-element layout transitions must degrade to immediate swaps when
prefers-reduced-motion is set; also require CSS guidance to wrap heavy
animations in a `@media` (prefers-reduced-motion: reduce) rule that disables or
replaces transform/opacity effects and prevents CPU-heavy micro-components from
animating while still preserving accessibility.

Comment on lines +92 to +95
- **Perpetual Micro-Interactions:** Every active component should have an infinite loop state (Pulse, Typewriter, Float, Shimmer)
- **Staggered Orchestration:** Never mount lists instantly — use cascade delays for waterfall reveals
- **Performance:** Animate exclusively via `transform` and `opacity`. Never animate `top`, `left`, `width`, `height`. Grain/noise filters on fixed pseudo-elements only

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid unconditional infinite-loop animation requirements.

Line 92 currently mandates perpetual loops for every active component. Add a reduced-motion exception so generated implementations remain accessible.

Suggested doc patch
-- **Perpetual Micro-Interactions:** Every active component should have an infinite loop state (Pulse, Typewriter, Float, Shimmer)
+- **Perpetual Micro-Interactions:** Active components may use looped states (Pulse, Typewriter, Float, Shimmer) when motion is appropriate.
+  - Under `prefers-reduced-motion: reduce`, disable perpetual loops and keep only essential state changes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/stitch-design-taste/SKILL.md around lines 92 - 95, Update the
"Perpetual Micro-Interactions" rule so infinite-loop animations like Pulse,
Typewriter, Float, Shimmer include a reduced-motion opt-out: detect
prefers-reduced-motion (or a runtime reducedMotion flag) and disable or replace
loops with a single static state or subtle non-animating fallback; reference the
"Perpetual Micro-Interactions" heading and the listed animation types (Pulse,
Typewriter, Float, Shimmer) when changing the wording to require accessible
fallbacks that honor reduced-motion preferences.

Comment thread apps/game-client/src/features/command/command-state.ts Outdated
Comment thread apps/game-client/src/features/command/command.tsx Outdated
Comment on lines +33 to +39
expect(screen.getByText("Tester")).toBeInTheDocument();
expect(screen.getByText("(240w)")).toBeInTheDocument();
expect(screen.getByText("Szuka grupy")).toBeInTheDocument();
expect(screen.getByText('"Brakuje tanka"')).toBeInTheDocument();
expect(screen.getByText("Poziom: 200 - 260")).toHaveClass(
"ll:text-gray-300",
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd single-notification-party-gathering.tsx --type f --exec cat {}
rg -nP 'Szuka grupy|Poziom:' -C2

Repository: lootlog/monorepo

Length of output: 4936


SingleNotificationPartyGathering hardcodes Polish strings in violation of the i18n guideline.

The component contains hardcoded Polish text:

  • Line 23: "Szuka grupy"
  • Line 39: "Poziom: {minLvl} - {maxLvl}"

This violates the coding guideline: "All static text in frontend apps must use i18n — never hardcode user-facing strings."

An i18n translation already exists in chat.json under partyGathering.levelLabel for the level label. Refactor the component to use useTranslation() and replace hardcoded strings with t() calls, then update test assertions to work with the i18n provider.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/game-client/src/features/notifications/components/single-notification-party-gathering.test.tsx`
around lines 33 - 39, The tests and component use hardcoded Polish strings;
update the SingleNotificationPartyGathering component to import and use
useTranslation() and replace hardcoded labels ("Szuka grupy" and the level label
"Poziom: {minLvl} - {maxLvl}") with t('chat:partyGathering.someKey') (use the
existing chat.json key partyGathering.levelLabel for the level label and add
keys for the other strings if needed), then update tests in
single-notification-party-gathering.test.tsx to render the component with an
i18n provider (or mock useTranslation/t to return expected strings) and change
assertions to look up the translated text rather than hardcoded Polish text;
locate code by the SingleNotificationPartyGathering component and the test file
to apply these changes.

Comment on lines +80 to +98
it("does not scroll on mount when there is no detection cycle yet", () => {
render(<NpcsList npcs={[createNpc()]} />);

const viewport = document.querySelector(
"[data-radix-scroll-area-viewport]",
);

if (!(viewport instanceof HTMLDivElement)) {
throw new Error("Expected scroll viewport");
}

const scrollToSpy = vi.fn();
Object.defineProperty(viewport, "scrollTo", {
configurable: true,
value: scrollToSpy,
});

expect(scrollToSpy).not.toHaveBeenCalled();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This test is tautological — the spy is installed after mount, so it cannot observe mount behavior.

scrollToSpy is created and assigned to viewport.scrollTo after render(...) has already completed. If the component did call scrollTo on mount, it would have invoked the element's original method, not the spy. The final expect(scrollToSpy).not.toHaveBeenCalled() only proves that a freshly-created vi.fn() was never called — which is trivially true.

To actually assert "no scroll on mount", the spy must be installed before render. One approach:

🛠 Proposed fix
   it("does not scroll on mount when there is no detection cycle yet", () => {
-    render(<NpcsList npcs={[createNpc()]} />);
-
-    const viewport = document.querySelector(
-      "[data-radix-scroll-area-viewport]",
-    );
-
-    if (!(viewport instanceof HTMLDivElement)) {
-      throw new Error("Expected scroll viewport");
-    }
-
-    const scrollToSpy = vi.fn();
-    Object.defineProperty(viewport, "scrollTo", {
-      configurable: true,
-      value: scrollToSpy,
-    });
-
-    expect(scrollToSpy).not.toHaveBeenCalled();
+    const scrollToSpy = vi.fn();
+    const originalScrollTo = HTMLDivElement.prototype.scrollTo;
+    HTMLDivElement.prototype.scrollTo = scrollToSpy;
+
+    try {
+      render(<NpcsList npcs={[createNpc()]} />);
+      expect(scrollToSpy).not.toHaveBeenCalled();
+    } finally {
+      HTMLDivElement.prototype.scrollTo = originalScrollTo;
+    }
   });

The sibling test at lines 100-127 has the same ordering issue; it happens to work only because the scrollTo is triggered by a later store update, but the setup pattern is fragile and should be made consistent.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("does not scroll on mount when there is no detection cycle yet", () => {
render(<NpcsList npcs={[createNpc()]} />);
const viewport = document.querySelector(
"[data-radix-scroll-area-viewport]",
);
if (!(viewport instanceof HTMLDivElement)) {
throw new Error("Expected scroll viewport");
}
const scrollToSpy = vi.fn();
Object.defineProperty(viewport, "scrollTo", {
configurable: true,
value: scrollToSpy,
});
expect(scrollToSpy).not.toHaveBeenCalled();
});
it("does not scroll on mount when there is no detection cycle yet", () => {
const scrollToSpy = vi.fn();
const originalScrollTo = HTMLDivElement.prototype.scrollTo;
HTMLDivElement.prototype.scrollTo = scrollToSpy;
try {
render(<NpcsList npcs={[createNpc()]} />);
expect(scrollToSpy).not.toHaveBeenCalled();
} finally {
HTMLDivElement.prototype.scrollTo = originalScrollTo;
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/game-client/src/features/npc-detector/components/npcs-list.test.tsx`
around lines 80 - 98, The test is tautological because scrollToSpy is attached
after render; install the spy on the element prototype before mounting so
mount-time calls are observed: create a scrollToSpy and
Object.defineProperty(HTMLDivElement.prototype, "scrollTo", { configurable:
true, value: scrollToSpy }) (or Element.prototype.scrollTo) prior to calling
render(<NpcsList npcs={[createNpc()]} />), then render and assert scrollToSpy
was not called, and finally restore the original scrollTo; apply the same change
to the sibling test that also installs the spy too late.

@kamilwronka kamilwronka merged commit 7ec5d40 into develop Apr 19, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants