import { JSX, JSXElement, splitProps } 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 = { // The one button that INVERTS with the theme: dark on a light page, light on a // dark one. It cannot be `bg-ink` with `text-white`, because the moment the fill // goes pale in dark mode the white label vanishes — the fill and its text have to // move together, which is what the three fill-neutral tokens are for. "neutral": "shadow-xs bg-fill-neutral text-on-fill-neutral hover:bg-fill-neutral-hover", "white": "shadow-xs bg-surface text-ink border border-line-strong hover:bg-surface-muted", "light-neutral": "shadow-xs bg-surface-muted text-ink border border-line-strong hover:bg-surface-raised", "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-surface-raised text-ink border border-line-strong hover:bg-surface-strong", "ghost": "shadow-none bg-transparent text-ink-soft border-none hover:bg-surface-raised", }; const OUTLINE_COLORS: Record = { "neutral": "text-ink", "white": "text-ink-faint", "light-neutral": "text-ink-faint", "blue": "text-sky-700 dark:text-sky-400", "dark-blue": "text-sky-900", "green": "text-green-700 dark:text-green-400", "dark-green": "text-green-900", "red": "text-red-700 dark:text-red-400", "dark-red": "text-red-900", "yellow": "text-yellow-700 dark:text-yellow-400", "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 [local, buttonProps] = splitProps(props, ["text", "icon", "outline", "color", "small", "class", "type", "children"]); const hasText = () => local.text !== undefined ? !!local.text : !local.icon; const cls = () => { let c = BASE; if (local.outline) { c += " " + OUTLINE_BASE + " " + (OUTLINE_COLORS[local.color!] || OUTLINE_COLORS["neutral"]); } else { c += " " + (COLORS[local.color!] || COLORS["neutral"]); } if (local.small) { c += local.icon ? " py-1 px-3" : " py-1 px-4"; } else if (local.icon && !hasText()) { c += " py-2 px-3"; } else if (local.icon) { c += " py-2 px-5"; } else { c += " py-2 px-8"; } if (local.class) c += " " + local.class; 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-ss" : "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-surface text-ink shadow-sm"; const inactiveCls = "text-ink-muted hover:text-ink"; 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-ink-soft hover:text-ink"); return ( {props.text} ); }