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,34 @@
.alert {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid currentColor;
border-radius: var(--radius-sm);
}
.inline {
padding: var(--space-3);
}
.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);
}
.title {
display: block;
margin-block-end: var(--space-1);
}
.content > :last-child {
margin-block-end: 0;
}
@@ -0,0 +1,45 @@
import { Icon, type ApprovedIconName } from '@/components/foundation/Icon';
import { classNames } from '@/lib/components/classNames';
import type { ReactNode } from 'react';
import styles from './Alert.module.css';
export type AlertTone = 'info' | 'success' | 'warning' | 'error';
const toneIcon: Record<AlertTone, ApprovedIconName> = {
info: 'info',
success: 'check',
warning: 'warning',
error: 'error',
};
interface AlertProps {
tone?: AlertTone;
title?: string;
children: ReactNode;
live?: 'off' | 'polite' | 'assertive';
inline?: boolean;
className?: string;
}
export function Alert({
tone = 'info',
title,
children,
live = 'off',
inline = false,
className,
}: AlertProps) {
return (
<div
className={classNames(styles.alert, styles[tone], inline && styles.inline, className)}
role={live === 'assertive' ? 'alert' : live === 'polite' ? 'status' : undefined}
aria-live={live === 'off' ? undefined : live}
>
<Icon name={toneIcon[tone]} />
<div>
{title ? <strong className={styles.title}>{title}</strong> : null}
<div className={styles.content}>{children}</div>
</div>
</div>
);
}
@@ -0,0 +1,43 @@
.spinnerGroup {
display: inline-flex;
align-items: center;
justify-content: center;
}
.spinner {
animation: spin 0.9s linear infinite;
}
.skeleton {
display: grid;
gap: var(--space-3);
}
.skeleton span {
block-size: 0.9rem;
border-radius: var(--radius-pill);
background: linear-gradient(
90deg,
var(--skeleton-base),
var(--skeleton-highlight),
var(--skeleton-base)
);
background-size: 200% 100%;
animation: shimmer 1.5s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes shimmer {
to {
background-position: -200% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.spinner {
animation-duration: 1.8s;
}
.skeleton span {
animation: none;
background: var(--skeleton-base);
}
}
@@ -0,0 +1,38 @@
import { Icon } from '@/components/foundation/Icon';
import { classNames } from '@/lib/components/classNames';
import type { HTMLAttributes } from 'react';
import styles from './Loading.module.css';
export function Spinner({ label, className }: { label: string; className?: string }) {
return (
<span className={classNames(styles.spinnerGroup, className)} role="status">
<Icon name="spinner" className={styles.spinner} />
<span className="visually-hidden">{label}</span>
</span>
);
}
interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
lines?: number;
label?: string;
}
export function Skeleton({
lines = 3,
label = 'Loading content',
className,
...props
}: SkeletonProps) {
return (
<div
className={classNames(styles.skeleton, className)}
aria-label={label}
aria-busy="true"
{...props}
>
{Array.from({ length: lines }, (_, index) => (
<span key={index} style={{ inlineSize: index === lines - 1 ? '68%' : '100%' }} />
))}
</div>
);
}
@@ -0,0 +1,16 @@
.group {
display: grid;
gap: var(--space-2);
}
.labelRow {
display: flex;
justify-content: space-between;
gap: var(--space-4);
font-size: var(--type-label-size);
font-weight: 700;
}
.progress {
inline-size: 100%;
block-size: 0.75rem;
accent-color: var(--interactive-primary);
}
@@ -0,0 +1,31 @@
import { BidiText } from '@/components/foundation/BidiText';
import { classNames } from '@/lib/components/classNames';
import styles from './Progress.module.css';
interface ProgressProps {
value?: number;
max?: number;
label: string;
showValue?: boolean;
className?: string;
}
export function Progress({ value, max = 100, label, showValue = false, className }: ProgressProps) {
const normalized = value === undefined ? undefined : Math.min(max, Math.max(0, value));
return (
<div className={classNames(styles.group, className)}>
<div className={styles.labelRow}>
<span>{label}</span>
{showValue && normalized !== undefined ? (
<BidiText>{`${Math.round((normalized / max) * 100)}%`}</BidiText>
) : null}
</div>
<progress
className={styles.progress}
max={max}
aria-label={label}
{...(normalized === undefined ? {} : { value: normalized })}
/>
</div>
);
}
@@ -0,0 +1,17 @@
.state {
display: grid;
justify-items: start;
max-inline-size: var(--container-reading);
gap: var(--space-3);
padding: var(--space-8);
border: 1px dashed var(--border-strong);
border-radius: var(--radius-md);
background: var(--surface-muted);
}
.state h2,
.state p {
margin: 0;
}
.state p {
color: var(--text-secondary);
}
@@ -0,0 +1,25 @@
import { ActionLink } from '@/components/actions/ActionLink';
import { Icon, type ApprovedIconName } from '@/components/foundation/Icon';
import styles from './StateMessage.module.css';
interface StateMessageProps {
icon?: ApprovedIconName;
title: string;
body: string;
action?: { label: string; href: string };
}
export function StateMessage({ icon = 'info', title, body, action }: StateMessageProps) {
return (
<section className={styles.state}>
<Icon name={icon} size="lg" />
<h2>{title}</h2>
<p>{body}</p>
{action ? (
<ActionLink href={action.href} variant="standalone" icon="arrow-forward">
{action.label}
</ActionLink>
) : null}
</section>
);
}