"use client" import * as React from "react" import { cn } from "../../lib/utils" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./dropdown-menu" import { Label } from "./label" import { RadioGroup, RadioGroupItem, type RadioGroupProps } from "./radio-group" import { HorizontalScrollControls, HorizontalScrollViewport, useHorizontalScrollAffordances, } from "./horizontal-scroll-region" export interface SelectionTileOption { value: T label: string /** Font Awesome icon class without prefix (e.g. `fa-table`); rendered with `fa-light`. Ignored when `leading` is set. */ icon?: string /** Custom graphic (SVG, swatch, etc.) instead of `icon`. */ leading?: React.ReactNode /** Optional helper copy — shown by {@link SelectionTileShowcase} (docs / marketing). */ description?: string } /** Selected preview well — brand wash; dark uses elevated tint (not pale light-mode wash). */ const TILE_SELECTED_PREVIEW = "border-brand bg-brand-tint shadow-none dark:border-brand-color-light dark:bg-brand-tint-light dark:shadow-sm" /** Icon inside selected preview well — always foreground on tinted well (brand-deep fails on dark tint). */ const TILE_SELECTED_ICON_ON_WELL = "text-foreground" /** Caption below preview (page canvas) — never mid-tone brand on dark bg. */ const TILE_SELECTED_CAPTION = "font-medium text-brand-deep dark:text-foreground" /** Shared surface classes for icon+label tiles (Properties view type, Export format, etc.). * * `aspect-square` + horizontal padding keeps every tile a uniform square regardless of how * many tiles are in the row — without it, the last row of a `grid-cols-N` grid (e.g. 3 tiles * in a 4-col grid for the Properties view selector) renders the same width per cell but the * eye reads them as inconsistent when the row's empty trailing cell shifts visual rhythm. * Two-line labels (e.g. "List & details", "Tree & details") still fit because the icon * column is only ~18px and the square grows with the cell. */ export function selectionTileClassNames(selected: boolean) { return cn( "flex aspect-square flex-col items-center justify-center gap-1.5 rounded-lg border px-2 py-3 text-xs leading-tight transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", selected ? cn(TILE_SELECTED_PREVIEW, TILE_SELECTED_CAPTION, "shadow-sm") : "border-border bg-background text-muted-foreground hover:border-border/80 hover:text-interactive-hover-foreground", ) } /** Icon well size for compact + label-below (Properties view type, Export format). */ export const SELECTION_TILE_COMPACT_ICON_SIZE = "2.5rem" /** Fixed preview for caption-below + default density (Settings theme/contrast/text size). */ export const SELECTION_TILE_BELOW_DEFAULT_PREVIEW_SIZE = "6rem" const COMPACT_BELOW_COLUMN_CLASS: Record<2 | 3 | 4 | 5 | 6 | 7, string> = { 2: "grid-cols-2", 3: "grid-cols-3", 4: "grid-cols-4", 5: "grid-cols-5", 6: "grid-cols-6", 7: "grid-cols-7", } /** Preview box only — label rendered below the graphic (Settings, Properties). */ export function selectionTilePreviewClassNames( selected: boolean, density: "default" | "compact" = "default", labelPlacement: "inside" | "below" = "inside", ) { if (density === "compact" && labelPlacement === "below") { return cn( "relative box-border flex shrink-0 items-center justify-center rounded-lg border transition-colors", "size-10", selected ? TILE_SELECTED_PREVIEW : "border-transparent bg-transparent", ) } if (labelPlacement === "below") { return cn( "relative box-border flex size-24 shrink-0 items-center justify-center overflow-hidden rounded-lg border p-1.5 transition-colors", selected ? TILE_SELECTED_PREVIEW : "border-border bg-background hover:border-border/80", ) } return cn( "relative box-border flex flex-col rounded-lg border transition-colors", "aspect-square w-full max-w-full p-1", selected ? TILE_SELECTED_PREVIEW : "border-border bg-background hover:border-border/80", ) } function SelectionTileGraphic({ option, selected, density = "default", }: { option: SelectionTileOption selected: boolean density?: "default" | "compact" }) { if (option.leading != null) { return ( {option.leading} ) } if (option.icon) { return (