feat(ui): one CountBadge for all counters; counters on tab corners
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m30s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m30s
- CountBadge component (accent / muted, inline / corner, 99+, showZero); used in the nav bar, task list and list tabs, admin organizations and numeric badges of the mobile menu; old badge-count removed - Counters sit on the top right corner of tabs; "Alle Aufgaben" gets one (desktop tab and mobile menu) - Open/done task counters include tasks in shared lists, matching what "Alle Aufgaben" shows (nav counter was too low) - Theme template derives --accent-dim/--accent-border from the accent - Docs: ui-patterns counters section Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a354f3d008
commit
a62b204e14
12 files changed
+167
-55
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CountBadgeProps {
|
||||
count: number | undefined | null;
|
||||
// accent: something waits for you (nav bar); muted: plain amount (tabs, lists)
|
||||
variant?: 'accent' | 'muted';
|
||||
// corner: notification badge on the top right of the nearest `relative` parent
|
||||
position?: 'inline' | 'corner';
|
||||
showZero?: boolean;
|
||||
className?: string; // e.g. to fine-tune the corner offset
|
||||
}
|
||||
|
||||
// Full class names: Tailwind drops component classes it cannot find literally in the source
|
||||
const VARIANT_CLASS = { accent: 'count-badge-accent', muted: 'count-badge-muted' };
|
||||
|
||||
// The one counter of the app (Doku/ui-patterns.md); hides itself at 0 unless showZero
|
||||
export const CountBadge: React.FC<CountBadgeProps> = ({
|
||||
count,
|
||||
variant = 'muted',
|
||||
position = 'inline',
|
||||
showZero = false,
|
||||
className = '',
|
||||
}) => {
|
||||
if (count === undefined || count === null || (count <= 0 && !showZero)) return null;
|
||||
const label = count > 99 ? '99+' : String(count);
|
||||
return (
|
||||
<span
|
||||
className={`count-badge ${VARIANT_CLASS[variant]} ${position === 'corner' ? 'count-badge-corner' : ''} ${className}`}
|
||||
aria-label={String(count)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { useMobileActions } from '../context/MobileActionContext';
|
||||
import { triggerHaptic } from '../lib/api';
|
||||
import { CountBadge } from './CountBadge';
|
||||
|
||||
interface MobileHeaderMenuProps {
|
||||
activeTabLabel: string;
|
||||
@@ -109,7 +110,9 @@ export const MobileHeaderMenu: React.FC<MobileHeaderMenuProps> = ({ activeTabLab
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{item.badge !== undefined && item.badge !== '' && (
|
||||
{typeof item.badge === 'number' && !isAccent ? (
|
||||
<CountBadge count={item.badge} showZero variant={isActive ? 'accent' : 'muted'} />
|
||||
) : item.badge !== undefined && item.badge !== '' && (
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-mono font-semibold ${
|
||||
isAccent
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
import { CountBadge } from '../CountBadge';
|
||||
|
||||
describe('CountBadge', () => {
|
||||
it('hides itself at zero unless asked', () => {
|
||||
expect(render(<CountBadge count={0} />).container.firstChild).toBeNull();
|
||||
expect(render(<CountBadge count={undefined} />).container.firstChild).toBeNull();
|
||||
expect(render(<CountBadge count={0} showZero />).container.textContent).toBe('0');
|
||||
});
|
||||
|
||||
it('caps large numbers and keeps the real value for screen readers', () => {
|
||||
const el = render(<CountBadge count={150} />).container.firstChild as HTMLElement;
|
||||
expect(el.textContent).toBe('99+');
|
||||
expect(el.getAttribute('aria-label')).toBe('150');
|
||||
});
|
||||
|
||||
it('maps variant and position to the shared classes', () => {
|
||||
const el = render(<CountBadge count={3} variant="accent" position="corner" />).container.firstChild as HTMLElement;
|
||||
expect(el.className).toContain('count-badge-accent');
|
||||
expect(el.className).toContain('count-badge-corner');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user