Skip to content

Feature: responsive pill count in emergency scenario visualisation for mobile#424

Open
mindmonk wants to merge 1 commit intodevelopfrom
feature/fix-emergency-visualization-mobile-pill-width
Open

Feature: responsive pill count in emergency scenario visualisation for mobile#424
mindmonk wants to merge 1 commit intodevelopfrom
feature/fix-emergency-visualization-mobile-pill-width

Conversation

@mindmonk
Copy link
Copy Markdown
Contributor

  • maxVisiblePills is now a computed value based on container width (2 on mobile < 380px, 3 on desktop)
  • Pill width is distributed across fewer pills, making names readable on small screens
  • Counter badge (+N) and index filters adapt dynamically to the visible pill count

Problem

On mobile viewports, the emergency scenario visualization tried to fit 3 pills into a narrow container (~320px), resulting in each pill being only ~68px wide.
User names were almost completely hidden due to truncation.

Solution

On containers narrower than 380px, only 2 pills are shown (~106px each), with the remaining council members indicated by the existing +N counter badge.

Desktop behavior (3 pills) is unchanged.

Screenshots

Before

localhost_3000_app_vaults (3)

After

localhost_3000_app_vaults (4)

@mindmonk mindmonk requested a review from overheadhunter March 18, 2026 10:49
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

Walkthrough

The EmergencyScenarioVisualization.vue component replaces a hard-coded constant maxVisiblePills value with a computed property that dynamically returns either 2 or 3 visible pills based on container width. When the container width is below 380 pixels, the component displays 2 pills; otherwise, it displays 3. All related calculations including overflow indicators, positioning, and rendering conditions are updated to reference this dynamic computed value instead of the fixed threshold.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested reviewers

  • SailReal
  • overheadhunter
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: making the pill count responsive for mobile devices in the emergency scenario visualization component.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem, solution, and providing before/after screenshots demonstrating the responsive pill count feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fix-emergency-visualization-mobile-pill-width
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (1)
frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue (1)

97-120: Observe the container itself instead of only window.resize.

Line 114 recalculates containerWidth only on viewport resizes, but this component is also used inside parent-controlled dialog/grid layouts. If that parent width changes without a window.resize, maxVisiblePills can stick on the wrong breakpoint. A ResizeObserver on pillContainer would make this truly container-driven.

♻️ Proposed change
 const pillContainer = ref<HTMLElement | null>(null);
 const containerWidth = ref(0);
+let resizeObserver: ResizeObserver | null = null;

 const maxVisiblePills = computed(() => {
   if (containerWidth.value > 0 && containerWidth.value < 380) return 2;
   return 3;
 });
@@
 onMounted(() => {
   nextTick(() => {
     updateContainerWidth();
-    window.addEventListener('resize', updateContainerWidth);
+    if (pillContainer.value) {
+      resizeObserver = new ResizeObserver(() => updateContainerWidth());
+      resizeObserver.observe(pillContainer.value);
+    }
   });
 });

 onBeforeUnmount(() => {
-  window.removeEventListener('resize', updateContainerWidth);
+  resizeObserver?.disconnect();
+  resizeObserver = null;
   stopRandomCouncilInterval();
 });

Please verify the supported browser matrix before relying solely on ResizeObserver; if needed, keep the current window.resize listener as a fallback.

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

In `@frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue`
around lines 97 - 120, The component only updates containerWidth on
window.resize, so when parent/layout changes without a viewport resize
maxVisiblePills can be wrong; attach a ResizeObserver to pillContainer (the ref)
in onMounted to call updateContainerWidth whenever the element's size changes,
keep the existing window 'resize' listener as a fallback, and in onBeforeUnmount
disconnect the ResizeObserver and still remove the window listener; ensure
updateContainerWidth reads pillContainer.value.clientWidth and call it once
after mounting, and keep stopRandomCouncilInterval() in onBeforeUnmount as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue`:
- Around line 123-127: The pillWidth computation divides by the visibility cap
and can produce negative values; change it to divide by the actual number of
rendered pills (use Math.min(maxVisiblePills.value, requiredKeyShares.value) or
the appropriate reactive that represents actual shown pills) instead of
maxVisiblePills.value, compute usableWidth as Math.max(containerWidth.value -
totalGap - 100, 0) to avoid negatives, then compute Math.floor(usableWidth /
numPills) and only apply the fallback (200) when the result is not a positive
finite number (e.g., if result <= 0 or !isFinite(result) then return 200).
Reference pillWidth, maxVisiblePills, requiredKeyShares, and containerWidth when
updating the code.

---

Nitpick comments:
In `@frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue`:
- Around line 97-120: The component only updates containerWidth on
window.resize, so when parent/layout changes without a viewport resize
maxVisiblePills can be wrong; attach a ResizeObserver to pillContainer (the ref)
in onMounted to call updateContainerWidth whenever the element's size changes,
keep the existing window 'resize' listener as a fallback, and in onBeforeUnmount
disconnect the ResizeObserver and still remove the window listener; ensure
updateContainerWidth reads pillContainer.value.clientWidth and call it once
after mounting, and keep stopRandomCouncilInterval() in onBeforeUnmount as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bbd9244e-1114-4161-896e-cc02b8c74a13

📥 Commits

Reviewing files that changed from the base of the PR and between 293b03f and b3f23e0.

📒 Files selected for processing (1)
  • frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue

Comment on lines 123 to +127
const pillWidth = computed(() => {
const totalGap = (maxVisiblePills - 1) * 8;
const numPills = maxVisiblePills.value;
const totalGap = (numPills - 1) * 8;
const usableWidth = Math.max(containerWidth.value - totalGap, 0) - 100;
return Math.floor(usableWidth / maxVisiblePills) || 200;
return Math.floor(usableWidth / numPills) || 200;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "EmergencyScenarioVisualization.vue" --type f

Repository: cryptomator/hub

Length of output: 133


🏁 Script executed:

fd "EmergencyAccessDialog.vue" --type f

Repository: cryptomator/hub

Length of output: 195


🏁 Script executed:

cat -n frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue | head -150

Repository: cryptomator/hub

Length of output: 6442


🏁 Script executed:

cat -n frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue | sed -n '150,250p'

Repository: cryptomator/hub

Length of output: 3670


🏁 Script executed:

cat -n frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue | sed -n '250,300p'

Repository: cryptomator/hub

Length of output: 1271


🏁 Script executed:

rg "EmergencyScenarioVisualization" -A 3 -B 3 --type vue

Repository: cryptomator/hub

Length of output: 86


🏁 Script executed:

rg "EmergencyScenarioVisualization" -A 3 -B 3

Repository: cryptomator/hub

Length of output: 7225


🏁 Script executed:

cat -n frontend/src/components/emergencyaccess/GrantEmergencyAccessDialog.vue | grep -A 20 "requiredKeyShares"

Repository: cryptomator/hub

Length of output: 6887


🏁 Script executed:

cat -n frontend/src/components/emergencyaccess/EmergencyAccessDialog.vue | sed -n '1,100p'

Repository: cryptomator/hub

Length of output: 5717


🏁 Script executed:

cat -n frontend/src/components/emergencyaccess/EmergencyAccessDialog.vue | sed -n '400,550p'

Repository: cryptomator/hub

Length of output: 6497


🏁 Script executed:

cat -n frontend/src/components/CreateVault.vue | grep -A 5 "EmergencyScenarioVisualization"

Repository: cryptomator/hub

Length of output: 669


🏁 Script executed:

cat -n frontend/src/components/AdminSettingsEmergencyAccess.vue | grep -B 10 -A 10 "EmergencyScenarioVisualization"

Repository: cryptomator/hub

Length of output: 1997


🏁 Script executed:

rg "requiredKeyShares" frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue -B 2 -A 2

Repository: cryptomator/hub

Length of output: 1503


🏁 Script executed:

rg "defaultRequiredEmergencyKeyShares\|defaultMinMembers" frontend/src/components/emergencyaccess/EmergencyAccessDialog.vue -B 2 -A 2

Repository: cryptomator/hub

Length of output: 41


🏁 Script executed:

rg "defaultRequiredEmergencyKeyShares" frontend/src/components/ -B 2 -A 2

Repository: cryptomator/hub

Length of output: 18560


🏁 Script executed:

node -e "console.log(-34 || 200); console.log(Math.floor(-100 / 3) || 200);"

Repository: cryptomator/hub

Length of output: 66


Divide by actual rendered pills, not the visibility cap.

Line 124 divides by maxVisiblePills instead of the number of pills that will actually be shown. When requiredKeyShares < maxVisiblePills, this causes avoidable text truncation. Additionally, line 127's fallback || 200 doesn't catch negative widths: when containerWidth === 0, the calculation produces a negative value, and negative numbers are truthy in JavaScript, so -34 || 200 evaluates to -34, not 200.

🐛 Proposed change
 const pillWidth = computed(() => {
-  const numPills = maxVisiblePills.value;
+  const numPills = Math.min(requiredKeyShares.value, maxVisiblePills.value);
+  if (numPills === 0) return 0;
   const totalGap = (numPills - 1) * 8;
   const usableWidth = Math.max(containerWidth.value - totalGap, 0) - 100;
-  return Math.floor(usableWidth / numPills) || 200;
+  return Math.max(Math.floor(usableWidth / numPills), 0);
 });
📝 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
const pillWidth = computed(() => {
const totalGap = (maxVisiblePills - 1) * 8;
const numPills = maxVisiblePills.value;
const totalGap = (numPills - 1) * 8;
const usableWidth = Math.max(containerWidth.value - totalGap, 0) - 100;
return Math.floor(usableWidth / maxVisiblePills) || 200;
return Math.floor(usableWidth / numPills) || 200;
const pillWidth = computed(() => {
const numPills = Math.min(requiredKeyShares.value, maxVisiblePills.value);
if (numPills === 0) return 0;
const totalGap = (numPills - 1) * 8;
const usableWidth = Math.max(containerWidth.value - totalGap, 0) - 100;
return Math.max(Math.floor(usableWidth / numPills), 0);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/emergencyaccess/EmergencyScenarioVisualization.vue`
around lines 123 - 127, The pillWidth computation divides by the visibility cap
and can produce negative values; change it to divide by the actual number of
rendered pills (use Math.min(maxVisiblePills.value, requiredKeyShares.value) or
the appropriate reactive that represents actual shown pills) instead of
maxVisiblePills.value, compute usableWidth as Math.max(containerWidth.value -
totalGap - 100, 0) to avoid negatives, then compute Math.floor(usableWidth /
numPills) and only apply the fallback (200) when the result is not a positive
finite number (e.g., if result <= 0 or !isFinite(result) then return 200).
Reference pillWidth, maxVisiblePills, requiredKeyShares, and containerWidth when
updating the code.

@overheadhunter
Copy link
Copy Markdown
Member

Please rebase on commit d40ea08, as I eliminated the calcLeft stuff

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