initial commit after project creation

This commit is contained in:
Gerhard Scheikl
2026-04-01 09:38:50 +02:00
commit b02af637d4
292 changed files with 61408 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
"use client";
import { XMarkIcon } from "@heroicons/react/24/outline";
import React from "react";
import { useEffect } from "react";
import { useEditState } from "tinacms/dist/react";
const AdminLink = () => {
const { edit } = useEditState();
const [showAdminLink, setShowAdminLink] = React.useState(false);
useEffect(() => {
setShowAdminLink(
!edit &&
JSON.parse((window.localStorage.getItem("tinacms-auth") as any) || "{}")
?.access_token
);
}, [edit]);
const handleDismiss = () => {
setShowAdminLink(false);
};
return (
<>
{showAdminLink && (
<div className="fixed right-4 top-4 z-50 flex items-center justify-between rounded-full bg-blue-500 px-3 py-1 text-white">
<a
href={`/admin/index.html#/~${window.location.pathname}`}
className="text-xs"
>
Edit This Page
</a>
<button
type="button"
onClick={handleDismiss}
className="ml-2 text-sm"
>
<XMarkIcon className="size-4" />
</button>
</div>
)}
</>
);
};
export default AdminLink;

View File

@@ -0,0 +1,130 @@
import Link from "next/link";
import type React from "react";
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
color?: "white" | "blue" | "orange" | "seafoam" | "ghost" | "ghostBlue";
size?: "large" | "small" | "medium" | "extraSmall";
className?: string;
href?: string;
type?: "button" | "submit" | "reset";
children: React.ReactNode | React.ReactNode[];
disabled?: boolean;
}
const baseClasses =
"transition duration-150 ease-out rounded-full flex items-center font-tuner whitespace-nowrap leading-snug focus:outline-none focus:shadow-outline hover:-translate-y-px active:translate-y-px hover:-translate-x-px active:translate-x-px leading-tight";
const raisedButtonClasses = "hover:shadow active:shadow-none";
const colorClasses = {
seafoam: `${raisedButtonClasses} text-orange-600 hover:text-orange-500 border border-seafoam-150 bg-gradient-to-br from-seafoam-50 to-seafoam-150`,
blue: `${raisedButtonClasses} text-white hover:text-gray-50 border border-blue-400 bg-gradient-to-br from-blue-300 via-blue-400 to-blue-600`,
orange: `${raisedButtonClasses} text-white hover:text-gray-50 border border-orange-600 bg-gradient-to-br from-orange-400 to-orange-600`,
white: `${raisedButtonClasses} text-orange-500 hover:text-orange-400 border border-gray-100/60 bg-gradient-to-br from-white to-gray-50`,
ghost: "text-orange-500 hover:text-orange-400",
orangeWithBorder:
"text-orange-500 hover:text-orange-400 border border-orange-500 bg-white",
ghostBlue: "text-blue-800 hover:text-blue-800",
};
const sizeClasses = {
large: "px-8 pt-[14px] pb-[12px] text-lg font-medium",
medium: "px-6 pt-[12px] pb-[10px] text-base font-medium",
small: "px-5 pt-[10px] pb-[8px] text-sm font-medium",
extraSmall: "px-4 pt-[8px] pb-[6px] text-xs font-medium",
};
export const Button = ({
color = "seafoam",
size = "medium",
className = "",
children,
...props
}: ButtonProps) => {
return (
<button
className={`${baseClasses} ${
colorClasses[color] ? colorClasses[color] : colorClasses.seafoam
} ${
sizeClasses[size] ? sizeClasses[size] : sizeClasses.medium
} ${className}`}
{...props}
>
{children}
</button>
);
};
export const LinkButton = ({
link = "/",
color = "seafoam",
size = "medium",
className = "",
children,
...props
}) => {
return (
<Link
href={link}
passHref
className={`${baseClasses} ${
colorClasses[color] ? colorClasses[color] : colorClasses.seafoam
} ${
sizeClasses[size] ? sizeClasses[size] : sizeClasses.medium
} ${className}`}
{...props}
>
{children}
</Link>
);
};
export const FlushButton = ({
link = "/",
color = "seafoam",
className = "",
children,
...props
}) => {
return (
<Link
href={link}
passHref
className={`${baseClasses} ${
colorClasses[color] ? colorClasses[color] : colorClasses.seafoam
} ${"hover:inner-link border-none bg-none p-2 hover:translate-x-0 hover:translate-y-0 hover:shadow-none"} ${className}`}
{...props}
>
{children}
</Link>
);
};
export const ModalButton = ({
color = "seafoam",
size = "medium",
className = "",
children,
...props
}) => {
return (
<button
className={`${baseClasses} ${
colorClasses[color] ? colorClasses[color] : colorClasses.seafoam
} ${
sizeClasses[size] ? sizeClasses[size] : sizeClasses.medium
} ${className}`}
{...props}
>
{children}
</button>
);
};
export const ButtonGroup = ({ children }) => {
return (
<div className="flex w-full flex-wrap items-center justify-start gap-4">
{children}
</div>
);
};

View File

@@ -0,0 +1,60 @@
import React from "react";
export const CustomColorToggle = ({ input }) => {
const { value = {}, onChange } = input;
const disableColor = value.disableColor || false;
const colorValue = value.colorValue || "#000000";
const handleCheckboxChange = (e) => {
onChange({ ...value, disableColor: e.target.checked });
};
const handleColorChange = (e) => {
onChange({ ...value, colorValue: e.target.value });
};
return (
<>
<label className="mb-2 block text-xs font-semibold text-gray-700">
Custom Background Selector
</label>
<div className="flex items-center pt-2">
<label className="flex cursor-pointer items-center">
<div className="relative shadow-lg">
<input
type="checkbox"
checked={disableColor}
onChange={handleCheckboxChange}
className="sr-only"
/>
<div
className={`h-5 w-10 rounded-full shadow-inner transition-colors duration-200 ${
disableColor ? "bg-green-500" : "bg-gray-300"
}`}
/>
<div
className={`absolute left-0 top-0 size-5 rounded-full bg-white shadow transition-transform duration-200 ${
disableColor ? "translate-x-full" : ""
}`}
/>
</div>
<span className="ml-3 text-gray-700">
Tick to use Default Background Color
</span>
</label>
{/* Color Picker */}
<div style={{ marginLeft: "1rem", opacity: disableColor ? 0.5 : 1 }}>
<input
type="color"
value={colorValue}
onChange={handleColorChange}
disabled={disableColor}
className="size-10 rounded border border-gray-300"
/>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,91 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@radix-ui/react-dropdown-menu";
import React, { useState } from "react";
import type { ReactNode } from "react";
import { MdArrowDropDown } from "react-icons/md";
export interface DropdownOption {
value: string;
label: ReactNode;
}
interface CustomDropdownProps {
/** The currently selected value */
value: string;
/** Function fired when a new option is selected */
onChange: (value: string) => void;
/** List of options to choose from */
options: DropdownOption[];
/** Placeholder text shown when no option is selected */
placeholder?: string;
/** Whether the dropdown is disabled */
disabled?: boolean;
/** Additional classes for the trigger button */
className?: string;
/** Additional classes for the dropdown content */
contentClassName?: string;
/** Additional classes for each menu item */
itemClassName?: string;
}
/**
* A reusable dropdown component built with Radix UI.
*
* It matches the full width of its trigger and automatically rotates the chevron icon when open.
*/
export const CustomDropdown = ({
value,
onChange,
options,
placeholder = "Select an option",
disabled = false,
className = "",
contentClassName = "",
itemClassName = "",
}: CustomDropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
// Find the label for the current value.
const activeOption = options.find((opt) => opt.value === value);
return (
<DropdownMenu onOpenChange={setIsOpen} open={disabled ? false : isOpen}>
<DropdownMenuTrigger asChild>
<button
type="button"
disabled={disabled}
className={`w-full p-2 border border-gray-300 rounded-md shadow-sm text-neutral hover:bg-neutral-background-secondary focus:outline-none flex items-center justify-between gap-2 max-w-full overflow-x-hidden ${
disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-gray-50"
} ${className}`}
>
<span className="truncate break-words whitespace-normal max-w-full text-left">
{activeOption ? activeOption.label : placeholder}
</span>
<MdArrowDropDown
className={`w-5 h-5 transition-transform duration-200 ${
isOpen ? "rotate-180" : "rotate-0"
} ${disabled ? "opacity-50" : ""}`}
/>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
className={`z-50 max-h-60 overflow-y-auto w-[var(--radix-dropdown-menu-trigger-width)] min-w-[200px] bg-white border border-gray-200 rounded-md shadow-lg ${contentClassName}`}
>
{options.map((opt) => (
<DropdownMenuItem
key={opt.value}
onClick={() => onChange(opt.value)}
className={`px-3 py-2 cursor-pointer truncate break-words whitespace-normal max-w-full w-full focus:outline-none focus:ring-0 hover:bg-gray-100 ${itemClassName}`}
>
{opt.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,27 @@
import Link, { type LinkProps } from "next/link";
import type React from "react";
type ExtraProps = Omit<LinkProps, "as" | "href">;
interface DynamicLinkProps extends ExtraProps {
href: string;
children?: React.ReactNode;
isFullWidth?: boolean;
}
export const DynamicLink = ({
href,
children,
isFullWidth = false,
...props
}: DynamicLinkProps) => {
return (
<Link
href={href}
{...props}
className={`cursor-pointer ${isFullWidth ? "" : ""}`}
>
{children}
</Link>
);
};

View File

@@ -0,0 +1,166 @@
"use client";
import Image, { type ImageLoader } from "next/image";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { MdClose } from "react-icons/md";
interface ImageOverlayWrapperProps {
children: React.ReactNode;
src: string;
alt: string;
caption?: string;
}
// Custom image loader to bypass Next.js image optimization
const customImageLoader: ImageLoader = ({ src, width, quality }) => {
// If it's already an absolute URL (starts with http:// or https://), return as-is
if (src.startsWith("http://") || src.startsWith("https://")) {
return src;
}
// For relative paths, prepend the base path if it exists
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
const fullSrc = `${basePath}${src}`;
// If the src already includes query parameters, append with &, otherwise use ?
const separator = fullSrc.includes("?") ? "&" : "?";
return `${fullSrc}${separator}w=${width}&q=${quality || 75}`;
};
export const ImageOverlayWrapper = ({
children,
src,
alt,
caption,
}: ImageOverlayWrapperProps) => {
const [isOpen, setIsOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (isOpen) {
// Disable scrolling when overlay is open
document.body.style.overflow = "hidden";
// Focus the overlay for keyboard interaction
if (overlayRef.current) {
overlayRef.current.focus();
}
return () => {
document.body.style.overflow = "unset";
};
}
}, [isOpen]);
const openOverlay = () => {
setIsOpen(true);
setIsLoading(true); // Reset loading state when opening overlay
};
const closeOverlay = () => setIsOpen(false);
const handleImageLoad = () => {
setIsLoading(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
closeOverlay();
}
};
const overlay =
isOpen && mounted
? createPortal(
<div
ref={overlayRef}
tabIndex={-1}
className="fixed inset-0 z-50 flex items-center justify-center backdrop-blur-lg outline-none"
onClick={closeOverlay}
onKeyDown={handleKeyDown}
>
{/* Close button */}
<button
type="button"
onClick={closeOverlay}
className="absolute top-4 right-4 z-10 flex items-center justify-center w-10 h-10 border border-brand-primary hover:border-brand-primary-hover bg-neutral-background-secondary hover:bg-neutral-background-secondary/80 rounded-full transition-colors duration-200 group"
aria-label="Close image overlay"
>
<MdClose className="w-6 h-6 text-neutral-text group-hover:text-neutral-text-secondary" />
</button>
{/* Image container */}
<div className="relative max-w-[90vw] max-h-[90vh] flex items-center justify-center p-8">
<div className="relative flex flex-col items-center justify-center">
<div
className="relative w-[80vw] h-[80vh] overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Loading skeleton */}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-neutral-background-secondary/50 rounded-lg">
<div className="flex flex-col items-center gap-3">
<div className="w-12 h-12 border-4 border-brand-primary border-t-transparent rounded-full animate-spin" />
<p className="text-neutral-text-secondary text-sm">
Loading image...
</p>
</div>
</div>
)}
<Image
loader={customImageLoader}
src={src}
alt={alt}
fill
style={{ objectFit: "contain", objectPosition: "center" }}
onLoad={handleImageLoad}
/>
</div>
{/* Caption */}
{caption && (
<div
className="mt-4 px-4 py-2 rounded-lg bg-neutral-background"
onClick={(e) => e.stopPropagation()}
>
<p className="text-neutral-text text-sm text-center font-light">
{caption}
</p>
</div>
)}
</div>
</div>
{/* Click anywhere to close hint */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2">
<p className="text-neutral-text-secondary text-sm">
Click anywhere to close
</p>
</div>
</div>,
document.body
)
: null;
return (
<>
<button
type="button"
onClick={openOverlay}
className="cursor-pointer transition-opacity duration-200 hover:opacity-80 active:opacity-90 border-none bg-transparent p-0 md:block w-full flex justify-center"
aria-label={`Open image overlay: ${alt}`}
>
{children}
</button>
{overlay}
</>
);
};

View File

@@ -0,0 +1,37 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { IoMoon, IoSunny } from "react-icons/io5";
export default function LightDarkSwitch() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const isLight = resolvedTheme === "light";
return (
<button
type="button"
className="flex items-center justify-center w-10 h-10 rounded-full transition-all duration-300 ease-in-out cursor-pointer"
onClick={() => setTheme(isLight ? "dark" : "light")}
>
{mounted ? (
isLight ? (
<IoSunny
size={20}
className="text-brand-primary transition-colors duration-300"
/>
) : (
<IoMoon
size={19}
className="text-neutral-text transition-colors duration-300"
/>
)
) : (
<div className="w-5 h-5 rounded-full animate-pulse opacity-20" />
)}
</button>
);
}

View File

@@ -0,0 +1,127 @@
import { usePathname } from "next/navigation";
import React from "react";
import { MdChevronLeft, MdChevronRight } from "react-icons/md";
import { useNavigation } from "../docs/layout/navigation-context";
import { DynamicLink } from "./dynamic-link";
export function Pagination() {
const [prevPage, setPrevPage] = React.useState<any>(null);
const [nextPage, setNextPage] = React.useState<any>(null);
const pathname = usePathname();
const docsData = useNavigation();
React.useEffect(() => {
if (!docsData?.data) return;
// Flatten the hierarchical structure into a linear array
const flattenItems = (items: any[]): any[] => {
const flattened: any[] = [];
const traverse = (itemList: any[]) => {
for (const item of itemList) {
if (item.slug) {
flattened.push({
slug: item.slug.id,
title: item.slug.title,
});
}
if (item.items) {
// This has nested items, traverse them
traverse(item.items);
}
}
};
traverse(items);
return flattened;
};
const getAllPages = (): any[] => {
const allPages: any[] = [];
for (const tab of docsData.data) {
if (tab.items) {
const flattenedItems = flattenItems(tab.items);
allPages.push(...flattenedItems);
}
}
return allPages;
};
// Get current slug from pathname
const slug =
pathname === "/docs"
? "content/docs/index.mdx"
: `content${pathname}.mdx`;
// Get all pages in sequence
const allPages = getAllPages();
// Find current page index
const currentIndex = allPages.findIndex((page: any) => page.slug === slug);
if (currentIndex !== -1) {
// Set previous page (if exists)
const prev = currentIndex > 0 ? allPages[currentIndex - 1] : null;
setPrevPage(prev);
// Set next page (if exists)
const next =
currentIndex < allPages.length - 1 ? allPages[currentIndex + 1] : null;
setNextPage(next);
} else {
setPrevPage(null);
setNextPage(null);
}
}, [docsData, pathname]);
return (
<div className="flex justify-between mt-2 py-4 rounded-lg gap-4 w-full">
{prevPage?.slug ? (
//Slices to remove content/ and .mdx from the filepath, and removes /index for index pages
<DynamicLink
href={prevPage.slug.slice(7, -4).replace(/\/index$/, "/")}
passHref
>
<div className="group relative block cursor-pointer py-4 text-left transition-all">
<span className="pl-10 text-sm uppercase opacity-50 group-hover:opacity-100 text-neutral-text-secondary">
Previous
</span>
<h5 className="pl m-0 flex items-center font-light leading-[1.3] text-brand-secondary opacity-80 group-hover:opacity-100 transition-all duration-150 ease-out group-hover:text-brand-primary md:text-xl">
<MdChevronLeft className="ml-2 size-7 fill-gray-400 transition-all duration-150 ease-out group-hover:fill-brand-primary" />
<span className="relative brand-secondary-gradient">
{prevPage.title}
<span className="absolute bottom-0 left-0 w-0 h-[1.5px] bg-gradient-to-r from-brand-secondary-gradient-start to-brand-secondary-gradient-end group-hover:w-full transition-all duration-300 ease-in-out" />
</span>
</h5>
</div>
</DynamicLink>
) : (
<div />
)}
{nextPage?.slug ? (
//Slices to remove content/ and .mdx from the filepath, and removes /index for index pages
<DynamicLink
href={nextPage.slug.slice(7, -4).replace(/\/index$/, "/")}
passHref
>
<div className="group relative col-start-2 block cursor-pointer p-4 text-right transition-all">
<span className="pr-6 text-sm uppercase opacity-50 md:pr-10 group-hover:opacity-100 text-neutral-text-secondary">
Next
</span>
<h5 className="m-0 flex items-center justify-end font-light leading-[1.3] text-brand-secondary opacity-80 group-hover:opacity-100 transition-all duration-150 ease-out group-hover:text-brand-primary md:text-xl">
<span className="relative brand-secondary-gradient">
{nextPage.title}
<span className="absolute bottom-0 left-0 w-0 h-[1.5px] bg-gradient-to-r from-brand-secondary-gradient-start to-brand-secondary-gradient-end group-hover:w-full transition-all duration-300 ease-in-out" />
</span>
<MdChevronRight className="ml-2 size-7 fill-gray-400 transition-all duration-150 ease-out group-hover:fill-brand-primary" />
</h5>
</div>
</DynamicLink>
) : (
<div />
)}
</div>
);
}

View File

@@ -0,0 +1,14 @@
export function TailwindIndicator() {
if (process.env.NODE_ENV === "production") return null;
return (
<div className="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
<div className="block sm:hidden">xs</div>
<div className="hidden sm:block md:hidden">sm</div>
<div className="hidden md:block lg:hidden">md</div>
<div className="hidden lg:block xl:hidden">lg</div>
<div className="hidden xl:block 2xl:hidden">xl</div>
<div className="hidden 2xl:block">2xl</div>
</div>
);
}

View File

@@ -0,0 +1,180 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useRef, useState } from "react";
import { MdArrowDropDown } from "react-icons/md";
import { MdHelpOutline } from "react-icons/md";
const themes = ["default", "tina", "blossom", "lake", "pine", "indigo"];
export const BROWSER_TAB_THEME_KEY = "browser-tab-theme";
// Default theme colors from root
const DEFAULT_COLORS = {
background: "#FFFFFF",
text: "#000000",
border: "#000000",
};
export const ThemeSelector = () => {
const { theme, setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const [selectedTheme, setSelectedTheme] = useState(() => {
if (typeof window !== "undefined") {
return sessionStorage.getItem(BROWSER_TAB_THEME_KEY) || theme;
}
return theme;
});
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
if (
tooltipRef.current &&
!tooltipRef.current.contains(event.target as Node)
) {
setShowTooltip(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Avoid hydration mismatch
useEffect(() => {
setMounted(true);
}, []);
// Update selected theme when theme changes from dropdown
useEffect(() => {
if (theme && !themes.includes(theme)) {
// If theme is not in our list, it means it's a dark/light mode change
setSelectedTheme(selectedTheme);
} else {
setSelectedTheme(theme);
}
}, [theme, selectedTheme]);
useEffect(() => {
if (mounted && selectedTheme) {
const isDark = resolvedTheme === "dark";
document.documentElement.className = `theme-${selectedTheme}${
isDark ? " dark" : ""
}`;
sessionStorage.setItem(BROWSER_TAB_THEME_KEY, selectedTheme);
}
}, [selectedTheme, resolvedTheme, mounted]);
if (!mounted) return null;
const handleThemeChange = (newTheme: string) => {
const currentMode = resolvedTheme;
setSelectedTheme(newTheme);
sessionStorage.setItem(BROWSER_TAB_THEME_KEY, newTheme);
setIsOpen(false);
if (currentMode === "dark") {
setTheme("light");
setTimeout(() => setTheme("dark"), 0);
} else {
setTheme("light");
}
};
return (
<div className="fixed bottom-4 right-4 z-50 bg-neutral-surface p-1 rounded-lg shadow-lg">
<div className="relative" ref={dropdownRef}>
<div className="flex items-center gap-2">
<div className="relative" ref={tooltipRef}>
<button
type="button"
onClick={() => setShowTooltip(!showTooltip)}
className="w-6 h-6 rounded-full bg-neutral-hover hover:bg-neutral-border flex items-center justify-center text-neutral-text transition-colors"
aria-label="Theme help"
>
<MdHelpOutline className="w-4 h-4" />
</button>
{showTooltip && <Tooltip selectedTheme={selectedTheme} />}
</div>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-[120px] rounded-md border border-neutral-border bg-neutral-surface px-3 py-1 text-sm text-neutral-text focus:outline-none focus:ring-2 focus:ring-brand-primary flex items-center justify-between cursor-pointer"
>
<span className="truncate">
{selectedTheme.charAt(0).toUpperCase() + selectedTheme.slice(1)}
</span>
<MdArrowDropDown
className={`w-4 h-4 text-brand-secondary-dark-dark transition-transform duration-200 flex-shrink-0 ${
isOpen ? "rotate-180" : ""
}`}
/>
</button>
</div>
{isOpen && (
<div className="absolute bottom-full left-0 right-0 mb-1 bg-neutral-surface rounded-md border border-neutral-border shadow-lg overflow-hidden w-[120px]">
{themes.map((t) => (
<button
type="button"
key={t}
onClick={() => handleThemeChange(t)}
className={`w-full px-3 py-1 text-sm text-left hover:bg-neutral-hover transition-colors cursor-pointer first:rounded-t-md last:rounded-b-md my-0.25 first:mt-0 last:mb-0 ${
t === "default" ? "" : `theme-${t}`
} ${t === selectedTheme ? "bg-neutral-hover" : ""}`}
style={{
backgroundColor:
t === "default"
? DEFAULT_COLORS.background
: "var(--brand-primary-light)",
color:
t === "default"
? DEFAULT_COLORS.text
: "var(--brand-primary)",
border:
t === "default"
? `1px solid ${DEFAULT_COLORS.border}`
: "1px solid var(--brand-primary)",
}}
>
{t.charAt(0).toUpperCase() + t.slice(1)}
</button>
))}
</div>
)}
</div>
</div>
);
};
const Tooltip = ({ selectedTheme }: { selectedTheme: string }) => {
return (
<div className="absolute bottom-full right-0 mb-2 w-64 p-3 bg-neutral-surface border border-neutral-border rounded-lg shadow-lg text-xs text-neutral-text min-w-fit">
<div className="font-medium mb-2">Theme Preview</div>
<p className="mb-2">
Theme changes are temporary and will reset when you open a new browser
window or tab.
</p>
<p className="mb-2">
To make theme changes permanent, update the{" "}
<code className="bg-neutral-hover px-1 rounded">Selected Theme</code>{" "}
field in your Settings through TinaCMS:
</p>
<code className="block bg-neutral-hover p-2 rounded text-xs font-mono">
selectedTheme={selectedTheme}
</code>
<div className="absolute top-full right-2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-neutral-border" />
</div>
);
};