This commit is contained in:
Tomas Dvorak committed 2026-05-05 09:48:07 +02:00
1 parent d854614a87
commit 48c3e15a38
295 files changed
+178378 -1036

No files matched your search

@@ -0,0 +1,240 @@
import { JSX, createSignal, onMount, Show } from "solid-js";
export type CharacterPose =
| "main"
| "hello"
| "laptop"
| "sleep"
| "lookup"
| "success"
| "like"
| "walk"
| "announcement"
| "educate"
| "happy_note"
| "maintenance"
| "sad"
// New mascot poses
| "time"
| "flag"
| "error"
| "headphones"
| "checkbox"
| "coffee"
| "coffe"
| "angry_bulb"
| "anoyed_lamp"
// Grayscaled error states
| "404"
| "forbidden"
| "connection_error"
| "fixing";
interface BookraCharacterProps {
pose: CharacterPose;
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
animate?: boolean;
class?: string;
alt?: string;
}
const sizeMap = {
xs: "w-16 h-16",
sm: "w-24 h-24",
md: "w-32 h-32",
lg: "w-48 h-48",
xl: "w-64 h-64",
"2xl": "w-80 h-80",
};
const illustrationMap: Record<CharacterPose, string> = {
main: "/bookra-illustrations/main.svg",
hello: "/bookra-illustrations/hello.svg",
laptop: "/bookra-illustrations/laptop.svg",
sleep: "/bookra-illustrations/sleep.svg",
lookup: "/bookra-illustrations/lookup.svg",
success: "/bookra-illustrations/success.svg",
like: "/bookra-illustrations/like.svg",
walk: "/bookra-illustrations/walk.svg",
announcement: "/bookra-illustrations/announcement.svg",
educate: "/bookra-illustrations/educate.svg",
happy_note: "/bookra-illustrations/happy-note.svg",
maintenance: "/bookra-illustrations/maintenance.svg",
sad: "/bookra-illustrations/sad.svg",
time: "/bookra-illustrations/clock.svg",
flag: "/bookra-illustrations/flag.svg",
error: "/bookra-illustrations/sad.svg",
headphones: "/bookra-illustrations/headphones.svg",
checkbox: "/bookra-illustrations/happy-note.svg",
coffee: "/bookra-illustrations/coffee.svg",
coffe: "/bookra-illustrations/coffee.svg",
angry_bulb: "/bookra-illustrations/angry-bulb.svg",
anoyed_lamp: "/bookra-illustrations/angry-bulb.svg",
"404": "/bookra-illustrations/404.svg",
forbidden: "/bookra-illustrations/forbidden.svg",
connection_error: "/bookra-illustrations/connection-error.svg",
fixing: "/bookra-illustrations/maintenance.svg",
};
export function BookraCharacter(props: BookraCharacterProps): JSX.Element {
const [isLoaded, setIsLoaded] = createSignal(false);
const [hasError, setHasError] = createSignal(false);
const sizeClass = () => sizeMap[props.size || "md"];
const animationClass = () => props.animate !== false ? "animate-float" : "";
const characterPath = () => illustrationMap[props.pose];
const label = () => props.alt || `Bookra illustration - ${props.pose.replaceAll("_", " ")}`;
return (
<div
class={`relative inline-flex items-center justify-center ${sizeClass()} ${props.class || ""}`}
aria-label={label()}
>
<Show when={!hasError()}>
<img
src={characterPath()}
alt={label()}
class={`w-full h-full object-contain transition-all duration-500 ${
isLoaded() ? "opacity-100 scale-100" : "opacity-0 scale-95"
} ${animationClass()}`}
onLoad={() => setIsLoaded(true)}
onError={() => setHasError(true)}
/>
</Show>
<Show when={hasError()}>
<div class="w-full h-full rounded-2xl bg-accent/10 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent">
<rect x="3" y="4" width="18" height="18" rx="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
</div>
</Show>
</div>
);
}
// Character with speech bubble for contextual messages
interface CharacterMessageProps extends BookraCharacterProps {
message: string;
position?: "left" | "right" | "top" | "bottom";
}
export function CharacterWithMessage(props: CharacterMessageProps): JSX.Element {
const positionClass = () => {
switch (props.position || "right") {
case "left": return "flex-row-reverse";
case "right": return "flex-row";
case "top": return "flex-col-reverse";
case "bottom": return "flex-col";
}
};
const bubblePosition = () => {
switch (props.position || "right") {
case "left": return "mr-4";
case "right": return "ml-4";
case "top": return "mb-3";
case "bottom": return "mt-3";
}
};
return (
<div class={`flex items-center ${positionClass()}`}>
<BookraCharacter
pose={props.pose}
size={props.size}
animate={props.animate}
class={props.class}
alt={props.alt}
/>
<div class={`surface-elevated px-4 py-3 rounded-2xl rounded-${
props.position === "left" ? "r" :
props.position === "right" ? "l" :
props.position === "top" ? "b" : "t"
}-lg max-w-xs ${bubblePosition()}`}>
<p class="text-ink text-sm leading-relaxed">{props.message}</p>
</div>
</div>
);
}
// Empty state with character
interface CharacterEmptyStateProps {
pose: CharacterPose;
title: string;
message: string;
action?: {
label: string;
onClick: () => void;
};
size?: "sm" | "md" | "lg";
}
export function CharacterEmptyState(props: CharacterEmptyStateProps): JSX.Element {
return (
<div class="flex flex-col items-center text-center py-12 px-4">
<BookraCharacter
pose={props.pose}
size={props.size || "lg"}
animate={true}
class="mb-6"
/>
<h3 class="font-display text-xl font-semibold text-ink mb-2">
{props.title}
</h3>
<p class="text-ink-muted max-w-sm mb-6 leading-relaxed">
{props.message}
</p>
{props.action && (
<button
onClick={props.action.onClick}
class="btn-primary"
>
{props.action.label}
</button>
)}
</div>
);
}
// Success state with character celebration
interface CharacterSuccessStateProps {
title: string;
message: string;
action?: {
label: string;
onClick: () => void;
};
}
export function CharacterSuccessState(props: CharacterSuccessStateProps): JSX.Element {
return (
<div class="flex flex-col items-center text-center py-12 px-4 animate-fade-in">
<div class="relative mb-6">
<BookraCharacter
pose="success"
size="xl"
animate={true}
/>
<div class="absolute -top-2 -right-2 text-3xl animate-bounce">✨</div>
</div>
<h3 class="font-display text-2xl font-semibold text-ink mb-3">
{props.title}
</h3>
<p class="text-ink-muted max-w-md mb-8 leading-relaxed">
{props.message}
</p>
{props.action && (
<button
onClick={props.action.onClick}
class="btn-primary inline-flex items-center gap-2"
>
{props.action.label}
</button>
)}
</div>
);
}