redesign the homepage
Build & Deploy / Build & Push Docker Image (push) Failing after 47s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Failing after 4m55s
Test / Admin Unit Tests (push) Successful in 9m37s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s

This commit is contained in:
root
2026-06-26 16:27:21 -04:00
parent 256ff0814e
commit d7fb7b7a7b
1030 changed files with 107374 additions and 2657 deletions
@@ -0,0 +1,17 @@
import type { ReactElement } from 'react';
interface AccessibleIconProps {
icon: ReactElement;
label?: string;
}
export function AccessibleIcon({ icon, label }: AccessibleIconProps) {
if (label) {
return (
<span role="img" aria-label={label}>
{icon}
</span>
);
}
return <span aria-hidden="true">{icon}</span>;
}
@@ -0,0 +1,3 @@
.noWrap {
white-space: nowrap;
}
@@ -0,0 +1,23 @@
import { classNames } from '@/lib/components/classNames';
import type { HTMLAttributes, ReactNode } from 'react';
import styles from './BidiText.module.css';
interface BidiTextProps extends Omit<HTMLAttributes<HTMLElement>, 'dir'> {
children: ReactNode;
direction?: 'ltr' | 'rtl' | 'auto';
noWrap?: boolean;
}
export function BidiText({
children,
direction = 'ltr',
noWrap = true,
className,
...props
}: BidiTextProps) {
return (
<bdi dir={direction} className={classNames(noWrap && styles.noWrap, className)} {...props}>
{children}
</bdi>
);
}
@@ -0,0 +1,25 @@
.icon {
flex: none;
inline-size: 1.25rem;
block-size: 1.25rem;
}
.sm {
inline-size: 1rem;
block-size: 1rem;
}
.md {
inline-size: 1.25rem;
block-size: 1.25rem;
}
.lg {
inline-size: 1.5rem;
block-size: 1.5rem;
}
html[dir='rtl'] .mirror {
transform: scaleX(-1);
}
@media (prefers-reduced-motion: reduce) {
.icon {
animation: none !important;
}
}
+139
View File
@@ -0,0 +1,139 @@
import { classNames } from '@/lib/components/classNames';
import type { ReactNode, SVGProps } from 'react';
import styles from './Icon.module.css';
export const approvedIconNames = [
'arrow-back',
'arrow-forward',
'calendar',
'car',
'check',
'chevron-down',
'chevron-forward',
'close',
'error',
'external',
'globe',
'info',
'menu',
'minus',
'plus',
'search',
'settings',
'spinner',
'warning',
] as const;
export type ApprovedIconName = (typeof approvedIconNames)[number];
export type IconSize = 'sm' | 'md' | 'lg';
export type IconDirectionBehavior = 'fixed' | 'mirror';
interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'name'> {
name: ApprovedIconName;
size?: IconSize;
decorative?: boolean;
label?: string;
directionBehavior?: IconDirectionBehavior;
}
const paths: Record<ApprovedIconName, ReactNode> = {
'arrow-back': <path d="m15 18-6-6 6-6M9 12h10" />,
'arrow-forward': <path d="m9 18 6-6-6-6m6 6H5" />,
calendar: (
<>
<rect x="3" y="5" width="18" height="16" rx="2" />
<path d="M16 3v4M8 3v4M3 10h18" />
</>
),
car: (
<>
<path d="m5 11 2-5h10l2 5" />
<path d="M3 11h18v7H3zM6 18v2m12-2v2M7 14h.01M17 14h.01" />
</>
),
check: <path d="m5 12 4 4L19 6" />,
'chevron-down': <path d="m6 9 6 6 6-6" />,
'chevron-forward': <path d="m9 18 6-6-6-6" />,
close: <path d="M6 6l12 12M18 6 6 18" />,
error: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 7v6m0 4h.01" />
</>
),
external: (
<>
<path d="M14 4h6v6M20 4l-9 9" />
<path d="M18 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6" />
</>
),
globe: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" />
</>
),
info: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 11v6m0-10h.01" />
</>
),
menu: <path d="M4 7h16M4 12h16M4 17h16" />,
minus: <path d="M5 12h14" />,
plus: <path d="M12 5v14M5 12h14" />,
search: (
<>
<circle cx="11" cy="11" r="7" />
<path d="m16 16 5 5" />
</>
),
settings: (
<>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1A1.7 1.7 0 0 0 9 4.6 1.7 1.7 0 0 0 10 3V2.8h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z" />
</>
),
spinner: <path d="M21 12a9 9 0 1 1-9-9" />,
warning: (
<>
<path d="M12 3 2.5 20h19L12 3Z" />
<path d="M12 9v5m0 3h.01" />
</>
),
};
export function Icon({
name,
size = 'md',
decorative = true,
label,
directionBehavior = 'fixed',
className,
...props
}: IconProps) {
const accessibleProps = decorative
? ({ 'aria-hidden': true } as const)
: ({ role: 'img', 'aria-label': label ?? name } as const);
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
focusable="false"
className={classNames(
styles.icon,
styles[size],
directionBehavior === 'mirror' && styles.mirror,
className,
)}
{...accessibleProps}
{...props}
>
{paths[name]}
</svg>
);
}
@@ -0,0 +1,11 @@
.region {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
border: 0;
clip-path: inset(50%);
white-space: nowrap;
}
@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
import styles from './LiveRegion.module.css';
interface LiveRegionProps {
children: ReactNode;
politeness?: 'polite' | 'assertive';
}
export function LiveRegion({ children, politeness = 'polite' }: LiveRegionProps) {
return (
<span className={styles.region} role="status" aria-live={politeness} aria-atomic="true">
{children}
</span>
);
}
@@ -0,0 +1,34 @@
.badge {
display: inline-flex;
min-block-size: 1.75rem;
align-items: center;
gap: var(--space-1);
inline-size: fit-content;
padding-block: var(--space-1);
padding-inline: var(--space-3);
border: 1px solid currentColor;
border-radius: var(--radius-pill);
font-size: var(--type-caption-size);
font-weight: 800;
line-height: 1.2;
}
.neutral {
background: var(--surface-muted);
color: var(--text-secondary);
}
.info {
background: var(--status-info-surface);
color: var(--status-info-text);
}
.success {
background: var(--status-success-surface);
color: var(--status-success-text);
}
.warning {
background: var(--status-warning-surface);
color: var(--status-warning-text);
}
.error {
background: var(--status-error-surface);
color: var(--status-error-text);
}
@@ -0,0 +1,29 @@
import { Icon, type ApprovedIconName } from './Icon';
import styles from './StatusBadge.module.css';
export type StatusTone = 'neutral' | 'info' | 'success' | 'warning' | 'error';
const icons: Partial<Record<StatusTone, ApprovedIconName>> = {
info: 'info',
success: 'check',
warning: 'warning',
error: 'error',
};
export function StatusBadge({
children,
tone = 'neutral',
showIcon = false,
}: {
children: string;
tone?: StatusTone;
showIcon?: boolean;
}) {
const icon = icons[tone];
return (
<span className={`${styles.badge} ${styles[tone]}`}>
{showIcon && icon ? <Icon name={icon} size="sm" /> : null}
{children}
</span>
);
}
@@ -0,0 +1,14 @@
import type { HTMLAttributes, ReactNode } from 'react';
interface VisuallyHiddenProps extends HTMLAttributes<HTMLSpanElement> {
children: ReactNode;
}
export function VisuallyHidden({ children, className, ...props }: VisuallyHiddenProps) {
const classes = ['visually-hidden', className].filter(Boolean).join(' ');
return (
<span className={classes} {...props}>
{children}
</span>
);
}
@@ -0,0 +1,74 @@
'use client';
import { useEffect, useId, useState, useSyncExternalStore, type RefObject } from 'react';
export function useAccessibleId(prefix = 'rdg'): string {
return `${prefix}-${useId().replaceAll(':', '')}`;
}
const subscribeHydration = () => () => undefined;
export function useHydrated(): boolean {
return useSyncExternalStore(
subscribeHydration,
() => true,
() => false,
);
}
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
const update = () => setMatches(media.matches);
update();
media.addEventListener('change', update);
return () => media.removeEventListener('change', update);
}, [query]);
return matches;
}
export function useReducedMotion(): boolean {
return useMediaQuery('(prefers-reduced-motion: reduce)');
}
export function useEscapeKey(onEscape: () => void, enabled = true): void {
useEffect(() => {
if (!enabled) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onEscape();
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [enabled, onEscape]);
}
export function useOutsideClick<T extends HTMLElement>(
ref: RefObject<T | null>,
onOutside: () => void,
enabled = true,
): void {
useEffect(() => {
if (!enabled) return;
const handlePointerDown = (event: PointerEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) onOutside();
};
document.addEventListener('pointerdown', handlePointerDown);
return () => document.removeEventListener('pointerdown', handlePointerDown);
}, [enabled, onOutside, ref]);
}
export function useScrollLock(locked: boolean): void {
useEffect(() => {
if (!locked) return;
const previousOverflow = document.body.style.overflow;
const previousPadding = document.body.style.paddingInlineEnd;
const scrollbar = window.innerWidth - document.documentElement.clientWidth;
document.body.style.overflow = 'hidden';
if (scrollbar > 0) document.body.style.paddingInlineEnd = `${scrollbar}px`;
return () => {
document.body.style.overflow = previousOverflow;
document.body.style.paddingInlineEnd = previousPadding;
};
}, [locked]);
}