shadcn primitives and logos keyed: dialog, sheet, pagination, carousel, breadcrumb, theme toggles

These are the strings a user meets everywhere, mostly sr-only: the dialog and
sheet close buttons, pagination labels, carousel arrows, the breadcrumb ellipsis
and both colour-mode toggles. nav aria-label='pagination' became a real key too —
a screen reader announces it, so it is user-facing.

Logo alt text keeps the product name and translates only around it
('Logótipo Officer'). Arrow-button insets became logical (-start-12/-end-12).

The three 'useX must be used within <Y>' Errors are deliberately untouched:
never-translate, per the doc's list.
This commit is contained in:
2026-08-23 04:10:39 +00:00
parent 5ce1a2928e
commit 2ec3f11cdd
9 changed files with 126 additions and 95 deletions
@@ -1,17 +1,14 @@
import { useTranslation } from '@/components/locales';
type SvgLogoProps = { type SvgLogoProps = {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
}; };
export function OfficerIconLogo({ size = 'xl' }: SvgLogoProps) { export function OfficerIconLogo({ size = 'xl' }: SvgLogoProps) {
const { t } = useTranslation();
const config = sizeConfig[size]; const config = sizeConfig[size];
return ( return <img src="/officer-logo.svg" alt={t('logo.icon')} style={{ width: config.width, height: 'auto' }} />;
<img
src="/officer-logo.svg"
alt="Officer Icon"
style={{ width: config.width, height: 'auto' }}
/>
);
} }
const sizeConfig = { const sizeConfig = {
@@ -1,3 +1,5 @@
import { useTranslation } from '@/components/locales';
type SvgLogoProps = { type SvgLogoProps = {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
square?: boolean; square?: boolean;
@@ -12,27 +14,17 @@ export function OfficerSvgLogo({ size = 'xl', square = false }: SvgLogoProps) {
} }
export function OfficerSvgLogoRegular({ size = 'xl' }: SvgLogoProps) { export function OfficerSvgLogoRegular({ size = 'xl' }: SvgLogoProps) {
const { t } = useTranslation();
const config = sizeConfig.regular[size]; const config = sizeConfig.regular[size];
return ( return <img src="/officer-logo.svg" alt={t('logo.logo')} style={{ width: config.width, height: 'auto' }} />;
<img
src="/officer-logo.svg"
alt="Officer Logo"
style={{ width: config.width, height: 'auto' }}
/>
);
} }
export function OfficerSvgLogoSquare({ size = 'xl' }: SvgLogoProps) { export function OfficerSvgLogoSquare({ size = 'xl' }: SvgLogoProps) {
const { t } = useTranslation();
const config = sizeConfig.square[size]; const config = sizeConfig.square[size];
return ( return <img src="/officer-logo.svg" alt={t('logo.logo')} style={{ width: config.width, height: 'auto' }} />;
<img
src="/officer-logo.svg"
alt="Officer Logo"
style={{ width: config.width, height: 'auto' }}
/>
);
} }
const sizeConfig = { const sizeConfig = {
+3 -1
View File
@@ -1,12 +1,14 @@
import { Moon, Sun } from 'lucide-react'; import { Moon, Sun } from 'lucide-react';
import { cn } from 'helpers/cn'; import { cn } from 'helpers/cn';
import { useColorMode } from './ThemeProvider'; import { useColorMode } from './ThemeProvider';
import { useTranslation } from '@/components/locales';
type ThemeToggleProps = { colorDark?: boolean; colorLight?: boolean }; type ThemeToggleProps = { colorDark?: boolean; colorLight?: boolean };
export const ThemeToggle = (props: ThemeToggleProps) => { export const ThemeToggle = (props: ThemeToggleProps) => {
const { colorDark, colorLight } = props; const { colorDark, colorLight } = props;
const { colorMode, setColorMode } = useColorMode(); const { colorMode, setColorMode } = useColorMode();
const { t } = useTranslation();
const toggle = () => { const toggle = () => {
setColorMode(colorMode === 'dark' ? 'light' : 'dark'); setColorMode(colorMode === 'dark' ? 'light' : 'dark');
@@ -26,7 +28,7 @@ export const ThemeToggle = (props: ThemeToggleProps) => {
colorDark && 'text-black', colorDark && 'text-black',
)} )}
/> />
<span className="sr-only">Toggle color mode</span> <span className="sr-only">{t('theme.toggle')}</span>
</div> </div>
); );
}; };
+15 -11
View File
@@ -3,6 +3,7 @@ import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react'; import { ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslation } from '@/components/locales';
const Breadcrumb = React.forwardRef< const Breadcrumb = React.forwardRef<
HTMLElement, HTMLElement,
@@ -66,17 +67,20 @@ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentP
); );
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator'; BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => ( const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => {
<span const { t } = useTranslation();
role="presentation" return (
aria-hidden="true" <span
className={cn('flex h-9 w-9 items-center justify-center', className)} role="presentation"
{...props} aria-hidden="true"
> className={cn('flex h-9 w-9 items-center justify-center', className)}
<MoreHorizontal className="h-4 w-4" /> {...props}
<span className="sr-only">More</span> >
</span> <MoreHorizontal className="h-4 w-4" />
); <span className="sr-only">{t('breadcrumb.more')}</span>
</span>
);
};
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis'; BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
export { export {
+7 -4
View File
@@ -4,6 +4,7 @@ import { ArrowLeft, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useTranslation } from '@/components/locales';
type CarouselApi = UseEmblaCarouselType[1]; type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>; type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
@@ -168,6 +169,7 @@ CarouselItem.displayName = 'CarouselItem';
const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>( const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = 'outline', size = 'icon', ...props }, ref) => { ({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel(); const { orientation, scrollPrev, canScrollPrev } = useCarousel();
const { t } = useTranslation();
return ( return (
<Button <Button
@@ -177,7 +179,7 @@ const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProp
className={cn( className={cn(
'absolute h-8 w-8 rounded-full', 'absolute h-8 w-8 rounded-full',
orientation === 'horizontal' orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2' ? '-start-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90', : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
className, className,
)} )}
@@ -186,7 +188,7 @@ const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProp
{...props} {...props}
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span> <span className="sr-only">{t('carousel.previous')}</span>
</Button> </Button>
); );
}, },
@@ -196,6 +198,7 @@ CarouselPrevious.displayName = 'CarouselPrevious';
const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>( const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = 'outline', size = 'icon', ...props }, ref) => { ({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel(); const { orientation, scrollNext, canScrollNext } = useCarousel();
const { t } = useTranslation();
return ( return (
<Button <Button
@@ -205,7 +208,7 @@ const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<ty
className={cn( className={cn(
'absolute h-8 w-8 rounded-full', 'absolute h-8 w-8 rounded-full',
orientation === 'horizontal' orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2' ? '-end-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90', : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className, className,
)} )}
@@ -214,7 +217,7 @@ const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<ty
{...props} {...props}
> >
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span> <span className="sr-only">{t('carousel.next')}</span>
</Button> </Button>
); );
}, },
+23 -19
View File
@@ -3,6 +3,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslation } from '@/components/locales';
const Dialog = DialogPrimitive.Root; const Dialog = DialogPrimitive.Root;
@@ -30,25 +31,28 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => {
<DialogPortal> const { t } = useTranslation();
<DialogOverlay /> return (
<DialogPrimitive.Content <DialogPortal>
ref={ref} <DialogOverlay />
className={cn( <DialogPrimitive.Content
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', ref={ref}
className, className={cn(
)} 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
{...props} className,
> )}
{children} {...props}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> >
<X className="h-4 w-4" /> {children}
<span className="sr-only">Close</span> <DialogPrimitive.Close className="absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
</DialogPrimitive.Close> <X className="h-4 w-4" />
</DialogPrimitive.Content> <span className="sr-only">{t('button.close')}</span>
</DialogPortal> </DialogPrimitive.Close>
)); </DialogPrimitive.Content>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName; DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
+3 -1
View File
@@ -3,10 +3,12 @@ import { Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useColorMode } from '@/components/ui/ThemeProvider'; import { useColorMode } from '@/components/ui/ThemeProvider';
import { cn } from 'helpers/cn'; import { cn } from 'helpers/cn';
import { useTranslation } from '@/components/locales';
export const ModeToggle = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>( export const ModeToggle = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {
const { colorMode, setColorMode } = useColorMode(); const { colorMode, setColorMode } = useColorMode();
const { t } = useTranslation();
return ( return (
<Button <Button
@@ -19,7 +21,7 @@ export const ModeToggle = React.forwardRef<React.ElementRef<typeof Button>, Reac
> >
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle color mode</span> <span className="sr-only">{t('theme.toggle')}</span>
</Button> </Button>
); );
}, },
+49 -26
View File
@@ -4,15 +4,19 @@ import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { ButtonProps } from '@/components/ui/button'; import type { ButtonProps } from '@/components/ui/button';
import { buttonVariants } from '@/components/ui/button'; import { buttonVariants } from '@/components/ui/button';
import { useTranslation } from '@/components/locales';
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => {
<nav const { t } = useTranslation();
role="navigation" return (
aria-label="pagination" <nav
className={cn('mx-auto flex w-full justify-center', className)} role="navigation"
{...props} aria-label={t('pagination.label')}
/> className={cn('mx-auto flex w-full justify-center', className)}
); {...props}
/>
);
};
Pagination.displayName = 'Pagination'; Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>( const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
@@ -47,28 +51,47 @@ const PaginationLink = ({ className, isActive, size = 'icon', ...props }: Pagina
); );
PaginationLink.displayName = 'PaginationLink'; PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => ( const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => {
<PaginationLink aria-label="Go to previous page" size="default" className={cn('gap-1 pl-2.5', className)} {...props}> const { t } = useTranslation();
<ChevronLeft className="h-4 w-4" /> return (
<span>Previous</span> <PaginationLink
</PaginationLink> aria-label={t('pagination.goToPrevious')}
); size="default"
className={cn('gap-1 ps-2.5', className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>{t('pagination.previous')}</span>
</PaginationLink>
);
};
PaginationPrevious.displayName = 'PaginationPrevious'; PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => ( const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => {
<PaginationLink aria-label="Go to next page" size="default" className={cn('gap-1 pr-2.5', className)} {...props}> const { t } = useTranslation();
<span>Next</span> return (
<ChevronRight className="h-4 w-4" /> <PaginationLink
</PaginationLink> aria-label={t('pagination.goToNext')}
); size="default"
className={cn('gap-1 pe-2.5', className)}
{...props}
>
<span>{t('pagination.next')}</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
};
PaginationNext.displayName = 'PaginationNext'; PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => ( const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => {
<span aria-hidden className={cn('flex h-9 w-9 items-center justify-center', className)} {...props}> const { t } = useTranslation();
<MoreHorizontal className="h-4 w-4" /> return (
<span className="sr-only">More pages</span> <span aria-hidden className={cn('flex h-9 w-9 items-center justify-center', className)} {...props}>
</span> <MoreHorizontal className="h-4 w-4" />
); <span className="sr-only">{t('pagination.morePages')}</span>
</span>
);
};
PaginationEllipsis.displayName = 'PaginationEllipsis'; PaginationEllipsis.displayName = 'PaginationEllipsis';
export { export {
+16 -12
View File
@@ -4,6 +4,7 @@ import { X } from 'lucide-react';
import * as React from 'react'; import * as React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslation } from '@/components/locales';
const Sheet = SheetPrimitive.Root; const Sheet = SheetPrimitive.Root;
@@ -51,18 +52,21 @@ interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, VariantProps<typeof sheetVariants> {} extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>( const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = 'right', className, children, ...props }, ref) => ( ({ side = 'right', className, children, ...props }, ref) => {
<SheetPortal> const { t } = useTranslation();
<SheetOverlay /> return (
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}> <SheetPortal>
{children} <SheetOverlay />
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
<X className="h-4 w-4" /> {children}
<span className="sr-only">Close</span> <SheetPrimitive.Close className="absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
</SheetPrimitive.Close> <X className="h-4 w-4" />
</SheetPrimitive.Content> <span className="sr-only">{t('button.close')}</span>
</SheetPortal> </SheetPrimitive.Close>
), </SheetPrimitive.Content>
</SheetPortal>
);
},
); );
SheetContent.displayName = SheetPrimitive.Content.displayName; SheetContent.displayName = SheetPrimitive.Content.displayName;