Converge choice and popover surfaces
This commit is contained in:
parent
44989700a6
commit
d2d06e15b6
6 changed files with 130 additions and 124 deletions
|
|
@ -4,6 +4,7 @@ import { useRef, useState, useEffect, useCallback } from 'react';
|
||||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react';
|
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react';
|
||||||
import { isLightColor, type CustomThemeColors } from '@/contexts/ThemeContext';
|
import { isLightColor, type CustomThemeColors } from '@/contexts/ThemeContext';
|
||||||
import { PaletteIcon } from '@/components/icons/Icons';
|
import { PaletteIcon } from '@/components/icons/Icons';
|
||||||
|
import { IconButton, Input, cn, popoverPanelClass } from '@/components/ui';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Curated swatch palettes per color role, sourced from existing themes
|
* Curated swatch palettes per color role, sourced from existing themes
|
||||||
|
|
@ -78,7 +79,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative flex items-center">
|
<Popover className="relative flex items-center">
|
||||||
<PopoverButton className="cursor-pointer group focus:outline-none">
|
<PopoverButton as={IconButton} size="sm" className="group rounded-full p-0" aria-label={`Pick ${label} color`}>
|
||||||
<div
|
<div
|
||||||
className="w-6 h-6 rounded-full border-2 transition duration-fast group-focus-visible:ring-2 group-focus-visible:ring-offset-1"
|
className="w-6 h-6 rounded-full border-2 transition duration-fast group-focus-visible:ring-2 group-focus-visible:ring-offset-1"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -91,8 +92,10 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
||||||
<PopoverPanel
|
<PopoverPanel
|
||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
transition
|
transition
|
||||||
className="z-[60] mt-2 w-56 rounded-lg shadow-elev-3 border border-line bg-background p-3 space-y-3
|
className={cn(
|
||||||
transition duration-fast ease-standard data-[closed]:opacity-0 data-[closed]:scale-95"
|
popoverPanelClass,
|
||||||
|
'z-[60] mt-2 w-56 bg-background space-y-3 transition duration-fast ease-standard data-[closed]:opacity-0 data-[closed]:scale-95',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{/* Label */}
|
{/* Label */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -101,14 +104,14 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
||||||
</span>
|
</span>
|
||||||
{/* Eyedropper / native picker */}
|
{/* Eyedropper / native picker */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<IconButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => nativeRef.current?.click()}
|
onClick={() => nativeRef.current?.click()}
|
||||||
className="p-1"
|
size="xs"
|
||||||
aria-label="Open system color picker"
|
aria-label="Open system color picker"
|
||||||
>
|
>
|
||||||
<PaletteIcon className="w-4 h-4 text-soft transform transition-transform duration-base ease-standard hover:text-accent" />
|
<PaletteIcon className="w-4 h-4 text-soft transform transition-transform duration-base ease-standard hover:text-accent" />
|
||||||
</button>
|
</IconButton>
|
||||||
<input
|
<input
|
||||||
ref={nativeRef}
|
ref={nativeRef}
|
||||||
type="color"
|
type="color"
|
||||||
|
|
@ -156,7 +159,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
||||||
borderColor: isLightColor(value) ? '#00000018' : '#ffffff18',
|
borderColor: isLightColor(value) ? '#00000018' : '#ffffff18',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={hexInput}
|
value={hexInput}
|
||||||
onChange={(e) => setHexInput(e.target.value)}
|
onChange={(e) => setHexInput(e.target.value)}
|
||||||
|
|
@ -166,7 +169,8 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
||||||
}}
|
}}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
maxLength={7}
|
maxLength={7}
|
||||||
className="flex-1 rounded-lg px-2 py-1 text-xs font-mono border border-line bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-accent-line"
|
controlSize="sm"
|
||||||
|
className="flex-1 font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import {
|
||||||
SidebarNavItem,
|
SidebarNavItem,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
Button,
|
Button,
|
||||||
|
ChoiceTile,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
ModalFrame,
|
ModalFrame,
|
||||||
|
|
@ -109,6 +110,54 @@ const CUSTOM_COLOR_FIELDS: { key: keyof CustomThemeColors; label: string }[] = [
|
||||||
{ key: 'muted', label: 'Muted' },
|
{ key: 'muted', label: 'Muted' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function ThemeSwatches({ colors }: { colors: ThemeColorSet }) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 ml-auto">
|
||||||
|
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
||||||
|
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
||||||
|
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
||||||
|
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
||||||
|
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThemeChoice({
|
||||||
|
label,
|
||||||
|
colors,
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
colors: ThemeColorSet;
|
||||||
|
selected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ChoiceTile
|
||||||
|
selected={selected}
|
||||||
|
onClick={onClick}
|
||||||
|
className={className}
|
||||||
|
style={{ backgroundColor: colors.base }}
|
||||||
|
>
|
||||||
|
{selected ? (
|
||||||
|
<CheckIcon className="h-3.5 w-3.5 shrink-0" style={{ color: colors.accent }} />
|
||||||
|
) : (
|
||||||
|
<span className="w-3.5 shrink-0" />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className="text-xs font-medium w-14 shrink-0"
|
||||||
|
style={{ color: colors.foreground }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<ThemeSwatches colors={colors} />
|
||||||
|
</ChoiceTile>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type SectionId = 'api' | 'theme' | 'docs' | 'account' | 'admin';
|
type SectionId = 'api' | 'theme' | 'docs' | 'account' | 'admin';
|
||||||
|
|
||||||
type SidebarSection = {
|
type SidebarSection = {
|
||||||
|
|
@ -880,34 +929,13 @@ export function SettingsModal({
|
||||||
const colors = getThemeColors(systemTheme.id);
|
const colors = getThemeColors(systemTheme.id);
|
||||||
const isActive = theme === systemTheme.id;
|
const isActive = theme === systemTheme.id;
|
||||||
return (
|
return (
|
||||||
<button
|
<ThemeChoice
|
||||||
|
label={systemTheme.name}
|
||||||
|
colors={colors}
|
||||||
|
selected={isActive}
|
||||||
onClick={() => setTheme(systemTheme.id)}
|
onClick={() => setTheme(systemTheme.id)}
|
||||||
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 w-full text-left transition duration-base ease-standard transform border
|
className="w-full"
|
||||||
${isActive
|
/>
|
||||||
? 'border-accent'
|
|
||||||
: 'border-line hover:border-accent-line'
|
|
||||||
}`}
|
|
||||||
style={{ backgroundColor: colors.base }}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<CheckIcon className="h-3.5 w-3.5 shrink-0 text-accent" />
|
|
||||||
) : (
|
|
||||||
<span className="w-3.5 shrink-0" />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className="text-xs font-medium w-14 shrink-0"
|
|
||||||
style={{ color: colors.foreground }}
|
|
||||||
>
|
|
||||||
{systemTheme.name}
|
|
||||||
</span>
|
|
||||||
<div className="flex gap-1 ml-auto">
|
|
||||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
|
||||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -921,37 +949,16 @@ export function SettingsModal({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<ThemeChoice
|
||||||
|
label="Custom"
|
||||||
|
colors={colors}
|
||||||
|
selected={isActive}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTheme('custom');
|
setTheme('custom');
|
||||||
setIsCustomExpanded(true);
|
setIsCustomExpanded(true);
|
||||||
}}
|
}}
|
||||||
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 flex-1 text-left transition duration-base ease-standard transform border
|
className="flex-1"
|
||||||
${isActive
|
/>
|
||||||
? 'border-accent'
|
|
||||||
: 'border-line hover:border-accent-line'
|
|
||||||
}`}
|
|
||||||
style={{ backgroundColor: colors.base }}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<CheckIcon className="h-3.5 w-3.5 shrink-0" style={{ color: colors.accent }} />
|
|
||||||
) : (
|
|
||||||
<span className="w-3.5 shrink-0" />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className="text-xs font-medium w-14 shrink-0"
|
|
||||||
style={{ color: colors.foreground }}
|
|
||||||
>
|
|
||||||
Custom
|
|
||||||
</span>
|
|
||||||
<div className="flex gap-1 ml-auto">
|
|
||||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
|
||||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCustomExpanded(!isCustomExpanded)}
|
onClick={() => setIsCustomExpanded(!isCustomExpanded)}
|
||||||
className="shrink-0 p-1.5 rounded-lg border border-line hover:border-accent-line transition-colors"
|
className="shrink-0 p-1.5 rounded-lg border border-line hover:border-accent-line transition-colors"
|
||||||
|
|
@ -1023,35 +1030,13 @@ export function SettingsModal({
|
||||||
const colors = getThemeColors(t.id);
|
const colors = getThemeColors(t.id);
|
||||||
const isActive = theme === t.id;
|
const isActive = theme === t.id;
|
||||||
return (
|
return (
|
||||||
<button
|
<ThemeChoice
|
||||||
key={t.id}
|
key={t.id}
|
||||||
|
label={t.name}
|
||||||
|
colors={colors}
|
||||||
|
selected={isActive}
|
||||||
onClick={() => setTheme(t.id)}
|
onClick={() => setTheme(t.id)}
|
||||||
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition duration-base ease-standard transform border
|
/>
|
||||||
${isActive
|
|
||||||
? 'border-accent'
|
|
||||||
: 'border-line hover:border-accent-line'
|
|
||||||
}`}
|
|
||||||
style={{ backgroundColor: colors.base }}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<CheckIcon className="h-3.5 w-3.5 shrink-0 text-accent" />
|
|
||||||
) : (
|
|
||||||
<span className="w-3.5 shrink-0" />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className="text-xs font-medium w-14 shrink-0"
|
|
||||||
style={{ color: colors.foreground }}
|
|
||||||
>
|
|
||||||
{t.name}
|
|
||||||
</span>
|
|
||||||
<div className="flex gap-1 ml-auto">
|
|
||||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
|
||||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1065,35 +1050,13 @@ export function SettingsModal({
|
||||||
const colors = getThemeColors(t.id);
|
const colors = getThemeColors(t.id);
|
||||||
const isActive = theme === t.id;
|
const isActive = theme === t.id;
|
||||||
return (
|
return (
|
||||||
<button
|
<ThemeChoice
|
||||||
key={t.id}
|
key={t.id}
|
||||||
|
label={t.name}
|
||||||
|
colors={colors}
|
||||||
|
selected={isActive}
|
||||||
onClick={() => setTheme(t.id)}
|
onClick={() => setTheme(t.id)}
|
||||||
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition duration-base ease-standard transform border
|
/>
|
||||||
${isActive
|
|
||||||
? 'border-accent'
|
|
||||||
: 'border-line hover:border-accent-line'
|
|
||||||
}`}
|
|
||||||
style={{ backgroundColor: colors.base }}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<CheckIcon className="h-3.5 w-3.5 shrink-0 text-accent" />
|
|
||||||
) : (
|
|
||||||
<span className="w-3.5 shrink-0" />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className="text-xs font-medium w-14 shrink-0"
|
|
||||||
style={{ color: colors.foreground }}
|
|
||||||
>
|
|
||||||
{t.name}
|
|
||||||
</span>
|
|
||||||
<div className="flex gap-1 ml-auto">
|
|
||||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
|
||||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
|
||||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,11 @@ import {
|
||||||
listboxOptionClass,
|
listboxOptionClass,
|
||||||
listboxOptionsClass,
|
listboxOptionsClass,
|
||||||
Button,
|
Button,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
MenuItemClass,
|
||||||
|
cn,
|
||||||
|
menuPanelClass,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
|
|
||||||
type ProviderType = TtsProviderId;
|
type ProviderType = TtsProviderId;
|
||||||
|
|
@ -641,7 +645,9 @@ export function AdminProvidersPanel() {
|
||||||
</div>
|
</div>
|
||||||
<Menu as="div" className="relative shrink-0">
|
<Menu as="div" className="relative shrink-0">
|
||||||
<MenuButton
|
<MenuButton
|
||||||
className="inline-flex items-center justify-center h-7 w-7 rounded-md border border-offbase bg-base text-muted hover:text-accent hover:bg-offbase transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-50"
|
as={IconButton}
|
||||||
|
tone="surface"
|
||||||
|
size="sm"
|
||||||
title="Provider actions"
|
title="Provider actions"
|
||||||
aria-label="Provider actions"
|
aria-label="Provider actions"
|
||||||
disabled={!!editingId || deleteMutation.isPending || toggleEnabledMutation.isPending || setDefaultMutation.isPending}
|
disabled={!!editingId || deleteMutation.isPending || toggleEnabledMutation.isPending || setDefaultMutation.isPending}
|
||||||
|
|
@ -659,14 +665,14 @@ export function AdminProvidersPanel() {
|
||||||
>
|
>
|
||||||
<MenuItems
|
<MenuItems
|
||||||
anchor="bottom end"
|
anchor="bottom end"
|
||||||
className="z-50 mt-2 min-w-[170px] rounded-md bg-base shadow-lg ring-1 ring-black/5 focus:outline-none p-1"
|
className={cn(menuPanelClass, 'z-50 mt-2 min-w-[170px] bg-base focus:outline-none')}
|
||||||
>
|
>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => startEdit(p)}
|
onClick={() => startEdit(p)}
|
||||||
className={`${active ? 'bg-offbase text-accent' : 'text-foreground'} flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
className={MenuItemClass(active)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -678,7 +684,7 @@ export function AdminProvidersPanel() {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDefault(p.slug)}
|
onClick={() => setDefault(p.slug)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`${disabled ? 'text-faint cursor-not-allowed' : active ? 'bg-offbase text-accent' : 'text-foreground'} flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
className={cn(MenuItemClass(active), disabled ? 'cursor-not-allowed text-faint' : '')}
|
||||||
>
|
>
|
||||||
Set as default
|
Set as default
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -689,7 +695,7 @@ export function AdminProvidersPanel() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleEnabled(p)}
|
onClick={() => toggleEnabled(p)}
|
||||||
className={`${active ? 'bg-offbase text-accent' : 'text-foreground'} flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
className={MenuItemClass(active)}
|
||||||
>
|
>
|
||||||
{p.enabled ? 'Disable' : 'Enable'}
|
{p.enabled ? 'Disable' : 'Enable'}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -700,7 +706,7 @@ export function AdminProvidersPanel() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => remove(p.id)}
|
onClick={() => remove(p.id)}
|
||||||
className={`${active ? 'bg-offbase' : ''} text-danger flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
className={MenuItemClass(active, 'danger')}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import type { Folder, SidebarFilter } from '@/types/documents';
|
||||||
import { PDFIcon, EPUBIcon, FileIcon, DotsHorizontalIcon } from '@/components/icons/Icons';
|
import { PDFIcon, EPUBIcon, FileIcon, DotsHorizontalIcon } from '@/components/icons/Icons';
|
||||||
import { FolderIcon, HomeIcon, ClockIcon, FolderPlusIcon } from './finderIcons';
|
import { FolderIcon, HomeIcon, ClockIcon, FolderPlusIcon } from './finderIcons';
|
||||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
||||||
import { IconButton, MenuItemClass, Sidebar as SidebarShell, SidebarNav, SidebarNavGroup, SidebarNavItem } from '@/components/ui';
|
import { IconButton, MenuItemClass, Sidebar as SidebarShell, SidebarNav, SidebarNavGroup, SidebarNavItem, menuPanelClass } from '@/components/ui';
|
||||||
|
|
||||||
interface FinderSidebarProps {
|
interface FinderSidebarProps {
|
||||||
filter: SidebarFilter;
|
filter: SidebarFilter;
|
||||||
|
|
@ -222,7 +222,7 @@ export function FinderSidebar({
|
||||||
>
|
>
|
||||||
<MenuItems
|
<MenuItems
|
||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
className="z-50 mt-2 min-w-[180px] rounded-md bg-surface shadow-elev-2 ring-1 ring-line-soft focus:outline-none p-1 normal-case tracking-normal font-normal"
|
className={`${menuPanelClass} z-50 mt-2 min-w-[180px] focus:outline-none normal-case tracking-normal font-normal`}
|
||||||
>
|
>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
|
|
|
||||||
32
src/components/ui/choice-tile.tsx
Normal file
32
src/components/ui/choice-tile.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
|
import { cn } from './cn';
|
||||||
|
import { focusRing, motionColors } from './tokens';
|
||||||
|
|
||||||
|
export function ChoiceTile({
|
||||||
|
selected = false,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
type = 'button',
|
||||||
|
...props
|
||||||
|
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||||
|
selected?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
aria-pressed={selected}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-lg border px-2 py-1.5 text-left',
|
||||||
|
'transition duration-base ease-standard',
|
||||||
|
focusRing,
|
||||||
|
motionColors,
|
||||||
|
selected ? 'border-accent-line' : 'border-line hover:border-accent-line',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * from './badge';
|
export * from './badge';
|
||||||
export * from './button';
|
export * from './button';
|
||||||
|
export * from './choice-tile';
|
||||||
export * from './cn';
|
export * from './cn';
|
||||||
export * from './dialog';
|
export * from './dialog';
|
||||||
export * from './divider';
|
export * from './divider';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue