Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions apps/web/content/docs/brand/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: Introduction
description: The VitNode brand - Tabby the pet, the hexagon mark and the blue palette, ready to use in your community and plugins.
icon: Power
---

The Brand section collects what makes VitNode look like VitNode, so your
community, plugin or blog post can use it without guessing. Everything here
ships in `@vitnode/core` or downloads straight from the page.

<Preview name="pet" />

## What's here

- **Tabby, the pet.** A striped hexagon cat with 14 states, from `hello` to
`dizzy`. Its body is the logo's hexagon and its colors are the logo's blues,
so it never looks off-brand next to the mark.
- **The palette.** Tabby and the logo use the same blues as the UI's primary
color, documented with every other token on the Colors page.

## Related

<Cards>
<Card
title="Pet"
href="/docs/brand/pet"
description="Use Tabby in your UI, or download it as SVG or PNG."
/>
<Card
title="Colors"
href="/docs/ui/colors"
description="The tokens behind VitNode's blue."
/>
</Cards>
7 changes: 7 additions & 0 deletions apps/web/content/docs/brand/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"title": "Brand",
"description": "VitNode's look and personality, starting with Tabby the pet",
"icon": "Sparkles",
"root": true,
"pages": ["index", "pet"]
}
110 changes: 110 additions & 0 deletions apps/web/content/docs/brand/pet.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
title: Pet
description: Tabby, the VitNode pet - an animated SVG mascot with 14 states for welcome screens, loading, empty states and error pages, plus SVG and PNG downloads.
icon: Cat
---

Tabby is VitNode's pet: a striped hexagon cat that ships as one React component
with 14 states. Drop it into an empty state, a loading screen or an error page
and the page instantly feels less like a stack trace.

<Preview name="pet" />

## Use the pet

Import `Pet` from core and pick a `state`. The pet scales to its box, so size it
with Tailwind classes. Keep roughly a 7:8 width-to-height ratio.

```tsx title="welcome.tsx"
import { Pet } from '@vitnode/core/components/pet/pet'

export const Welcome = () => <Pet className="h-44 w-40" state="hello" />
```

## States

<Preview name="pet-states" className="*:max-w-full" />

Every card has three buttons:

- **Copy** puts the SVG markup on your clipboard, ready to paste into code or a
design tool.
- **SVG** downloads an animated file. The animation styles travel inside it, so
Tabby keeps moving wherever you open it.
- **PNG** downloads a still, 1024 px wide image with a transparent background.

| State | Use it for |
| ----------- | ------------------------------ |
| `idle` | Default pose |
| `hello` | Onboarding and welcome screens |
| `coding` | Docs and developer pages |
| `coffee` | Breaks and quiet days |
| `love` | Thank-you screens |
| `music` | Media and playlists |
| `sleeping` | Idle or maintenance mode |
| `thinking` | Loading states |
| `celebrate` | Success and first publish |
| `searching` | Empty search results |
| `offline` | Lost connections |
| `oops` | 404 pages |
| `dizzy` | 500 errors |
| `building` | Maintenance and coming soon |

## Error pages

`ErrorContent` draws the status code in huge digits and lets Tabby stand in for
the middle one: a 404 shows `oops` between the two 4s, and a 500 shows `dizzy`
between the 5 and the 0. The built-in pages need no extra code. Other codes, such
as 403, keep all three digits and skip the pet. Screen readers hear the code and
title together, not the decorative digits.

## Props

import { TypeTable } from 'fumadocs-ui/components/type-table'

<TypeTable
type={{
state: {
description: 'Which pose and animation to show.',
type: 'PetState',
default: "'idle'",
},
label: {
description:
'Accessible name. Without it, the pet is decorative and hidden from screen readers.',
type: 'string',
},
className: {
description: 'Classes for the SVG, usually its size.',
type: 'string',
},
}}
/>

## Accessibility

- The pet is decorative by default (`aria-hidden`). When it is the only thing
that explains the screen, pass a `label` and it becomes an image with that
name.
- Only parts move: tail, ears, blinks, paws and props. The body never floats.
- All animation stops when the visitor turns on reduced motion.

## Related

<Cards>
<Card
title="Empty"
href="/docs/ui/empty"
description="Empty states that a pet fits into."
/>
<Card
title="Motion"
href="/docs/ui/motion"
description="The motion rules the pet follows."
/>
<Card
title="Accessibility"
href="/docs/ui/accessibility"
description="Labels, reduced motion and screen readers."
/>
</Cards>
3 changes: 3 additions & 0 deletions apps/web/content/docs/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"pages": ["dev", "guides", "ui", "brand"]
}
104 changes: 104 additions & 0 deletions apps/web/src/docs/examples/pet-states.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
downloadPetPng,
downloadPetSvg,
getPetSvgMarkup,
} from '@vitnode/core/components/pet/export'
import { Pet, type PetState } from '@vitnode/core/components/pet/pet'
import { Button } from '@vitnode/core/components/ui/button'
import { ButtonGroup } from '@vitnode/core/components/ui/button-group'
import { CopyButton } from '@vitnode/core/components/ui/copy-button'
import { DownloadIcon } from 'lucide-react'
import React from 'react'
import { toast } from 'sonner'

const STATES: { name: string; state: PetState; use: string }[] = [
{ name: 'Idle', state: 'idle', use: 'Default pose' },
{ name: 'Hello', state: 'hello', use: 'Onboarding, welcome screens' },
{ name: 'Coding', state: 'coding', use: 'Docs and developer pages' },
{ name: 'Coffee', state: 'coffee', use: 'Breaks, quiet days' },
{ name: 'Love', state: 'love', use: 'Thank-you screens' },
{ name: 'Music', state: 'music', use: 'Media and playlists' },
{ name: 'Sleeping', state: 'sleeping', use: 'Idle or maintenance mode' },
{ name: 'Thinking', state: 'thinking', use: 'Loading states' },
{ name: 'Celebrate', state: 'celebrate', use: 'Success, first publish' },
{ name: 'Searching', state: 'searching', use: 'Empty search results' },
{ name: 'Offline', state: 'offline', use: 'Lost connection' },
{ name: 'Oops', state: 'oops', use: '404 pages' },
{ name: 'Dizzy', state: 'dizzy', use: '500 errors' },
{ name: 'Building', state: 'building', use: 'Maintenance, coming soon' },
]

const PetCard = ({ name, state, use }: (typeof STATES)[number]) => {
const stageRef = React.useRef<HTMLDivElement>(null)
const fileName = `vitnode-pet-${state}`

const petSvg = () => {
const svg = stageRef.current?.querySelector('svg')
if (!(svg instanceof SVGSVGElement))
throw new Error('Pet is not rendered yet')

return svg
}

const downloadPng = async () => {
try {
await downloadPetPng(petSvg(), fileName)
} catch {
toast.error('Download failed', {
description: `Your browser could not turn ${name} into a PNG. Try the SVG instead.`,
})
}
}

return (
<li className="bg-card text-card-foreground flex h-full flex-col items-center gap-4 rounded-xl border p-4">
<div className="h-36 w-32" ref={stageRef}>
<Pet className="size-full" state={state} />
</div>
<div className="flex flex-col items-center gap-1 text-center">
<span className="text-sm font-semibold">{name}</span>
<code className="text-muted-foreground text-xs">state="{state}"</code>
<span className="text-muted-foreground text-sm leading-relaxed text-pretty">
{use}
</span>
</div>
<ButtonGroup aria-label={`Export ${name}`} className="mt-auto">
<CopyButton
aria-label={`Copy ${name} as SVG`}
content={() => getPetSvgMarkup(petSvg())}
size="sm"
>
Copy
</CopyButton>
<Button
aria-label={`Download ${name} as SVG`}
onClick={() => downloadPetSvg(petSvg(), fileName)}
size="sm"
variant="outline"
>
<DownloadIcon data-icon="inline-start" />
SVG
</Button>
<Button
aria-label={`Download ${name} as PNG`}
onClick={() => void downloadPng()}
size="sm"
variant="outline"
>
<DownloadIcon data-icon="inline-start" />
PNG
</Button>
</ButtonGroup>
</li>
)
}

export default function PetStates() {
return (
<ul className="not-prose grid w-full grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
{STATES.map((item) => (
<PetCard key={item.state} {...item} />
))}
</ul>
)
}
11 changes: 11 additions & 0 deletions apps/web/src/docs/examples/pet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Pet } from '@vitnode/core/components/pet/pet'

export default function PetExample() {
return (
<Pet
className="h-64 w-56"
label="Tabby, the VitNode pet, waving hello"
state="hello"
/>
)
}
2 changes: 1 addition & 1 deletion apps/web/src/docs/page-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const DocsPageHeader = ({
return (
<header className="flex items-center gap-10 border-b pt-2 pb-10">
<div className="flex min-w-0 flex-1 flex-col gap-4">
{(icon || sectionPath.length > 0) && (
{(Boolean(icon) || sectionPath.length > 0) && (
<div className="flex items-center gap-3">
{icon && (
<span
Expand Down
11 changes: 5 additions & 6 deletions apps/web/src/docs/search-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useDocsSearch } from 'fumadocs-core/search/client'
import { useFetchSearch } from 'fumadocs-core/search/client'
import {
SearchDialog,
SearchDialogClose,
Expand All @@ -14,15 +14,14 @@ import {
import { DOCS_SEARCH_PATH } from './shared'

const DocsSearchDialog = (props: SharedProps) => {
const { query, search, setSearch } = useDocsSearch({
const { data, isLoading, onSearchChange, search } = useFetchSearch({
api: DOCS_SEARCH_PATH,
type: 'fetch',
})

return (
<SearchDialog
isLoading={query.isLoading}
onSearchChange={setSearch}
isLoading={isLoading}
onSearchChange={onSearchChange}
search={search}
{...props}
>
Expand All @@ -33,7 +32,7 @@ const DocsSearchDialog = (props: SharedProps) => {
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== 'empty' ? query.data : null} />
<SearchDialogList items={data?.items ?? null} />
</SearchDialogContent>
</SearchDialog>
)
Expand Down
Loading
Loading