import clsx from 'clsx';
import {
Ban,
ChevronLeft,
ChevronRight,
CircleDot,
GitMerge,
GitPullRequest,
GitPullRequestClosed,
LoaderCircle,
UserRound,
} from 'lucide-solid';
import { A } from '@solidjs/router';
import { Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js';
import { createQuery } from '@tanstack/solid-query';
import { resolveAvatarUrl } from '../lib/api/identity';
export const ToggleButton: Component<{
active: boolean;
label: string;
meta?: string;
onClick: () => void;
}> = (props) => (
);
export const PaginationControls: Component<{
basePath?: string;
offset: number;
limit: number;
totalCount?: number;
hasNext?: boolean;
query?: Record;
hrefForOffset?: (offset: number) => string;
}> = (props) => {
const safeLimit = createMemo(() => Math.max(1, props.limit));
const hasExactTotal = createMemo(() => typeof props.totalCount === 'number');
const lastOffset = createMemo(() => Math.floor(Math.max((props.totalCount ?? 0) - 1, 0) / safeLimit()) * safeLimit());
const previousOffset = createMemo(() => Math.max(0, props.offset - safeLimit()));
const nextOffset = createMemo(() => props.offset + safeLimit());
const hasNextPage = createMemo(() =>
hasExactTotal() ? nextOffset() < (props.totalCount ?? 0) : Boolean(props.hasNext),
);
const pageNumber = (offset: number) => Math.floor(offset / safeLimit()) + 1;
const hrefForOffset = (offset: number) => {
if (props.hrefForOffset) {
return props.hrefForOffset(Math.max(0, offset));
}
const params = new URLSearchParams();
for (const [key, value] of Object.entries(props.query ?? {})) {
if (value) {
params.set(key, value);
}
}
params.set('offset', String(Math.max(0, offset)));
params.set('limit', String(safeLimit()));
return `${props.basePath ?? ''}?${params.toString()}`;
};
const showNextNumber = createMemo(() => hasExactTotal() ? nextOffset() < lastOffset() : hasNextPage());
const showGap = createMemo(() => hasExactTotal() && nextOffset() < (props.totalCount ?? 0) - safeLimit() * 2);
const linkClass = 'flex items-center gap-1 no-underline hover:no-underline dark:text-white text-sm';
return (
0 || hasNextPage()}>
);
};
export const LoadingState: Component<{ label: string }> = (props) => (
{props.label}
);
export const SkeletonBlock: Component<{ class?: string }> = (props) => (
);
export const ErrorState: Component<{ message: string }> = (props) => (
{props.message}
);
export const PlaceholderAvatar: Component<{ size?: string; iconSize?: string }> = (props) => (
);
export const Avatar: Component<{ did: string; size?: string }> = (props) => {
const [failed, setFailed] = createSignal(false);
const avatarQuery = createQuery(() => ({
queryKey: ['avatar', props.did],
queryFn: async () => resolveAvatarUrl(props.did),
staleTime: 300_000,
}));
createEffect(() => {
props.did;
setFailed(false);
});
return (
}
>
setFailed(true)}
/>
);
};
export const StateBadge: Component<{
state: 'open' | 'closed' | 'merged';
kind: 'issue' | 'pull';
}> = (props) => {
const isIssue = () => props.kind === 'issue';
const classes = createMemo(() => {
if (props.state === 'open') {
return 'bg-green-600 dark:bg-green-700';
}
if (props.state === 'merged') {
return 'bg-purple-600 dark:bg-purple-700';
}
if (props.kind === 'pull') {
return 'bg-red-600 dark:bg-red-700';
}
return 'bg-gray-800 dark:bg-gray-700';
});
return (
{isIssue() ? : }
{props.state}
);
};
export const cardStyles = (extra?: string): string =>
clsx('bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700', extra);
export const buttonStyles = (variant: 'default' | 'primary' = 'default'): string =>
clsx(
'untangled-button inline-flex items-center gap-2 rounded border px-3 text-sm no-underline hover:no-underline',
variant === 'primary'
? 'btn-create text-white hover:text-white'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/40',
);
export const inputStyles = (): string =>
'untangled-input w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-gray-100';
export const textareaStyles = (): string =>
'untangled-textarea w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-gray-100';