mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: skeleton
This commit is contained in:
1 parent
f7c6fdd861
commit
b50c47b744
11 files changed
+88
-28
No files matched your search
@@ -17,6 +17,13 @@
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
&.skeleton {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
@@ -31,5 +38,10 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { literals } from "@blitzkit/i18n";
|
||||
import { useStrings } from "../../hooks/useStrings";
|
||||
import type { Avatar } from "../../protos/avatar";
|
||||
import type { SkeletonProps } from "../../types/SkeletonProps";
|
||||
import type { PropsWithSkeleton } from "../../types/propsWithSkeleton";
|
||||
import { Badge } from "../Badge";
|
||||
import { InlineSkeleton } from "../InlineSkeleton";
|
||||
import { Skeleton } from "../Skeleton";
|
||||
import { Text } from "../Text";
|
||||
import "./index.css";
|
||||
|
||||
@@ -14,13 +16,13 @@ interface Props {
|
||||
const MAX_CARDS = 3;
|
||||
const CARD_TRIM = "var(--space-3)";
|
||||
|
||||
export function AvatarCard(props: SkeletonProps<Props>) {
|
||||
export function AvatarCard(props: PropsWithSkeleton<Props>) {
|
||||
return (
|
||||
<div className="avatar-card">
|
||||
{!props.skeleton && <Series avatars={props.avatars} />}
|
||||
<Series skeleton={props.skeleton} {...props} />
|
||||
|
||||
<Text align="center" className="avatar-label">
|
||||
{props.skeleton ? "LOADING" : props.name}
|
||||
{props.skeleton ? <InlineSkeleton /> : props.name}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
@@ -30,32 +32,35 @@ interface SeriesProps {
|
||||
avatars: Avatar[];
|
||||
}
|
||||
|
||||
function Series({ avatars }: SeriesProps) {
|
||||
const count = Math.min(avatars.length, MAX_CARDS);
|
||||
function Series(props: PropsWithSkeleton<SeriesProps>) {
|
||||
const count = Math.min(props.skeleton ? 0 : props.avatars.length, MAX_CARDS);
|
||||
const strings = useStrings();
|
||||
|
||||
return (
|
||||
<div className="avatar-series">
|
||||
{avatars.slice(-MAX_CARDS).map((avatar, index) => (
|
||||
<div
|
||||
key={avatar.name}
|
||||
className="avatar-entry"
|
||||
style={{
|
||||
filter: `brightness(${(index + 1) / count})`,
|
||||
top: `calc(${index} * ${CARD_TRIM})`,
|
||||
left: `calc(${index} * ${CARD_TRIM})`,
|
||||
width: `calc(100% - ${CARD_TRIM} * ${count - 1})`,
|
||||
height: `calc(100% - ${CARD_TRIM} * ${count - 1})`,
|
||||
backgroundImage: `url(/api/avatars/${avatar.name}.webp)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{!props.skeleton &&
|
||||
props.avatars.slice(-MAX_CARDS).map((avatar, index) => (
|
||||
<div
|
||||
key={avatar.name}
|
||||
className="avatar-entry"
|
||||
style={{
|
||||
filter: `brightness(${(index + 1) / count})`,
|
||||
top: `calc(${index} * ${CARD_TRIM})`,
|
||||
left: `calc(${index} * ${CARD_TRIM})`,
|
||||
width: `calc(100% - ${CARD_TRIM} * ${count - 1})`,
|
||||
height: `calc(100% - ${CARD_TRIM} * ${count - 1})`,
|
||||
backgroundImage: `url(/api/avatars/${avatar.name}.webp)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{avatars.length > 1 && (
|
||||
{!props.skeleton && props.avatars.length > 1 && (
|
||||
<Badge color="gray" highContrast className="count">
|
||||
{literals(strings.units.x, { value: avatars.length })}
|
||||
{literals(strings.units.x, { value: props.avatars.length })}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{props.skeleton && <Skeleton className="avatar-entry" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
.avatars-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--space-7), 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
|
||||
gap: var(--space-5) var(--space-4);
|
||||
}
|
||||
@@ -75,8 +75,8 @@ export function AvatarsList() {
|
||||
return (
|
||||
<div className="avatars-list">
|
||||
<IncrementalLoader
|
||||
initial={40}
|
||||
intermediate={10}
|
||||
initial={9 * 5}
|
||||
intermediate={9 * 3}
|
||||
data={filtered}
|
||||
Component={AvatarCard}
|
||||
/>
|
||||
|
||||
@@ -80,5 +80,6 @@ import "@fontsource-variable/roboto-flex";
|
||||
--blur-tint: blur(8rem);
|
||||
|
||||
--duration-brief: 200ms;
|
||||
--duration-subtle: 2s;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { times } from "lodash-es";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import type { SkeletonProps } from "../../types/SkeletonProps";
|
||||
import type { PropsWithSkeleton } from "../../types/propsWithSkeleton";
|
||||
import "./index.css";
|
||||
|
||||
export interface IncrementalLoaderProps<Props extends { key: string }> {
|
||||
@@ -10,7 +10,7 @@ export interface IncrementalLoaderProps<Props extends { key: string }> {
|
||||
|
||||
skeleton?: boolean;
|
||||
data: Props[];
|
||||
Component: (props: SkeletonProps<Omit<Props, "key">>) => ReactNode;
|
||||
Component: (props: PropsWithSkeleton<Omit<Props, "key">>) => ReactNode;
|
||||
}
|
||||
|
||||
export function IncrementalLoader<Props extends { key: string }>({
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.inline-skeleton {
|
||||
display: inline-block;
|
||||
|
||||
height: 1em;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import { Skeleton, type SkeletonProps } from "../Skeleton";
|
||||
import "./index.css";
|
||||
|
||||
export interface InlineSkeletonProps extends SkeletonProps {}
|
||||
|
||||
export function InlineSkeleton({ className, ...props }: InlineSkeletonProps) {
|
||||
return (
|
||||
<Skeleton className={classNames("inline-skeleton", className)} {...props} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@keyframes skeleton-animation {
|
||||
0% {
|
||||
background-color: var(--black-a8);
|
||||
}
|
||||
50% {
|
||||
background-color: var(--black-a5);
|
||||
}
|
||||
100% {
|
||||
background-color: var(--black-a8);
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
animation: skeleton-animation var(--duration-subtle) ease infinite;
|
||||
|
||||
border-radius: var(--space-2);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import "./index.css";
|
||||
|
||||
export interface SkeletonProps extends ComponentProps<"div"> {}
|
||||
|
||||
export function Skeleton({ className, ...props }: SkeletonProps) {
|
||||
return <div className={classNames("skeleton", className)} {...props} />;
|
||||
}
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
export type SkeletonProps<Props> =
|
||||
export type PropsWithSkeleton<Props> =
|
||||
| { skeleton: true }
|
||||
| (Props & { skeleton?: false });
|
||||
Reference in new issue
Block a user