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
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user