mirror of https://github.com/usememos/memos
feat(about): add about page with bird sprites
parent
638e4f398e
commit
411ba7b34c
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
@ -0,0 +1,87 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="32" viewBox="0 0 128 32" role="img" aria-labelledby="title desc" shape-rendering="crispEdges">
|
||||
<title id="title">Toucan idle pixel tileset</title>
|
||||
<desc id="desc">A four-frame 32 by 32 pixel toucan idle animation strip.</desc>
|
||||
<defs>
|
||||
<style>
|
||||
.outline { fill: #2f2a24; }
|
||||
.body { fill: #1f2528; }
|
||||
.bodyHighlight { fill: #354046; }
|
||||
.face { fill: #2b3539; }
|
||||
.chest { fill: #f1d88b; }
|
||||
.chestShadow { fill: #c99b55; }
|
||||
.beak { fill: #e57f24; }
|
||||
.beakHighlight { fill: #f4c247; }
|
||||
.beakAccent { fill: #b93d22; }
|
||||
.eye { fill: #120f0d; }
|
||||
.eyeLight { fill: #f4ead5; }
|
||||
.foot { fill: #b86f2e; }
|
||||
</style>
|
||||
|
||||
<symbol id="toucan-open" viewBox="0 0 32 32">
|
||||
<path class="outline" d="M14 5h7v2h3v4h2v12h-2v4h-3v2h-8v-2h-3v-5H8v-3H5v-2H2v-6h2V9h4V7h6V5z" />
|
||||
<path class="beak" d="M4 10h10v1h3v3h-1v3H4v-1H3v-4h1v-2z" />
|
||||
<path class="beakHighlight" d="M5 10h8v1h2v2H5v-3z" />
|
||||
<rect class="beakAccent" x="3" y="14" width="4" height="2" />
|
||||
|
||||
<path class="body" d="M15 8h6v2h2v12h-2v3h-3v2h-5v-2h-2v-5h2v-3h2V8z" />
|
||||
<path class="face" d="M15 8h5v2h1v5h-2v1h-4V8z" />
|
||||
<path class="bodyHighlight" d="M18 10h3v8h-1v2h-2V10z" />
|
||||
<rect class="chest" x="15" y="9" width="2" height="2" />
|
||||
<path class="chest" d="M11 16h6v8h-1v2h-3v-2h-2v-8z" />
|
||||
<rect class="chestShadow" x="12" y="23" width="4" height="1" />
|
||||
<rect class="body" x="16" y="17" width="1" height="5" />
|
||||
|
||||
<rect class="eyeLight" x="15" y="9" width="2" height="2" />
|
||||
<rect class="eye" x="16" y="10" width="1" height="1" />
|
||||
<rect class="bodyHighlight" x="19" y="12" width="1" height="4" />
|
||||
|
||||
<path class="body" d="M24 20h3v2h2v2h-4v-1h-1v-3z" />
|
||||
<rect class="bodyHighlight" x="25" y="21" width="2" height="2" />
|
||||
<rect class="foot" x="13" y="27" width="2" height="3" />
|
||||
<rect class="foot" x="18" y="27" width="2" height="3" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="toucan-breathe" viewBox="0 0 32 32">
|
||||
<path class="outline" d="M14 6h7v2h3v4h2v11h-2v4h-3v2h-8v-2h-3v-5H8v-3H5v-2H2v-6h2V9h4V7h6V6z" />
|
||||
<path class="beak" d="M4 11h10v1h3v3h-1v3H4v-1H3v-4h1v-2z" />
|
||||
<path class="beakHighlight" d="M5 11h8v1h2v2H5v-3z" />
|
||||
<rect class="beakAccent" x="3" y="15" width="4" height="2" />
|
||||
|
||||
<path class="body" d="M15 9h6v2h2v11h-2v3h-3v2h-5v-2h-2v-5h2v-3h2V9z" />
|
||||
<path class="face" d="M15 9h5v2h1v5h-2v1h-4V9z" />
|
||||
<path class="bodyHighlight" d="M18 11h3v7h-1v2h-2v-9z" />
|
||||
<rect class="chest" x="15" y="10" width="2" height="2" />
|
||||
<path class="chest" d="M10 16h7v9h-1v2h-3v-2h-3v-9z" />
|
||||
<rect class="chestShadow" x="11" y="24" width="5" height="1" />
|
||||
<rect class="body" x="17" y="17" width="1" height="5" />
|
||||
|
||||
<rect class="eyeLight" x="15" y="10" width="2" height="2" />
|
||||
<rect class="eye" x="16" y="11" width="1" height="1" />
|
||||
<rect class="bodyHighlight" x="19" y="13" width="1" height="3" />
|
||||
|
||||
<path class="body" d="M25 21h3v2h1v2h-4v-1h-1v-2h1v-1z" />
|
||||
<rect class="bodyHighlight" x="26" y="22" width="2" height="2" />
|
||||
<rect class="foot" x="13" y="27" width="2" height="3" />
|
||||
<rect class="foot" x="18" y="27" width="2" height="3" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="toucan-blink" viewBox="0 0 32 32">
|
||||
<use href="#toucan-open" />
|
||||
<rect class="face" x="15" y="9" width="2" height="2" />
|
||||
<rect class="eye" x="15" y="10" width="2" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="toucan-settle" viewBox="0 0 32 32">
|
||||
<use href="#toucan-open" />
|
||||
<rect class="bodyHighlight" x="18" y="10" width="3" height="2" />
|
||||
<rect class="chest" x="12" y="17" width="4" height="1" />
|
||||
<rect class="body" x="24" y="21" width="3" height="2" />
|
||||
<rect class="bodyHighlight" x="25" y="22" width="2" height="1" />
|
||||
</symbol>
|
||||
</defs>
|
||||
|
||||
<use href="#toucan-open" x="0" y="0" width="32" height="32" />
|
||||
<use href="#toucan-breathe" x="32" y="0" width="32" height="32" />
|
||||
<use href="#toucan-blink" x="64" y="0" width="32" height="32" />
|
||||
<use href="#toucan-settle" x="96" y="0" width="32" height="32" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@ -1,162 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="32" viewBox="0 0 192 32" role="img" aria-labelledby="title desc" shape-rendering="crispEdges">
|
||||
<title id="title">Woodpecker peck pixel tileset</title>
|
||||
<desc id="desc">A six-frame 32 by 32 pixel woodpecker pecking a tree trunk.</desc>
|
||||
<defs>
|
||||
<style>
|
||||
.outline { fill: #17110e; }
|
||||
.bark { fill: #6c4b34; }
|
||||
.bark-dark { fill: #3f2b20; }
|
||||
.bark-light { fill: #9b6a45; }
|
||||
.crest { fill: #c8442f; }
|
||||
.crest-dark { fill: #7c261d; }
|
||||
.head { fill: #f0d3a4; }
|
||||
.cheek { fill: #f2efe7; }
|
||||
.head-shadow { fill: #b78354; }
|
||||
.beak { fill: #2f2a24; }
|
||||
.body { fill: #835b3c; }
|
||||
.belly { fill: #e6d7bf; }
|
||||
.wing { fill: #27201d; }
|
||||
.wing-light { fill: #f2efe7; }
|
||||
.stripe { fill: #5b4d42; }
|
||||
.tail { fill: #2f2a24; }
|
||||
.claw { fill: #8e8376; }
|
||||
.eye { fill: #120f0d; }
|
||||
.eye-light { fill: #f4ead5; }
|
||||
.chip { fill: #d8a55f; }
|
||||
</style>
|
||||
|
||||
<symbol id="trunk" viewBox="0 0 32 32">
|
||||
<path class="outline" d="M1 0h5v32H1V0z" />
|
||||
<rect class="bark" x="2" y="0" width="3" height="32" />
|
||||
<rect class="bark-dark" x="2" y="6" width="1" height="5" />
|
||||
<rect class="bark-dark" x="4" y="17" width="1" height="6" />
|
||||
<rect class="bark-light" x="4" y="2" width="1" height="4" />
|
||||
<rect class="bark-light" x="3" y="24" width="1" height="5" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="woodpecker-rest" viewBox="0 0 32 32">
|
||||
<use href="#trunk" />
|
||||
|
||||
<path class="outline" d="M14 3h4v2h2v3h2v4h2v2h2v12h-2v3h-4v2h-7v-2h-2v-3H9v-4h1v-5H8v-2H6v-2h3V8h2V5h3V3z" />
|
||||
<path class="crest" d="M14 4h3v2h2v2h1v2h-5V8h-2V6h1V4z" />
|
||||
<rect class="crest-dark" x="16" y="6" width="2" height="2" />
|
||||
|
||||
<path class="head" d="M11 8h8v2h2v4h-2v2h-7v-2h-2v-4h1V8z" />
|
||||
<rect class="cheek" x="11" y="12" width="4" height="3" />
|
||||
<rect class="head-shadow" x="16" y="14" width="3" height="2" />
|
||||
<rect class="eye-light" x="14" y="9" width="2" height="2" />
|
||||
<rect class="eye" x="14" y="9" width="1" height="1" />
|
||||
<path class="beak" d="M5 11h7v1H5v-1z" />
|
||||
<rect class="beak" x="6" y="12" width="6" height="1" />
|
||||
|
||||
<path class="body" d="M13 16h9v2h2v8h-2v2h-8v-2h-2v-8h1v-2z" />
|
||||
<path class="belly" d="M13 17h4v8h-1v1h-2v-2h-1v-7z" />
|
||||
<path class="wing" d="M17 16h7v3h-1v5h-2v2h-5v-9h1v-1z" />
|
||||
<rect class="wing-light" x="18" y="17" width="3" height="1" />
|
||||
<rect class="wing-light" x="20" y="20" width="3" height="1" />
|
||||
<rect class="wing-light" x="17" y="23" width="3" height="1" />
|
||||
<rect class="stripe" x="18" y="18" width="2" height="1" />
|
||||
<rect class="stripe" x="21" y="21" width="2" height="1" />
|
||||
<path class="tail" d="M22 24h3v2h2v3h-2v1h-4v-2h1v-4z" />
|
||||
<rect class="claw" x="7" y="17" width="4" height="1" />
|
||||
<rect class="claw" x="8" y="24" width="4" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="woodpecker-lean" viewBox="0 0 32 32">
|
||||
<use href="#trunk" />
|
||||
|
||||
<path class="outline" d="M13 3h4v2h2v3h2v4h2v2h2v12h-2v3h-4v2h-7v-2h-2v-3H8v-4h1v-5H7v-2H5v-2h3V8h2V5h3V3z" />
|
||||
<path class="crest" d="M13 4h3v2h2v2h1v2h-5V8h-2V6h1V4z" />
|
||||
<rect class="crest-dark" x="15" y="6" width="2" height="2" />
|
||||
<path class="head" d="M10 8h8v2h2v4h-2v2h-7v-2H9v-4h1V8z" />
|
||||
<rect class="cheek" x="10" y="12" width="4" height="3" />
|
||||
<rect class="head-shadow" x="15" y="14" width="3" height="2" />
|
||||
<rect class="eye-light" x="13" y="9" width="2" height="2" />
|
||||
<rect class="eye" x="13" y="9" width="1" height="1" />
|
||||
<path class="beak" d="M4 11h7v1H4v-1z" />
|
||||
<rect class="beak" x="5" y="12" width="6" height="1" />
|
||||
|
||||
<path class="body" d="M12 16h9v2h2v8h-2v2h-8v-2h-2v-8h1v-2z" />
|
||||
<path class="belly" d="M12 17h4v8h-1v1h-2v-2h-1v-7z" />
|
||||
<path class="wing" d="M16 16h7v3h-1v5h-2v2h-5v-9h1v-1z" />
|
||||
<rect class="wing-light" x="17" y="17" width="3" height="1" />
|
||||
<rect class="wing-light" x="19" y="20" width="3" height="1" />
|
||||
<rect class="wing-light" x="16" y="23" width="3" height="1" />
|
||||
<path class="tail" d="M21 24h3v2h2v3h-2v1h-4v-2h1v-4z" />
|
||||
<rect class="claw" x="7" y="17" width="4" height="1" />
|
||||
<rect class="claw" x="8" y="24" width="4" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="woodpecker-strike" viewBox="0 0 32 32">
|
||||
<use href="#trunk" />
|
||||
|
||||
<path class="outline" d="M12 4h4v2h2v3h2v4h2v2h2v11h-2v3h-4v2h-7v-2H9v-3H7v-4h1v-5H6v-2H4v-3h4V9h2V6h2V4z" />
|
||||
<path class="crest" d="M12 5h3v2h2v2h1v2h-5V9h-2V7h1V5z" />
|
||||
<rect class="crest-dark" x="14" y="7" width="2" height="2" />
|
||||
<path class="head" d="M9 9h8v2h2v4h-2v2h-7v-2H8v-4h1V9z" />
|
||||
<rect class="cheek" x="9" y="13" width="4" height="3" />
|
||||
<rect class="head-shadow" x="14" y="15" width="3" height="2" />
|
||||
<rect class="eye-light" x="12" y="10" width="2" height="2" />
|
||||
<rect class="eye" x="12" y="10" width="1" height="1" />
|
||||
<path class="beak" d="M3 12h7v1H3v-1z" />
|
||||
<rect class="beak" x="4" y="13" width="6" height="1" />
|
||||
|
||||
<path class="body" d="M11 17h9v2h2v7h-2v2h-8v-2h-2v-8h1v-1z" />
|
||||
<path class="belly" d="M11 18h4v7h-1v1h-2v-2h-1v-6z" />
|
||||
<path class="wing" d="M15 17h7v3h-1v4h-2v2h-5v-8h1v-1z" />
|
||||
<rect class="wing-light" x="16" y="18" width="3" height="1" />
|
||||
<rect class="wing-light" x="18" y="21" width="3" height="1" />
|
||||
<rect class="wing-light" x="15" y="23" width="3" height="1" />
|
||||
<path class="tail" d="M20 24h3v2h2v3h-2v1h-4v-2h1v-4z" />
|
||||
<rect class="claw" x="7" y="17" width="4" height="1" />
|
||||
<rect class="claw" x="8" y="24" width="4" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="woodpecker-impact" viewBox="0 0 32 32">
|
||||
<use href="#woodpecker-strike" />
|
||||
<rect class="chip" x="1" y="10" width="1" height="1" />
|
||||
<rect class="chip" x="3" y="9" width="1" height="1" />
|
||||
<rect class="chip" x="2" y="14" width="1" height="1" />
|
||||
<rect class="bark-light" x="5" y="12" width="1" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="woodpecker-recoil" viewBox="0 0 32 32">
|
||||
<use href="#trunk" />
|
||||
|
||||
<path class="outline" d="M15 2h4v2h2v3h2v4h2v2h2v13h-2v3h-4v2h-7v-2h-2v-3h-2v-4h1v-5H9v-2H7v-2h3V7h2V4h3V2z" />
|
||||
<path class="crest" d="M15 3h3v2h2v2h1v2h-5V7h-2V5h1V3z" />
|
||||
<rect class="crest-dark" x="17" y="5" width="2" height="2" />
|
||||
<path class="head" d="M12 7h8v2h2v4h-2v2h-7v-2h-2V9h1V7z" />
|
||||
<rect class="cheek" x="12" y="11" width="4" height="3" />
|
||||
<rect class="eye-light" x="15" y="8" width="2" height="2" />
|
||||
<rect class="eye" x="15" y="8" width="1" height="1" />
|
||||
<path class="beak" d="M6 10h7v1H6v-1z" />
|
||||
<rect class="beak" x="7" y="11" width="6" height="1" />
|
||||
|
||||
<path class="body" d="M14 16h9v2h2v8h-2v2h-8v-2h-2v-8h1v-2z" />
|
||||
<path class="belly" d="M14 17h4v8h-1v1h-2v-2h-1v-7z" />
|
||||
<path class="wing" d="M18 16h7v3h-1v5h-2v2h-5v-9h1v-1z" />
|
||||
<rect class="wing-light" x="19" y="17" width="3" height="1" />
|
||||
<rect class="wing-light" x="21" y="20" width="3" height="1" />
|
||||
<rect class="wing-light" x="18" y="23" width="3" height="1" />
|
||||
<path class="tail" d="M24 24h3v2h2v3h-2v1h-4v-2h1v-4z" />
|
||||
<rect class="claw" x="8" y="17" width="4" height="1" />
|
||||
<rect class="claw" x="9" y="24" width="4" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="woodpecker-settle" viewBox="0 0 32 32">
|
||||
<use href="#woodpecker-rest" />
|
||||
<rect class="crest-dark" x="14" y="5" width="3" height="1" />
|
||||
<rect class="wing-light" x="21" y="19" width="2" height="1" />
|
||||
<rect class="claw" x="8" y="18" width="3" height="1" />
|
||||
<rect class="claw" x="9" y="25" width="3" height="1" />
|
||||
</symbol>
|
||||
</defs>
|
||||
|
||||
<use href="#woodpecker-rest" x="0" y="0" width="32" height="32" />
|
||||
<use href="#woodpecker-lean" x="32" y="0" width="32" height="32" />
|
||||
<use href="#woodpecker-strike" x="64" y="0" width="32" height="32" />
|
||||
<use href="#woodpecker-impact" x="96" y="0" width="32" height="32" />
|
||||
<use href="#woodpecker-recoil" x="128" y="0" width="32" height="32" />
|
||||
<use href="#woodpecker-settle" x="160" y="0" width="32" height="32" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.0 KiB |
@ -0,0 +1,159 @@
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { TILE_SPRITES, type TileSprite } from "@/components/Placeholder/tileSprites";
|
||||
import SettingGroup from "@/components/Settings/SettingGroup";
|
||||
import SettingSection from "@/components/Settings/SettingSection";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const SPRITE_SCALE = 2;
|
||||
|
||||
const PRODUCT_LINKS = [
|
||||
{ label: "Website", href: "https://usememos.com/" },
|
||||
{ label: "GitHub", href: "https://github.com/usememos/memos" },
|
||||
{ label: "Docs", href: "https://usememos.com/docs" },
|
||||
];
|
||||
|
||||
const PRODUCT_POINTS = ["Open. Write. Done.", "Markdown-native.", "Fully yours."];
|
||||
|
||||
const BIRD_META: Record<TileSprite["name"], { label: string; description: string }> = {
|
||||
OwlBlink: {
|
||||
label: "Owl",
|
||||
description: "Night watch idle with a compact blink.",
|
||||
},
|
||||
EagleIdle: {
|
||||
label: "Eagle",
|
||||
description: "Perched idle with a sharp head and steady chest motion.",
|
||||
},
|
||||
ToucanIdle: {
|
||||
label: "Toucan",
|
||||
description: "Calm tropical idle built around a large curved beak.",
|
||||
},
|
||||
};
|
||||
|
||||
const getBirdAnimationName = (sprite: TileSprite) => `about-bird-${sprite.name}`;
|
||||
|
||||
const BIRD_KEYFRAMES_CSS = `
|
||||
${TILE_SPRITES.map(
|
||||
(sprite) => `
|
||||
@keyframes ${getBirdAnimationName(sprite)} {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-${sprite.frameWidth * sprite.frames * SPRITE_SCALE}px); }
|
||||
}
|
||||
`,
|
||||
).join("\n")}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.about-bird-strip {
|
||||
animation: none !important;
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const BirdSprite = ({ sprite }: { sprite: TileSprite }) => {
|
||||
const stripWidth = sprite.frameWidth * sprite.frames;
|
||||
const displayWidth = stripWidth * SPRITE_SCALE;
|
||||
const displayHeight = sprite.frameHeight * SPRITE_SCALE;
|
||||
const frameDisplayWidth = displayWidth / sprite.frames;
|
||||
const meta = BIRD_META[sprite.name];
|
||||
|
||||
return (
|
||||
<figure className="flex min-w-0 flex-1 basis-36 flex-col items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-4 text-center">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-testid="about-bird-sprite"
|
||||
className="relative size-16 shrink-0 overflow-hidden"
|
||||
style={{ width: frameDisplayWidth, height: displayHeight }}
|
||||
>
|
||||
<img
|
||||
className="about-bird-strip"
|
||||
src={sprite.src}
|
||||
alt=""
|
||||
width={stripWidth}
|
||||
height={sprite.frameHeight}
|
||||
draggable={false}
|
||||
style={{
|
||||
display: "block",
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
maxWidth: "none",
|
||||
imageRendering: "pixelated",
|
||||
animationName: getBirdAnimationName(sprite),
|
||||
animationDuration: `${sprite.duration}ms`,
|
||||
animationTimingFunction: `steps(${sprite.frames})`,
|
||||
animationIterationCount: "infinite",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<figcaption className="min-w-0">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<h3 className="font-mono text-sm font-semibold text-foreground">{sprite.name}</h3>
|
||||
<span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">{meta.label}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-muted-foreground">{meta.description}</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
};
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<section className="mx-auto w-full max-w-5xl min-h-full flex flex-col justify-start items-start sm:pt-3 md:pt-6 pb-8">
|
||||
<style>{BIRD_KEYFRAMES_CSS}</style>
|
||||
|
||||
<div className="w-full px-4 sm:px-6">
|
||||
<div className="w-full rounded-xl border border-border bg-background px-4 py-4 text-muted-foreground">
|
||||
<SettingSection
|
||||
title="About Memos"
|
||||
description="Open-source, self-hosted note-taking built for quick capture: Markdown-native, lightweight, and fully yours."
|
||||
>
|
||||
<SettingGroup>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<img className="size-12 shrink-0 select-none rounded-md" src="/logo.webp" alt="" draggable={false} />
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-foreground">Memos</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Capture first. Keep it yours.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-wrap gap-2">
|
||||
{PRODUCT_LINKS.map((link) => (
|
||||
<Button key={link.href} asChild variant="outline" size="lg">
|
||||
<a href={link.href} target="_blank" rel="noreferrer">
|
||||
{link.label}
|
||||
<ExternalLinkIcon className="size-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SettingGroup>
|
||||
|
||||
<SettingGroup
|
||||
showSeparator
|
||||
title="Product"
|
||||
description="A small timeline for notes that should be saved now and organized later."
|
||||
>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{PRODUCT_POINTS.map((item) => (
|
||||
<div key={item} className="rounded-lg bg-muted/40 px-3 py-2 text-sm text-foreground">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SettingGroup>
|
||||
|
||||
<SettingGroup showSeparator title="Birds" description="Pixel tile strips used by empty states.">
|
||||
<section aria-label="Birds" className="flex flex-row flex-wrap gap-3">
|
||||
{TILE_SPRITES.map((sprite) => (
|
||||
<BirdSprite key={sprite.name} sprite={sprite} />
|
||||
))}
|
||||
</section>
|
||||
</SettingGroup>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@ -0,0 +1,21 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TILE_SPRITES } from "@/components/Placeholder/tileSprites";
|
||||
import About from "@/pages/About";
|
||||
|
||||
describe("<About>", () => {
|
||||
it("renders the product story and current bird sprites", () => {
|
||||
render(<About />);
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Memos" })).toBeInTheDocument();
|
||||
expect(screen.getByText(/Capture first/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/quick capture/i)).toBeInTheDocument();
|
||||
|
||||
const birds = screen.getByRole("region", { name: "Birds" });
|
||||
expect(within(birds).getAllByTestId("about-bird-sprite")).toHaveLength(TILE_SPRITES.length);
|
||||
|
||||
for (const sprite of TILE_SPRITES) {
|
||||
expect(within(birds).getByText(sprite.name)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue