feat(about): add about page with bird sprites
This commit is contained in:
parent
638e4f398e
commit
411ba7b34c
17 changed files with 345 additions and 218 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { BellIcon, EarthIcon, LibraryIcon, PaperclipIcon, UserCircleIcon } from "lucide-react";
|
||||
import { BellIcon, EarthIcon, InfoIcon, LibraryIcon, PaperclipIcon, UserCircleIcon } from "lucide-react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
|
|
@ -40,6 +40,12 @@ const Navigation = (props: Props) => {
|
|||
title: t("common.explore"),
|
||||
icon: <EarthIcon className="w-6 h-auto shrink-0" />,
|
||||
};
|
||||
const aboutNavLink: NavLinkItem = {
|
||||
id: "header-about",
|
||||
path: Routes.ABOUT,
|
||||
title: t("common.about"),
|
||||
icon: <InfoIcon className="w-6 h-auto shrink-0" />,
|
||||
};
|
||||
const attachmentsNavLink: NavLinkItem = {
|
||||
id: "header-attachments",
|
||||
path: Routes.ATTACHMENTS,
|
||||
|
|
@ -71,7 +77,7 @@ const Navigation = (props: Props) => {
|
|||
|
||||
const primaryNavLinks: NavLinkItem[] = currentUser
|
||||
? [homeNavLink, exploreNavLink, attachmentsNavLink, inboxNavLink]
|
||||
: [exploreNavLink, signInNavLink];
|
||||
: [exploreNavLink, aboutNavLink, signInNavLink];
|
||||
const inboxAriaLabel = unreadCount > 0 ? `${t("common.inbox")}, ${unreadCount} unread` : t("common.inbox");
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ These SVGs are pixel-art tile strips for the placeholder component. They should
|
|||
|
||||
## Naming
|
||||
|
||||
- Start with the animal name, for example `Owl` or `Falcon`.
|
||||
- Add the animation name when needed, for example `OwlBlink` or `FalconIdle`.
|
||||
- Start with the animal name, for example `Owl` or `Eagle`.
|
||||
- Add the animation name when needed, for example `OwlBlink` or `EagleIdle`.
|
||||
- Do not name assets after UI states or empty-state scenarios.
|
||||
|
||||
## Frame Count
|
||||
|
|
@ -45,5 +45,5 @@ Avoid padding an animation with duplicate frames just to hit a standard count. A
|
|||
## Current Assets
|
||||
|
||||
- `OwlBlink.svg`: five-frame blink/idle strip with breathing wings, blink, and ear-feather settle.
|
||||
- `FalconIdle.svg`: four-frame idle strip with breathing, blink, alert head shift, and tail flick.
|
||||
- `WoodpeckerPeck.svg`: six-frame tree-trunk peck strip with red crest, chisel beak, barred wing, impact chips, recoil, and settle.
|
||||
- `EagleIdle.svg`: four-frame idle strip with breathing, blink, alert head shift, and tail flick.
|
||||
- `ToucanIdle.svg`: four-frame idle strip with beak bob, chest breathing, blink, tail flick, and settle.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<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">Falcon idle pixel tileset</title>
|
||||
<desc id="desc">A four-frame 32 by 32 pixel falcon idle animation strip.</desc>
|
||||
<title id="title">Eagle idle pixel tileset</title>
|
||||
<desc id="desc">A four-frame 32 by 32 pixel eagle idle animation strip.</desc>
|
||||
<defs>
|
||||
<style>
|
||||
.outline { fill: #120f0d; }
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
.eye-light { fill: #ffd069; }
|
||||
</style>
|
||||
|
||||
<symbol id="falcon-head" viewBox="0 0 32 32">
|
||||
<symbol id="eagle-head" viewBox="0 0 32 32">
|
||||
<path class="outline" d="M14 2h10v1h1v12h2v3h-1v1h-4v-2h-3v-3h-3v-2h-2V9H5V8h1V6h5V4h3V2z" />
|
||||
<path class="head" d="M15 4h8v1h1v10h2v2h-3v-2h-3v-3h-3v-2h-2V8h-4V6h4V4z" />
|
||||
<rect class="head-shadow" x="16" y="14" width="3" height="2" />
|
||||
|
|
@ -34,14 +34,14 @@
|
|||
<rect class="beak-shadow" x="6" y="15" width="8" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="falcon-head-alert" viewBox="0 0 32 32">
|
||||
<use href="#falcon-head" x="-1" />
|
||||
<symbol id="eagle-head-alert" viewBox="0 0 32 32">
|
||||
<use href="#eagle-head" x="-1" />
|
||||
<rect class="outline" x="18" y="7" width="4" height="1" />
|
||||
<rect class="eye" x="19" y="8" width="2" height="2" />
|
||||
<rect class="eye-light" x="20" y="8" width="1" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="falcon-body" viewBox="0 0 32 32">
|
||||
<symbol id="eagle-body" viewBox="0 0 32 32">
|
||||
<path class="outline" d="M7 15h17v2h2v9h-2v3h-4v2H10v-2H7v-3H6v-9h1v-2z" />
|
||||
<path class="body" d="M9 16h14v2h1v8h-2v2H12v-2H9V16z" />
|
||||
<path class="wing" d="M7 17h3v9H8v-1H7v-8z" />
|
||||
|
|
@ -65,8 +65,8 @@
|
|||
<path class="claw" d="M20 26h1v3h2v1h-5v-1h2v-3z" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="falcon-body-breathe" viewBox="0 0 32 32">
|
||||
<use href="#falcon-body" />
|
||||
<symbol id="eagle-body-breathe" viewBox="0 0 32 32">
|
||||
<use href="#eagle-body" />
|
||||
<rect class="outline" x="7" y="16" width="3" height="1" />
|
||||
<rect class="wing" x="7" y="18" width="3" height="8" />
|
||||
<rect class="outline" x="23" y="16" width="3" height="1" />
|
||||
|
|
@ -74,49 +74,49 @@
|
|||
<rect class="body" x="11" y="17" width="10" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="falcon-tail" viewBox="0 0 32 32">
|
||||
<symbol id="eagle-tail" viewBox="0 0 32 32">
|
||||
<path class="outline" d="M23 23h3v2h3v1h2v3h-2v1h-5v-2h-2v-3h1v-2z" />
|
||||
<path class="body-dark" d="M23 24h3v2h2v1h-4v-1h-1v-2z" />
|
||||
<rect class="tail-tip" x="26" y="25" width="2" height="2" />
|
||||
<rect class="tail-tip" x="24" y="27" width="2" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="falcon-tail-flick" viewBox="0 0 32 32">
|
||||
<symbol id="eagle-tail-flick" viewBox="0 0 32 32">
|
||||
<path class="outline" d="M23 22h3v2h3v1h2v3h-2v1h-5v-2h-2v-3h1v-2z" />
|
||||
<path class="body-dark" d="M23 23h3v2h2v1h-4v-1h-1v-2z" />
|
||||
<rect class="tail-tip" x="26" y="24" width="2" height="2" />
|
||||
<rect class="tail-tip" x="24" y="26" width="2" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="falcon-idle-a" viewBox="0 0 32 32">
|
||||
<use href="#falcon-tail" />
|
||||
<use href="#falcon-body" />
|
||||
<use href="#falcon-head" />
|
||||
<symbol id="eagle-idle-a" viewBox="0 0 32 32">
|
||||
<use href="#eagle-tail" />
|
||||
<use href="#eagle-body" />
|
||||
<use href="#eagle-head" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="falcon-idle-b" viewBox="0 0 32 32">
|
||||
<use href="#falcon-tail" />
|
||||
<use href="#falcon-body-breathe" />
|
||||
<use href="#falcon-head" />
|
||||
<symbol id="eagle-idle-b" viewBox="0 0 32 32">
|
||||
<use href="#eagle-tail" />
|
||||
<use href="#eagle-body-breathe" />
|
||||
<use href="#eagle-head" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="falcon-idle-blink" viewBox="0 0 32 32">
|
||||
<use href="#falcon-tail" />
|
||||
<use href="#falcon-body" />
|
||||
<use href="#falcon-head" />
|
||||
<symbol id="eagle-idle-blink" viewBox="0 0 32 32">
|
||||
<use href="#eagle-tail" />
|
||||
<use href="#eagle-body" />
|
||||
<use href="#eagle-head" />
|
||||
<rect class="head" x="20" y="8" width="2" height="2" />
|
||||
<rect class="outline" x="19" y="8" width="3" height="1" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="falcon-idle-c" viewBox="0 0 32 32">
|
||||
<use href="#falcon-tail-flick" />
|
||||
<use href="#falcon-body" />
|
||||
<use href="#falcon-head-alert" />
|
||||
<symbol id="eagle-idle-c" viewBox="0 0 32 32">
|
||||
<use href="#eagle-tail-flick" />
|
||||
<use href="#eagle-body" />
|
||||
<use href="#eagle-head-alert" />
|
||||
</symbol>
|
||||
</defs>
|
||||
|
||||
<use href="#falcon-idle-a" x="0" y="0" width="32" height="32" />
|
||||
<use href="#falcon-idle-b" x="32" y="0" width="32" height="32" />
|
||||
<use href="#falcon-idle-blink" x="64" y="0" width="32" height="32" />
|
||||
<use href="#falcon-idle-c" x="96" y="0" width="32" height="32" />
|
||||
<use href="#eagle-idle-a" x="0" y="0" width="32" height="32" />
|
||||
<use href="#eagle-idle-b" x="32" y="0" width="32" height="32" />
|
||||
<use href="#eagle-idle-blink" x="64" y="0" width="32" height="32" />
|
||||
<use href="#eagle-idle-c" x="96" y="0" width="32" height="32" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
87
web/src/components/Placeholder/pieces/ToucanIdle.svg
Normal file
87
web/src/components/Placeholder/pieces/ToucanIdle.svg
Normal file
|
|
@ -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 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
import FalconIdle from "./pieces/FalconIdle.svg?url";
|
||||
import EagleIdle from "./pieces/EagleIdle.svg?url";
|
||||
import OwlBlink from "./pieces/OwlBlink.svg?url";
|
||||
import WoodpeckerPeck from "./pieces/WoodpeckerPeck.svg?url";
|
||||
import ToucanIdle from "./pieces/ToucanIdle.svg?url";
|
||||
|
||||
export interface TileSprite {
|
||||
name: string;
|
||||
|
|
@ -21,20 +21,20 @@ export const TILE_SPRITES: TileSprite[] = [
|
|||
duration: 1500,
|
||||
},
|
||||
{
|
||||
name: "FalconIdle",
|
||||
src: FalconIdle,
|
||||
name: "EagleIdle",
|
||||
src: EagleIdle,
|
||||
frameWidth: 32,
|
||||
frameHeight: 32,
|
||||
frames: 4,
|
||||
duration: 960,
|
||||
},
|
||||
{
|
||||
name: "WoodpeckerPeck",
|
||||
src: WoodpeckerPeck,
|
||||
name: "ToucanIdle",
|
||||
src: ToucanIdle,
|
||||
frameWidth: 32,
|
||||
frameHeight: 32,
|
||||
frames: 6,
|
||||
duration: 1080,
|
||||
frames: 4,
|
||||
duration: 1120,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const SettingSection: React.FC<SettingSectionProps> = ({ title, description, chi
|
|||
{typeof title === "string" ? <h3>{title}</h3> : title}
|
||||
</div>
|
||||
)}
|
||||
{description && <p className="max-w-2xl text-sm leading-6 text-muted-foreground">{description}</p>}
|
||||
{description && <p className="w-full text-sm leading-6 text-muted-foreground">{description}</p>}
|
||||
</div>
|
||||
{actions && <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
import { ArchiveIcon, CheckIcon, GlobeIcon, LogOutIcon, PaletteIcon, SettingsIcon, SquareUserIcon, User2Icon } from "lucide-react";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
CheckIcon,
|
||||
GlobeIcon,
|
||||
InfoIcon,
|
||||
LogOutIcon,
|
||||
PaletteIcon,
|
||||
SettingsIcon,
|
||||
SquareUserIcon,
|
||||
User2Icon,
|
||||
} from "lucide-react";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { useSSEConnectionStatus } from "@/hooks/useLiveMemoRefresh";
|
||||
|
|
@ -132,6 +142,10 @@ const UserMenu = (props: Props) => {
|
|||
<ArchiveIcon className="size-4 text-muted-foreground" />
|
||||
{t("common.archived")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigateTo(Routes.ABOUT)}>
|
||||
<InfoIcon className="size-4 text-muted-foreground" />
|
||||
{t("common.about")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<GlobeIcon className="size-4 text-muted-foreground" />
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const MainLayout = () => {
|
|||
const location = useLocation();
|
||||
const currentUser = useCurrentUser();
|
||||
const [profileUserName, setProfileUserName] = useState<string | undefined>();
|
||||
const showMemoExplorer = location.pathname !== Routes.ABOUT;
|
||||
|
||||
// Determine context based on current route
|
||||
const context: MemoExplorerContext = useMemo(() => {
|
||||
|
|
@ -69,12 +70,8 @@ const MainLayout = () => {
|
|||
|
||||
return (
|
||||
<section className="@container w-full min-h-full flex flex-col justify-start items-center md:flex-row md:items-start">
|
||||
{!md && (
|
||||
<MobileHeader>
|
||||
<MemoExplorerDrawer {...memoExplorerProps} />
|
||||
</MobileHeader>
|
||||
)}
|
||||
{md && (
|
||||
{!md && <MobileHeader>{showMemoExplorer && <MemoExplorerDrawer {...memoExplorerProps} />}</MobileHeader>}
|
||||
{md && showMemoExplorer && (
|
||||
<div className={DESKTOP_EXPLORER_CLASS_NAME}>
|
||||
<MemoExplorer className="px-3 py-6" {...memoExplorerProps} />
|
||||
</div>
|
||||
|
|
|
|||
159
web/src/pages/About.tsx
Normal file
159
web/src/pages/About.tsx
Normal file
|
|
@ -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;
|
||||
|
|
@ -25,6 +25,7 @@ function lazyWithReload<T extends React.ComponentType>(factory: () => Promise<{
|
|||
}
|
||||
|
||||
const AdminSignIn = lazyWithReload(() => import("@/pages/AdminSignIn"));
|
||||
const About = lazyWithReload(() => import("@/pages/About"));
|
||||
const Archived = lazyWithReload(() => import("@/pages/Archived"));
|
||||
const AuthCallback = lazyWithReload(() => import("@/pages/AuthCallback"));
|
||||
const Explore = lazyWithReload(() => import("@/pages/Explore"));
|
||||
|
|
@ -84,6 +85,7 @@ export const routeConfig: RouteObject[] = [
|
|||
element: <LandingRoute />,
|
||||
children: [{ index: true, element: <Home /> }],
|
||||
},
|
||||
{ path: Routes.ABOUT, element: <About /> },
|
||||
{ path: Routes.EXPLORE, element: <Explore /> },
|
||||
{ path: "u/:username", element: <UserProfile /> },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export const ROUTES = {
|
||||
HOME: "/",
|
||||
ABOUT: "/about",
|
||||
ATTACHMENTS: "/attachments",
|
||||
INBOX: "/inbox",
|
||||
ARCHIVED: "/archived",
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export function buildAuthRoute(options?: { redirect?: string | null; reason?: st
|
|||
|
||||
const PUBLIC_ROUTE_PREFIXES = [
|
||||
ROUTES.AUTH, // Authentication pages
|
||||
ROUTES.ABOUT, // About page
|
||||
ROUTES.EXPLORE, // Explore page
|
||||
`${ROUTES.SHARED_MEMO}/`, // Shared memo pages (share-link viewer)
|
||||
"/u/", // User profile pages (dynamic)
|
||||
|
|
|
|||
21
web/tests/about-page.test.tsx
Normal file
21
web/tests/about-page.test.tsx
Normal file
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -4,11 +4,11 @@ import { DEFAULT_MESSAGES, type PlaceholderVariant } from "@/components/Placehol
|
|||
|
||||
describe("TILE_SPRITES integrity", () => {
|
||||
it("registers 32px by 32px sprite strips with animation-specific frame counts", () => {
|
||||
expect(TILE_SPRITES.map((sprite) => sprite.name)).toEqual(["OwlBlink", "FalconIdle", "WoodpeckerPeck"]);
|
||||
expect(TILE_SPRITES.map((sprite) => sprite.name)).toEqual(["OwlBlink", "EagleIdle", "ToucanIdle"]);
|
||||
expect(TILE_SPRITES.map((sprite) => [sprite.name, sprite.frames])).toEqual([
|
||||
["OwlBlink", 5],
|
||||
["FalconIdle", 4],
|
||||
["WoodpeckerPeck", 6],
|
||||
["EagleIdle", 4],
|
||||
["ToucanIdle", 4],
|
||||
]);
|
||||
|
||||
for (const sprite of TILE_SPRITES) {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ describe("isPublicRoute", () => {
|
|||
it("identifies anonymous-accessible page prefixes", () => {
|
||||
expect(isPublicRoute("/auth")).toBe(true);
|
||||
expect(isPublicRoute("/auth/signup")).toBe(true);
|
||||
expect(isPublicRoute("/about")).toBe(true);
|
||||
expect(isPublicRoute("/explore")).toBe(true);
|
||||
expect(isPublicRoute("/memos/abc")).toBe(true);
|
||||
expect(isPublicRoute("/memos/shares/abc")).toBe(true);
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ describe("router configuration", () => {
|
|||
});
|
||||
|
||||
it("leaves public pages outside RequireAuthRoute", () => {
|
||||
for (const path of [ROUTES.EXPLORE, "memos/:uid", "memos/shares/:token", "u/:username"]) {
|
||||
for (const path of [ROUTES.ABOUT, ROUTES.EXPLORE, "memos/:uid", "memos/shares/:token", "u/:username"]) {
|
||||
expect(hasAncestorOfType(routeConfig, path, RequireAuthRoute)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue