import { JSX, JSXElement } from "solid-js"; import { Icon } from "./Icons.tsx"; export const BUTTON_COLOR_NEUTRAL = "neutral"; export const BUTTON_COLOR_WHITE = "white"; export const BUTTON_COLOR_LIGHT_NEUTRAL = "light-neutral"; export const BUTTON_COLOR_BLUE = "blue"; export const BUTTON_COLOR_DARK_BLUE = "dark-blue"; export const BUTTON_COLOR_GREEN = "green"; export const BUTTON_COLOR_DARK_GREEN = "dark-green"; export const BUTTON_COLOR_RED = "red"; export const BUTTON_COLOR_DARK_RED = "dark-red"; export const BUTTON_COLOR_YELLOW = "yellow"; export const BUTTON_COLOR_ORANGE = "orange"; export const BUTTON_COLOR_PRIMARY = "primary"; const BASE = "inline-flex items-center justify-center gap-2 cursor-pointer text-sm font-normal rounded-default transition disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2"; const COLORS: Record = { "neutral": "shadow-xs bg-neutral-700 text-white hover:bg-neutral-800", "white": "shadow-xs bg-white text-black border border-neutral-300 hover:bg-neutral-50", "light-neutral": "shadow-xs bg-neutral-50 text-black border border-neutral-300 hover:bg-neutral-100", "blue": "shadow-xs bg-sky-700 text-white hover:bg-sky-800", "dark-blue": "shadow-xs bg-sky-900 text-white hover:bg-sky-950", "green": "shadow-xs bg-green-700 text-white hover:bg-green-800", "dark-green": "shadow-xs bg-green-900 text-white hover:bg-green-950", "red": "shadow-xs bg-red-700 text-white hover:bg-red-800", "dark-red": "shadow-xs bg-red-900 text-white hover:bg-red-950", "yellow": "shadow-xs bg-yellow-700 text-white hover:bg-yellow-800", "orange": "shadow-xs bg-orange-600 text-white hover:bg-orange-700", "primary": "shadow-xs bg-primary text-white hover:bg-primary-hover", "secondary": "shadow-none bg-neutral-100 text-neutral-700 border border-neutral-300 hover:bg-neutral-200", "ghost": "shadow-none bg-transparent text-neutral-600 border-none hover:bg-neutral-100", }; const OUTLINE_COLORS: Record = { "neutral": "text-neutral-700", "white": "text-neutral-300", "light-neutral": "text-neutral-300", "blue": "text-sky-700", "dark-blue": "text-sky-900", "green": "text-green-700", "dark-green": "text-green-900", "red": "text-red-700", "dark-red": "text-red-900", "yellow": "text-yellow-700", "orange": "text-orange-600", "primary": "text-primary", }; const OUTLINE_BASE = "bg-transparent shadow-[inset_0_0_0_1px_currentColor] hover:shadow-[inset_0_0_0_2px_currentColor]"; interface ButtonUIProps extends JSX.ButtonHTMLAttributes { text?: string; icon?: unknown; outline?: boolean; color?: string; small?: boolean; } export function ButtonUI(props: ButtonUIProps) { const hasText = () => props.text !== undefined ? !!props.text : !props.icon; const cls = () => { let c = BASE; if (props.outline) { c += " " + OUTLINE_BASE + " " + (OUTLINE_COLORS[props.color!] || OUTLINE_COLORS["neutral"]); } else { c += " " + (COLORS[props.color!] || COLORS["neutral"]); } if (props.small) { c += props.icon ? " py-1 px-3" : " py-1 px-4"; } else if (props.icon && !hasText()) { c += " py-2 px-3"; } else if (props.icon) { c += " py-2 px-5"; } else { c += " py-2 px-8"; } return c; }; return ( ); } interface ButtonLinkProps { onclick?: (e: MouseEvent) => void; children?: JSXElement; } export function ButtonLink(props: ButtonLinkProps) { return ( ); } export function ButtonLinkRed(props: ButtonLinkProps) { return ( ); } // SegmentedButtons renders a horizontal group of mutually-exclusive button // options - one is "selected" at any time. Used for "toggle" patterns like // the events sidebar's date/class switcher. Pill styling: a neutral track // holds a raised white chip that marks the selected option; the rest sit // muted. Buttons stretch to fill the track (flex-1). type Reactive2 = T | (() => T); export interface SegmentedButtonOption { value: string; label: string; icon?: string; } interface SegmentedButtonsProps { options: Reactive2; value: Reactive2; onchange: (v: string) => void; small?: boolean; class?: string; } export function SegmentedButtons(props: SegmentedButtonsProps) { const resolveR = (v: Reactive2): T => (typeof v === "function" ? (v as () => T)() : v); // Concentric corner radii: outer = inner + gap, where the gap is the // track's p-0.5 (2px) padding -> 6px = 4px + 2px. Reusing one radius for // both makes the chip corners read as too sharp inside the track. const innerRadius = "rounded-default"; // chips: 4px const outerRadius = "rounded-md"; // track: 4px + 2px = 6px const sizeCls = () => props.small ? "py-0.5 px-2 text-xs" : "py-1 px-3 text-sm"; const baseCls = "inline-flex items-center justify-center gap-1.5 flex-1 cursor-pointer font-medium transition-colors whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"; const activeCls = "bg-white text-text-heading shadow-sm"; const inactiveCls = "text-neutral-500 hover:text-neutral-700"; const buttonCls = (v: string) => { const selected = resolveR(props.value) === v; return baseCls + " " + innerRadius + " " + sizeCls() + " " + (selected ? activeCls : inactiveCls); }; return (
{resolveR(props.options).map((opt) => ( ))}
); } interface BackLinkProps { href: string; text?: string; onDark?: boolean; } // Bare inline-flex anchor — callers control surrounding spacing so // the link can sit cleanly inside a flex row without throwing off // vertical alignment (e.g. inside a page header next to a title). export function BackLink(props: BackLinkProps) { const cls = () => "inline-flex items-center gap-1 text-sm no-underline " + (props.onDark ? "text-text-on-dark-muted hover:text-text-on-dark" : "text-neutral-600 hover:text-neutral-900"); return ( {props.text} ); }