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,71 @@
|
||||
.dialog {
|
||||
max-inline-size: none;
|
||||
max-block-size: none;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
overflow: visible;
|
||||
}
|
||||
.dialog::backdrop {
|
||||
background: var(--overlay-scrim);
|
||||
}
|
||||
.panel {
|
||||
display: grid;
|
||||
max-block-size: calc(100dvh - var(--space-8));
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-standard);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface-elevated);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
.modal .panel {
|
||||
inline-size: min(44rem, calc(100vw - var(--space-8)));
|
||||
}
|
||||
.drawer {
|
||||
margin-block: 0;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
.drawer .panel {
|
||||
inline-size: min(24rem, 88vw);
|
||||
min-block-size: 100dvh;
|
||||
border-radius: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
border-block-end: 1px solid var(--border-standard);
|
||||
}
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-size: var(--type-heading-3-size);
|
||||
}
|
||||
.header p {
|
||||
margin-block: var(--space-2) 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.content {
|
||||
min-block-size: 0;
|
||||
padding: var(--space-6);
|
||||
overflow-y: auto;
|
||||
}
|
||||
@media (max-width: 479px) {
|
||||
.modal .panel {
|
||||
inline-size: calc(100vw - var(--space-4));
|
||||
max-block-size: calc(100dvh - var(--space-4));
|
||||
}
|
||||
.header,
|
||||
.content {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.panel {
|
||||
border: 2px solid CanvasText;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/actions/Button';
|
||||
import { useScrollLock } from '@/components/foundation/client-hooks';
|
||||
import { classNames } from '@/lib/components/classNames';
|
||||
import { useEffect, useId, useRef, type ReactNode, type RefObject } from 'react';
|
||||
import styles from './Dialog.module.css';
|
||||
|
||||
interface DialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
closeLabel: string;
|
||||
initialFocusRef?: RefObject<HTMLElement | null>;
|
||||
returnFocusRef?: RefObject<HTMLElement | null>;
|
||||
variant?: 'modal' | 'drawer';
|
||||
dismissible?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
closeLabel,
|
||||
initialFocusRef,
|
||||
returnFocusRef,
|
||||
variant = 'modal',
|
||||
dismissible = true,
|
||||
className,
|
||||
}: DialogProps) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const closeRef = useRef<HTMLButtonElement>(null);
|
||||
const titleId = useId();
|
||||
const descriptionId = useId();
|
||||
useScrollLock(open);
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
if (open && !dialog.open) {
|
||||
dialog.showModal();
|
||||
window.requestAnimationFrame(() => (initialFocusRef?.current ?? closeRef.current)?.focus());
|
||||
} else if (!open && dialog.open) {
|
||||
dialog.close();
|
||||
}
|
||||
}, [initialFocusRef, open]);
|
||||
|
||||
const close = () => {
|
||||
dialogRef.current?.close();
|
||||
onOpenChange(false);
|
||||
window.requestAnimationFrame(() => returnFocusRef?.current?.focus());
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className={classNames(styles.dialog, styles[variant], className)}
|
||||
aria-labelledby={titleId}
|
||||
aria-describedby={description ? descriptionId : undefined}
|
||||
onCancel={(event) => {
|
||||
if (!dismissible) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
close();
|
||||
}}
|
||||
onClose={() => onOpenChange(false)}
|
||||
onClick={(event) => {
|
||||
if (dismissible && event.target === dialogRef.current) close();
|
||||
}}
|
||||
>
|
||||
<div className={styles.panel}>
|
||||
<header className={styles.header}>
|
||||
<div>
|
||||
<h2 id={titleId}>{title}</h2>
|
||||
{description ? <p id={descriptionId}>{description}</p> : null}
|
||||
</div>
|
||||
{dismissible ? (
|
||||
<Button
|
||||
ref={closeRef}
|
||||
intent="ghost"
|
||||
size="small"
|
||||
icon="close"
|
||||
aria-label={closeLabel}
|
||||
onClick={close}
|
||||
/>
|
||||
) : null}
|
||||
</header>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.menu {
|
||||
position: absolute;
|
||||
z-index: var(--layer-overlay);
|
||||
inset-block-start: calc(100% + var(--space-2));
|
||||
inset-inline-end: 0;
|
||||
display: grid;
|
||||
min-inline-size: 13rem;
|
||||
padding: var(--space-2);
|
||||
border: 1px solid var(--border-standard);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-elevated);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
.item {
|
||||
min-block-size: 2.75rem;
|
||||
padding-block: var(--space-2);
|
||||
padding-inline: var(--space-3);
|
||||
border: 0;
|
||||
border-radius: calc(var(--radius-sm) - 2px);
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
}
|
||||
.item:hover:not(:disabled),
|
||||
.item:focus-visible {
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
.item:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: var(--opacity-disabled);
|
||||
}
|
||||
.destructive {
|
||||
color: var(--status-error-text);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/actions/Button';
|
||||
import { useEscapeKey, useOutsideClick } from '@/components/foundation/client-hooks';
|
||||
import { classNames } from '@/lib/components/classNames';
|
||||
import { useRef, useState, type KeyboardEvent } from 'react';
|
||||
import styles from './DropdownMenu.module.css';
|
||||
|
||||
export interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
onSelect: () => void;
|
||||
disabled?: boolean;
|
||||
destructive?: boolean;
|
||||
}
|
||||
|
||||
interface DropdownMenuProps {
|
||||
label: string;
|
||||
items: MenuItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DropdownMenu({ label, items, className }: DropdownMenuProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
useOutsideClick(rootRef, () => setOpen(false), open);
|
||||
useEscapeKey(() => setOpen(false), open);
|
||||
|
||||
const focusItem = (index: number) => {
|
||||
const enabled = items
|
||||
.map((item, itemIndex) => ({ item, itemIndex }))
|
||||
.filter(({ item }) => !item.disabled);
|
||||
const target = enabled[(index + enabled.length) % enabled.length];
|
||||
if (target) itemRefs.current[target.itemIndex]?.focus();
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
const currentIndex = itemRefs.current.findIndex((node) => node === document.activeElement);
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
focusItem(currentIndex + 1);
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
focusItem(currentIndex - 1);
|
||||
} else if (event.key === 'Home') {
|
||||
event.preventDefault();
|
||||
focusItem(0);
|
||||
} else if (event.key === 'End') {
|
||||
event.preventDefault();
|
||||
focusItem(items.length - 1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className={classNames(styles.root, className)} onKeyDown={handleKeyDown}>
|
||||
<Button
|
||||
intent="tertiary"
|
||||
trailingIcon="chevron-down"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
{open ? (
|
||||
<div className={styles.menu} role="menu">
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={item.id}
|
||||
ref={(node) => {
|
||||
itemRefs.current[index] = node;
|
||||
}}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
disabled={item.disabled}
|
||||
className={classNames(styles.item, item.destructive && styles.destructive)}
|
||||
onClick={() => {
|
||||
item.onSelect();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: var(--layer-overlay);
|
||||
inset-block-end: calc(100% + var(--space-2));
|
||||
inset-inline-start: 50%;
|
||||
inline-size: max-content;
|
||||
max-inline-size: 16rem;
|
||||
padding-block: var(--space-2);
|
||||
padding-inline: var(--space-3);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-strong);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--type-caption-size);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%) translateY(var(--space-1));
|
||||
transition:
|
||||
opacity var(--duration-fast) var(--easing-productive),
|
||||
transform var(--duration-fast) var(--easing-productive);
|
||||
}
|
||||
.root:hover .tooltip,
|
||||
.root:focus-within .tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
html[dir='rtl'] .tooltip {
|
||||
transform: translateX(50%) translateY(var(--space-1));
|
||||
}
|
||||
html[dir='rtl'] .root:hover .tooltip,
|
||||
html[dir='rtl'] .root:focus-within .tooltip {
|
||||
transform: translateX(50%) translateY(0);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tooltip {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { classNames } from '@/lib/components/classNames';
|
||||
import type { ReactNode } from 'react';
|
||||
import styles from './Tooltip.module.css';
|
||||
|
||||
export function Tooltip({
|
||||
content,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
content: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<span className={classNames(styles.root, className)}>
|
||||
{children}
|
||||
<span role="tooltip" className={styles.tooltip}>
|
||||
{content}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user